diff --git a/src/Analysis/Ast/Impl/Analyzer/Definitions/IPythonAnalyzer.cs b/src/Analysis/Ast/Impl/Analyzer/Definitions/IPythonAnalyzer.cs index 0a4a23ddb..26cfce286 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Definitions/IPythonAnalyzer.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Definitions/IPythonAnalyzer.cs @@ -37,7 +37,12 @@ public interface IPythonAnalyzer { /// Invalidates current analysis for the module, assuming that AST for the new analysis will be provided later. /// void InvalidateAnalysis(IPythonModule module); - + + /// + /// Removes modules from the analysis. + /// + void RemoveAnalysis(IPythonModule module); + /// /// Get most recent analysis for module. If after specified time analysis isn't available, returns previously calculated analysis. /// diff --git a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Scopes.cs b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Scopes.cs index ca27a311e..a2877e8a9 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Scopes.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Scopes.cs @@ -175,7 +175,7 @@ public IDisposable OpenScope(IPythonModule module, ScopeStatement node, out Scop if (scope == null) { scope = new Scope(node, fromScope, true); fromScope.AddChildScope(scope); - _scopeLookupCache[node] = fromScope; + _scopeLookupCache[node] = scope; } } diff --git a/src/Analysis/Ast/Impl/Analyzer/PythonAnalyzer.cs b/src/Analysis/Ast/Impl/Analyzer/PythonAnalyzer.cs index d68a5b0cd..9929221af 100644 --- a/src/Analysis/Ast/Impl/Analyzer/PythonAnalyzer.cs +++ b/src/Analysis/Ast/Impl/Analyzer/PythonAnalyzer.cs @@ -112,6 +112,12 @@ public void InvalidateAnalysis(IPythonModule module) { } } + public void RemoveAnalysis(IPythonModule module) { + lock (_syncObj) { + _analysisEntries.Remove(new AnalysisModuleKey(module)); + } + } + public void EnqueueDocumentForAnalysis(IPythonModule module, ImmutableArray analysisDependencies) { var key = new AnalysisModuleKey(module); PythonAnalyzerEntry entry; @@ -269,7 +275,7 @@ private async Task AnalyzeAffectedEntriesAsync(IDependencyChainWalker missingKeys) { foreach (var (moduleName, _, isTypeshed) in missingKeys) { var moduleResolution = isTypeshed ? interpreter.TypeshedResolution : interpreter.ModuleResolution; @@ -356,7 +362,7 @@ private void AnalyzeEntry(PythonAnalyzerEntry entry, IPythonModule module, Pytho // Python analyzer to call NotifyAnalysisComplete. walker.Complete(); cancellationToken.ThrowIfCancellationRequested(); - var analysis = new DocumentAnalysis((IDocument) module, version, walker.GlobalScope, walker.Eval); + var analysis = new DocumentAnalysis((IDocument)module, version, walker.GlobalScope, walker.Eval); (module as IAnalyzable)?.NotifyAnalysisComplete(analysis); entry.TrySetAnalysis(analysis, version); diff --git a/src/Analysis/Ast/Impl/Analyzer/PythonInterpreter.cs b/src/Analysis/Ast/Impl/Analyzer/PythonInterpreter.cs index 3745ce3b5..dd387a924 100644 --- a/src/Analysis/Ast/Impl/Analyzer/PythonInterpreter.cs +++ b/src/Analysis/Ast/Impl/Analyzer/PythonInterpreter.cs @@ -47,18 +47,19 @@ private async Task LoadBuiltinTypesAsync(string root, IServiceManager sm, Cancel sm.AddService(this); _moduleResolution = new MainModuleResolution(root, sm); - await _moduleResolution.InitializeAsync(cancellationToken); - _stubResolution = new TypeshedResolution(sm); - await _stubResolution.InitializeAsync(cancellationToken); - - var builtinModule = _moduleResolution.BuiltinsModule; lock (_lock) { + var builtinModule = _moduleResolution.CreateBuiltinsModule(); _builtinTypes[BuiltinTypeId.NoneType] = new PythonType("NoneType", builtinModule, string.Empty, LocationInfo.Empty, BuiltinTypeId.NoneType); _builtinTypes[BuiltinTypeId.Unknown] = UnknownType = new PythonType("Unknown", builtinModule, string.Empty, LocationInfo.Empty); } + await _moduleResolution.InitializeAsync(cancellationToken); + + _stubResolution = new TypeshedResolution(sm); + await _stubResolution.InitializeAsync(cancellationToken); + await _moduleResolution.LoadBuiltinTypesAsync(cancellationToken); } diff --git a/src/Analysis/Ast/Impl/Dependencies/DependencyResolver.cs b/src/Analysis/Ast/Impl/Dependencies/DependencyResolver.cs index e10301434..3f50a626f 100644 --- a/src/Analysis/Ast/Impl/Dependencies/DependencyResolver.cs +++ b/src/Analysis/Ast/Impl/Dependencies/DependencyResolver.cs @@ -21,7 +21,6 @@ namespace Microsoft.Python.Analysis.Dependencies { internal sealed class DependencyResolver : IDependencyResolver { - private readonly IDependencyFinder _dependencyFinder; private readonly DependencyGraph _vertices = new DependencyGraph(); private readonly Dictionary> _changedVertices = new Dictionary>(); private readonly object _syncObj = new object(); diff --git a/src/Analysis/Ast/Impl/Documents/Definitions/IRunningDocumentTable.cs b/src/Analysis/Ast/Impl/Documents/Definitions/IRunningDocumentTable.cs index 6164f3125..465998596 100644 --- a/src/Analysis/Ast/Impl/Documents/Definitions/IRunningDocumentTable.cs +++ b/src/Analysis/Ast/Impl/Documents/Definitions/IRunningDocumentTable.cs @@ -55,6 +55,20 @@ public interface IRunningDocumentTable: IEnumerable { /// IDocument GetDocument(string name); + /// + /// Increase reference count of the document. + /// + /// + /// New lock count or -1 if document was not found. + int LockDocument(Uri uri); + + /// + /// Decrease reference count of the document. + /// + /// + /// New lock count or -1 if document was not found. + int UnlockDocument(Uri uri); + /// /// Fires when document is opened. /// @@ -64,5 +78,10 @@ public interface IRunningDocumentTable: IEnumerable { /// Fires when document is closed. /// event EventHandler Closed; + + /// + /// Fires when document is removed. + /// + event EventHandler Removed; } } diff --git a/src/Analysis/Ast/Impl/Documents/RunningDocumentTable.cs b/src/Analysis/Ast/Impl/Documents/RunningDocumentTable.cs index 389bf6c32..a267be97b 100644 --- a/src/Analysis/Ast/Impl/Documents/RunningDocumentTable.cs +++ b/src/Analysis/Ast/Impl/Documents/RunningDocumentTable.cs @@ -19,9 +19,9 @@ using System.Diagnostics; using System.IO; using System.Linq; +using Microsoft.Python.Analysis.Analyzer; using Microsoft.Python.Analysis.Modules; using Microsoft.Python.Core; -using Microsoft.Python.Core.Diagnostics; namespace Microsoft.Python.Analysis.Documents { /// @@ -52,6 +52,7 @@ public RunningDocumentTable(string workspaceRoot, IServiceContainer services) { public event EventHandler Opened; public event EventHandler Closed; + public event EventHandler Removed; /// /// Adds file to the list of available documents. @@ -60,17 +61,19 @@ public RunningDocumentTable(string workspaceRoot, IServiceContainer services) { /// Document content /// Optional file path, if different from the URI. public IDocument OpenDocument(Uri uri, string content, string filePath = null) { - var justOpened = false; + bool justOpened; DocumentEntry entry; lock (_lock) { entry = FindDocument(null, uri); if (entry == null) { + var resolver = _services.GetService().ModuleResolution.CurrentPathResolver; + var moduleType = resolver.IsLibraryFile(uri.ToAbsolutePath()) ? ModuleType.Library : ModuleType.User; var mco = new ModuleCreationOptions { ModuleName = Path.GetFileNameWithoutExtension(uri.LocalPath), Content = content, FilePath = filePath, Uri = uri, - ModuleType = ModuleType.User + ModuleType = moduleType }; entry = CreateDocument(mco); } @@ -114,10 +117,29 @@ public IDocument GetDocument(string name) { } } + public int LockDocument(Uri uri) { + lock (_lock) { + if (_documentsByUri.TryGetValue(uri, out var entry)) { + return ++entry.LockCount; + } + return -1; + } + } + + public int UnlockDocument(Uri uri) { + lock (_lock) { + if (_documentsByUri.TryGetValue(uri, out var entry)) { + return --entry.LockCount; + } + return -1; + } + } + public IEnumerator GetEnumerator() => _documentsByUri.Values.Select(e => e.Document).GetEnumerator(); public void CloseDocument(Uri documentUri) { - var justClosed = false; + var closed = false; + var removed = false; DocumentEntry entry; lock (_lock) { if (_documentsByUri.TryGetValue(documentUri, out entry)) { @@ -125,7 +147,7 @@ public void CloseDocument(Uri documentUri) { if (entry.Document.IsOpen) { entry.Document.IsOpen = false; - justClosed = true; + closed = true; } entry.LockCount--; @@ -133,20 +155,23 @@ public void CloseDocument(Uri documentUri) { if (entry.LockCount == 0) { _documentsByUri.Remove(documentUri); _documentsByName.Remove(entry.Document.Name); + removed = true; entry.Document.Dispose(); } - // TODO: Remove from module resolution? } } - if(justClosed) { + if (closed) { Closed?.Invoke(this, new DocumentEventArgs(entry.Document)); } + if (removed) { + Removed?.Invoke(this, new DocumentEventArgs(entry.Document)); + } } IEnumerator IEnumerable.GetEnumerator() => _documentsByUri.Values.GetEnumerator(); public void Dispose() { - lock(_lock) { + lock (_lock) { foreach (var d in _documentsByUri.Values.OfType()) { d.Dispose(); } diff --git a/src/Analysis/Ast/Impl/Modules/Definitions/IModuleCache.cs b/src/Analysis/Ast/Impl/Modules/Definitions/IModuleCache.cs index b4d4a231a..40ea4c0a6 100644 --- a/src/Analysis/Ast/Impl/Modules/Definitions/IModuleCache.cs +++ b/src/Analysis/Ast/Impl/Modules/Definitions/IModuleCache.cs @@ -13,10 +13,6 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Python.Analysis.Documents; - namespace Microsoft.Python.Analysis.Modules { public interface IModuleCache { string GetCacheFilePath(string filePath); diff --git a/src/Analysis/Ast/Impl/Modules/Definitions/IModuleResolution.cs b/src/Analysis/Ast/Impl/Modules/Definitions/IModuleResolution.cs index 67aa559b4..e0959f44d 100644 --- a/src/Analysis/Ast/Impl/Modules/Definitions/IModuleResolution.cs +++ b/src/Analysis/Ast/Impl/Modules/Definitions/IModuleResolution.cs @@ -13,7 +13,6 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Python.Analysis.Core.DependencyResolution; diff --git a/src/Analysis/Ast/Impl/Modules/ModuleCache.cs b/src/Analysis/Ast/Impl/Modules/ModuleCache.cs index 3fa18a348..4052283c7 100644 --- a/src/Analysis/Ast/Impl/Modules/ModuleCache.cs +++ b/src/Analysis/Ast/Impl/Modules/ModuleCache.cs @@ -18,9 +18,7 @@ using System.IO; using System.Security.Cryptography; using System.Text; -using System.Threading; using System.Threading.Tasks; -using Microsoft.Python.Analysis.Documents; using Microsoft.Python.Core; using Microsoft.Python.Core.IO; using Microsoft.Python.Core.Logging; @@ -43,7 +41,7 @@ public ModuleCache(IPythonInterpreter interpreter, IServiceContainer services) { _log = services.GetService(); _skipCache = string.IsNullOrEmpty(_interpreter.Configuration.DatabasePath); } - + public string GetCacheFilePath(string filePath) { if (string.IsNullOrEmpty(filePath) || !PathEqualityComparer.IsValidPath(ModuleCachePath)) { if (!_loggedBadDbPath) { diff --git a/src/Analysis/Ast/Impl/Modules/PythonModule.cs b/src/Analysis/Ast/Impl/Modules/PythonModule.cs index b5fb1fa60..07824e2b2 100644 --- a/src/Analysis/Ast/Impl/Modules/PythonModule.cs +++ b/src/Analysis/Ast/Impl/Modules/PythonModule.cs @@ -29,6 +29,7 @@ using Microsoft.Python.Analysis.Values; using Microsoft.Python.Core; using Microsoft.Python.Core.Diagnostics; +using Microsoft.Python.Core.Disposables; using Microsoft.Python.Core.IO; using Microsoft.Python.Core.Logging; using Microsoft.Python.Core.Text; @@ -53,7 +54,7 @@ private enum State { } private readonly DocumentBuffer _buffer = new DocumentBuffer(); - private readonly CancellationTokenSource _allProcessingCts = new CancellationTokenSource(); + private readonly DisposeToken _disposeToken = DisposeToken.Create< PythonModule>(); private IReadOnlyList _parseErrors = Array.Empty(); private readonly IDiagnosticsService _diagnosticsService; @@ -242,8 +243,7 @@ private void LoadContent(string content) { protected virtual void Dispose(bool disposing) { _diagnosticsService?.Remove(Uri); - _allProcessingCts.Cancel(); - _allProcessingCts.Dispose(); + _disposeToken.TryMarkDisposed(); } #endregion @@ -310,7 +310,7 @@ public void Update(IEnumerable changes) { _parseCts = new CancellationTokenSource(); _linkedParseCts?.Dispose(); - _linkedParseCts = CancellationTokenSource.CreateLinkedTokenSource(_allProcessingCts.Token, _parseCts.Token); + _linkedParseCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeToken.CancellationToken, _parseCts.Token); _buffer.Update(changes); Parse(); @@ -332,7 +332,7 @@ private void Parse() { _parseCts = new CancellationTokenSource(); _linkedParseCts?.Dispose(); - _linkedParseCts = CancellationTokenSource.CreateLinkedTokenSource(_allProcessingCts.Token, _parseCts.Token); + _linkedParseCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeToken.CancellationToken, _parseCts.Token); ContentState = State.Parsing; _parsingTask = Task.Run(() => Parse(_linkedParseCts.Token), _linkedParseCts.Token); @@ -383,7 +383,7 @@ private void Parse(CancellationToken cancellationToken) { ContentState = State.Analyzing; var analyzer = Services.GetService(); - analyzer.EnqueueDocumentForAnalysis(this, ast, version, _allProcessingCts.Token); + analyzer.EnqueueDocumentForAnalysis(this, ast, version, _disposeToken.CancellationToken); } lock (AnalysisLock) { diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs index 9a951daa2..e13d613d6 100644 --- a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs +++ b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs @@ -34,23 +34,30 @@ namespace Microsoft.Python.Analysis.Modules.Resolution { internal sealed class MainModuleResolution : ModuleResolutionBase, IModuleManagement { private readonly ConcurrentDictionary _specialized = new ConcurrentDictionary(); + private IRunningDocumentTable _rdt; private IReadOnlyList _searchPaths; public MainModuleResolution(string root, IServiceContainer services) : base(root, services) { } + internal IBuiltinsPythonModule CreateBuiltinsModule() { + if (BuiltinsModule == null) { + // Initialize built-in + var moduleName = BuiltinTypeId.Unknown.GetModuleName(_interpreter.LanguageVersion); + + ModuleCache = new ModuleCache(_interpreter, _services); + var modulePath = ModuleCache.GetCacheFilePath(_interpreter.Configuration.InterpreterPath); + + var b = new BuiltinsPythonModule(moduleName, modulePath, _services); + BuiltinsModule = b; + Modules[BuiltinModuleName] = new ModuleRef(b); + } + return BuiltinsModule; + } + internal async Task InitializeAsync(CancellationToken cancellationToken = default) { - // Add names from search paths await ReloadAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - - // Initialize built-in - var moduleName = BuiltinTypeId.Unknown.GetModuleName(_interpreter.LanguageVersion); - var modulePath = ModuleCache.GetCacheFilePath(_interpreter.Configuration.InterpreterPath); - - var b = new BuiltinsPythonModule(moduleName, modulePath, _services); - BuiltinsModule = b; - Modules[BuiltinModuleName] = new ModuleRef(b); } public async Task> GetSearchPathsAsync(CancellationToken cancellationToken = default) { @@ -60,16 +67,19 @@ public async Task> GetSearchPathsAsync(CancellationToken c _searchPaths = await GetInterpreterSearchPathsAsync(cancellationToken); Debug.Assert(_searchPaths != null, "Should have search paths"); - _searchPaths = _searchPaths != null - ? Configuration.SearchPaths != null - ? _searchPaths.Concat(Configuration.SearchPaths).ToArray() - : _searchPaths - : Array.Empty(); + _searchPaths = _searchPaths ?? Array.Empty(); - _log?.Log(TraceEventType.Information, "SearchPaths:"); + _log?.Log(TraceEventType.Information, "Python search paths:"); foreach (var s in _searchPaths) { _log?.Log(TraceEventType.Information, $" {s}"); } + + var configurationSearchPaths = Configuration.SearchPaths ?? Array.Empty(); + + _log?.Log(TraceEventType.Information, "Configuration search paths:"); + foreach (var s in configurationSearchPaths) { + _log?.Log(TraceEventType.Information, $" {s}"); + } return _searchPaths; } @@ -80,11 +90,11 @@ protected override IPythonModule CreateModule(string name) { return null; } - var rdt = _services.GetService(); IPythonModule module; if (!string.IsNullOrEmpty(moduleImport.ModulePath) && Uri.TryCreate(moduleImport.ModulePath, UriKind.Absolute, out var uri)) { - module = rdt.GetDocument(uri); + module = GetRdt().GetDocument(uri); if (module != null) { + GetRdt().LockDocument(uri); return module; } } @@ -117,7 +127,7 @@ protected override IPythonModule CreateModule(string name) { FilePath = moduleImport.ModulePath, Stub = stub }; - module = rdt.AddModule(mco); + module = GetRdt().AddModule(mco); } return module; @@ -176,9 +186,10 @@ internal async Task LoadBuiltinTypesAsync(CancellationToken cancellationToken = } public async Task ReloadAsync(CancellationToken cancellationToken = default) { + foreach (var m in Modules) { + GetRdt()?.UnlockDocument(m.Value.Value.Uri); + } Modules.Clear(); - - ModuleCache = new ModuleCache(_interpreter, _services); PathResolver = new PathResolver(_interpreter.LanguageVersion); var addedRoots = new HashSet(); @@ -187,7 +198,8 @@ public async Task ReloadAsync(CancellationToken cancellationToken = default) { var interpreterPaths = await GetSearchPathsAsync(cancellationToken); addedRoots.UnionWith(PathResolver.SetInterpreterSearchPaths(interpreterPaths)); - addedRoots.UnionWith(SetUserSearchPaths(_interpreter.Configuration.SearchPaths)); + var userSearchPaths = _interpreter.Configuration.SearchPaths.Except(interpreterPaths, StringExtensions.PathsStringComparer); + addedRoots.UnionWith(SetUserSearchPaths(userSearchPaths)); ReloadModulePaths(addedRoots); } @@ -213,5 +225,8 @@ private bool TryCreateModuleStub(string name, string modulePath, out IPythonModu module = !string.IsNullOrEmpty(stubPath) ? new StubPythonModule(name, stubPath, false, _services) : null; return module != null; } + + private IRunningDocumentTable GetRdt() + => _rdt ?? (_rdt = _services.GetService()); } } diff --git a/src/Analysis/Ast/Test/FluentAssertions/DocumentAnalysisAssertions.cs b/src/Analysis/Ast/Test/FluentAssertions/DocumentAnalysisAssertions.cs index 6f04641b6..e2f06b179 100644 --- a/src/Analysis/Ast/Test/FluentAssertions/DocumentAnalysisAssertions.cs +++ b/src/Analysis/Ast/Test/FluentAssertions/DocumentAnalysisAssertions.cs @@ -22,7 +22,7 @@ namespace Microsoft.Python.Analysis.Tests.FluentAssertions { [ExcludeFromCodeCoverage] - internal sealed class DocumentAnalysisAssertions : ReferenceTypeAssertions { + public sealed class DocumentAnalysisAssertions : ReferenceTypeAssertions { private readonly ScopeAssertions _scopeAssertions; public DocumentAnalysisAssertions(IDocumentAnalysis analysis) { diff --git a/src/Analysis/Ast/Test/FluentAssertions/VariableAssertions.cs b/src/Analysis/Ast/Test/FluentAssertions/VariableAssertions.cs index 269948ba9..6f0aac0c1 100644 --- a/src/Analysis/Ast/Test/FluentAssertions/VariableAssertions.cs +++ b/src/Analysis/Ast/Test/FluentAssertions/VariableAssertions.cs @@ -22,7 +22,7 @@ using Microsoft.Python.Analysis.Values; namespace Microsoft.Python.Analysis.Tests.FluentAssertions { - internal sealed class VariableAssertions : ReferenceTypeAssertions { + public sealed class VariableAssertions : ReferenceTypeAssertions { public IMember Value { get; } public VariableAssertions(IVariable v) { Subject = v; diff --git a/src/Analysis/Ast/Test/FluentAssertions/VariableAssertionsExtensions.cs b/src/Analysis/Ast/Test/FluentAssertions/VariableAssertionsExtensions.cs index 055b877e7..5a965b91e 100644 --- a/src/Analysis/Ast/Test/FluentAssertions/VariableAssertionsExtensions.cs +++ b/src/Analysis/Ast/Test/FluentAssertions/VariableAssertionsExtensions.cs @@ -21,7 +21,7 @@ namespace Microsoft.Python.Analysis.Tests.FluentAssertions { [ExcludeFromCodeCoverage] - internal static class VariableAssertionsExtensions { + public static class VariableAssertionsExtensions { public static AndWhichConstraint OfType( this AndWhichConstraint andWhichConstraint, BuiltinTypeId typeId, string because = "", params object[] reasonArgs) { andWhichConstraint.Subject.Value.Should().HaveType(typeId, because, reasonArgs); diff --git a/src/Analysis/Core/Impl/DependencyResolution/PathResolverSnapshot.cs b/src/Analysis/Core/Impl/DependencyResolution/PathResolverSnapshot.cs index dfa81d7c2..bed14e386 100644 --- a/src/Analysis/Core/Impl/DependencyResolution/PathResolverSnapshot.cs +++ b/src/Analysis/Core/Impl/DependencyResolution/PathResolverSnapshot.cs @@ -73,7 +73,7 @@ private IEnumerable GetModuleNames(IEnumerable roots) => roots .Where(n => n.IsModule) .Concat(_builtins.Children) .Select(n => n.FullModuleName); - + public ModuleImport GetModuleImportFromModuleName(in string fullModuleName) { for (var rootIndex = 0; rootIndex < _roots.Count; rootIndex++) { if (TryFindModuleByName(rootIndex, fullModuleName, out var lastEdge) && TryCreateModuleImport(lastEdge, out var moduleImports)) { @@ -237,6 +237,9 @@ public IImportSearchResult GetImportsFromRelativePath(in string modulePath, in i return new ImportNotFound(fullName); } + public bool IsLibraryFile(string filePath) + => TryFindModule(filePath, out var edge, out _) && IsLibraryPath(edge.FirstEdge.End.Name); + private bool TryGetSearchResults(in ImmutableArray matchedEdges, out IImportSearchResult searchResult) { foreach (var edge in matchedEdges) { if (TryCreateModuleImport(edge, out var moduleImport)) { @@ -253,7 +256,7 @@ private bool TryGetSearchResults(in ImmutableArray matchedEdges, out IImpo return false; } - private bool TryCreateModuleImport(Edge lastEdge, out ModuleImport moduleImport) { + private bool TryCreateModuleImport(Edge lastEdge, out ModuleImport moduleImport) { var moduleNode = lastEdge.End; var rootNode = lastEdge.FirstEdge.End; @@ -265,7 +268,7 @@ private bool TryCreateModuleImport(Edge lastEdge, out ModuleImport moduleImport) rootNode.Name, initPyNode.ModulePath, false, - IsLibrary(rootNode.Name)); + IsLibraryPath(rootNode.Name)); return true; } @@ -278,7 +281,7 @@ private bool TryCreateModuleImport(Edge lastEdge, out ModuleImport moduleImport) rootNode.Name, moduleNode.ModulePath, IsPythonCompiled(moduleNode.ModulePath), - IsLibrary(rootNode.Name)); + IsLibraryPath(rootNode.Name)); return true; } @@ -765,8 +768,9 @@ private static bool IsNotInitPy(in Node node) private static bool IsInitPyModule(in Node node, out Node initPyNode) => node.TryGetChild("__init__", out initPyNode) && initPyNode.IsModule; - private bool IsLibrary(string rootPath) - => _interpreterSearchPaths.Contains(rootPath, StringExtensions.PathsStringComparer); + private bool IsLibraryPath(string rootPath) + => !_userSearchPaths.Contains(rootPath, StringExtensions.PathsStringComparer) + && _interpreterSearchPaths.Except(_userSearchPaths).Contains(rootPath, StringExtensions.PathsStringComparer); private PathResolverSnapshot ReplaceNonRooted(Node nonRooted) => new PathResolverSnapshot(_pythonLanguageVersion, _workDirectory, _userSearchPaths, _interpreterSearchPaths, _roots, _userRootsCount, nonRooted, _builtins, Version + 1); diff --git a/src/Core/Impl/Threading/AsyncManualResetEvent.cs b/src/Core/Impl/Threading/AsyncManualResetEvent.cs index 00e301ea7..46152e1e2 100644 --- a/src/Core/Impl/Threading/AsyncManualResetEvent.cs +++ b/src/Core/Impl/Threading/AsyncManualResetEvent.cs @@ -13,8 +13,6 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. -using System; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs b/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs index 037cc700b..ada637d60 100644 --- a/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs +++ b/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs @@ -75,8 +75,12 @@ public DiagnosticsService(IServiceContainer services) { public IReadOnlyDictionary> Diagnostics { get { lock (_lock) { - return _diagnostics.ToDictionary(kvp => kvp.Key, - kvp => FilterBySeverityMap(kvp.Value).ToList() as IReadOnlyList); + return _diagnostics.ToDictionary( + kvp => kvp.Key, + kvp => Rdt.GetDocument(kvp.Key)?.IsOpen == true + ? FilterBySeverityMap(kvp.Value).ToList() as IReadOnlyList + : Array.Empty() + ); } } } @@ -205,29 +209,34 @@ private void ConnectToRdt() { if (_rdt != null) { _rdt.Opened += OnOpenDocument; _rdt.Closed += OnCloseDocument; + _rdt.Removed += OnRemoveDocument; _disposables .Add(() => _rdt.Opened -= OnOpenDocument) - .Add(() => _rdt.Closed -= OnCloseDocument); + .Add(() => _rdt.Closed -= OnCloseDocument) + .Add(() => _rdt.Removed -= OnRemoveDocument); } } } private void OnOpenDocument(object sender, DocumentEventArgs e) { lock (_lock) { - if(_diagnostics.TryGetValue(e.Document.Uri, out var d)) { + if (_diagnostics.TryGetValue(e.Document.Uri, out var d)) { d.Changed = d.Entries.Length > 0; } } } - private void OnCloseDocument(object sender, DocumentEventArgs e) { + private void OnCloseDocument(object sender, DocumentEventArgs e) => ClearDiagnostics(e.Document.Uri, false); + private void OnRemoveDocument(object sender, DocumentEventArgs e) => ClearDiagnostics(e.Document.Uri, true); + + private void ClearDiagnostics(Uri uri, bool remove) { lock (_lock) { - // Before removing the document, make sure we clear its diagnostics. - if (_diagnostics.TryGetValue(e.Document.Uri, out var d)) { - d.Clear(); + if (_diagnostics.TryGetValue(uri, out var _)) { PublishDiagnostics(); - _diagnostics.Remove(e.Document.Uri); + if (remove) { + _diagnostics.Remove(uri); + } } } } diff --git a/src/LanguageServer/Impl/Implementation/Server.Documents.cs b/src/LanguageServer/Impl/Implementation/Server.Documents.cs index 6305645d7..5aac074de 100644 --- a/src/LanguageServer/Impl/Implementation/Server.Documents.cs +++ b/src/LanguageServer/Impl/Implementation/Server.Documents.cs @@ -56,9 +56,13 @@ public void DidChangeTextDocument(DidChangeTextDocumentParams @params) { } public void DidChangeWatchedFiles(DidChangeWatchedFilesParams @params) { + _disposableBag.ThrowIfDisposed(); foreach (var c in @params.changes.MaybeEnumerate()) { - _disposableBag.ThrowIfDisposed(); - // TODO: handle? + switch (c.type) { + case FileChangeType.Deleted: + _interpreter.ModuleResolution.CurrentPathResolver.RemoveModulePath(c.uri.ToAbsolutePath()); + break; + } } } diff --git a/src/LanguageServer/Test/DiagnosticsTests.cs b/src/LanguageServer/Test/DiagnosticsTests.cs index bf674f609..9000a8d15 100644 --- a/src/LanguageServer/Test/DiagnosticsTests.cs +++ b/src/LanguageServer/Test/DiagnosticsTests.cs @@ -13,13 +13,11 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Python.Analysis.Diagnostics; using Microsoft.Python.Analysis.Documents; -using Microsoft.Python.Core.Idle; using Microsoft.Python.Core.Services; using Microsoft.Python.Core.Text; using Microsoft.Python.LanguageServer.Protocol; @@ -249,17 +247,5 @@ public async Task OnlyPublishChangedFile() { reported[0].uri.Should().Be(doc1.Uri); reported[0].diagnostics.Length.Should().Be(1); } - - private IDiagnosticsService GetDiagnosticsService() { - var ds = Services.GetService(); - ds.PublishingDelay = 0; - return ds; - } - private void PublishDiagnostics() { - var ds = Services.GetService(); - ds.PublishingDelay = 0; - var idle = Services.GetService(); - idle.Idle += Raise.EventWith(null, EventArgs.Empty); - } } } diff --git a/src/LanguageServer/Test/FluentAssertions/AssertionsFactory.cs b/src/LanguageServer/Test/FluentAssertions/AssertionsFactory.cs index 50c29fb39..5054053aa 100644 --- a/src/LanguageServer/Test/FluentAssertions/AssertionsFactory.cs +++ b/src/LanguageServer/Test/FluentAssertions/AssertionsFactory.cs @@ -15,7 +15,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Python.Analysis; using Microsoft.Python.Analysis.Tests.FluentAssertions; +using Microsoft.Python.Analysis.Values; using Microsoft.Python.Core.Text; using Microsoft.Python.LanguageServer.Completion; using Microsoft.Python.LanguageServer.Protocol; @@ -43,5 +45,8 @@ public static SignatureInformationAssertions Should(this SignatureInformation si public static TextEditCollectionAssertions Should(this IEnumerable textEdits) => new TextEditCollectionAssertions(textEdits); + + public static DocumentAnalysisAssertions Should(this IDocumentAnalysis analysis) => new DocumentAnalysisAssertions(analysis); + public static VariableAssertions Should(this IVariable v) => new VariableAssertions(v); } } diff --git a/src/LanguageServer/Test/LanguageServerTestBase.cs b/src/LanguageServer/Test/LanguageServerTestBase.cs index 88c882da8..8537b2d81 100644 --- a/src/LanguageServer/Test/LanguageServerTestBase.cs +++ b/src/LanguageServer/Test/LanguageServerTestBase.cs @@ -13,14 +13,33 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. +using System; using Microsoft.Python.Analysis.Diagnostics; using Microsoft.Python.Analysis.Tests; using Microsoft.Python.Core; +using Microsoft.Python.Core.Idle; using Microsoft.Python.LanguageServer.Diagnostics; +using NSubstitute; namespace Microsoft.Python.LanguageServer.Tests { public abstract class LanguageServerTestBase : AnalysisTestBase { protected static readonly ServerSettings ServerSettings = new ServerSettings(); protected override IDiagnosticsService GetDiagnosticsService(IServiceContainer s) => new DiagnosticsService(s); + + + protected IDiagnosticsService GetDiagnosticsService() { + var ds = Services.GetService(); + ds.PublishingDelay = 0; + return ds; + } + protected void PublishDiagnostics() { + GetDiagnosticsService(); + RaiseIdleEvent(); + } + + protected void RaiseIdleEvent() { + var idle = Services.GetService(); + idle.Idle += Raise.EventWith(null, EventArgs.Empty); + } } } diff --git a/src/LanguageServer/Test/RdtTests.cs b/src/LanguageServer/Test/RdtTests.cs index a4289944e..7ca457416 100644 --- a/src/LanguageServer/Test/RdtTests.cs +++ b/src/LanguageServer/Test/RdtTests.cs @@ -13,12 +13,19 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. +using System; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Python.Analysis.Analyzer; using Microsoft.Python.Analysis.Diagnostics; using Microsoft.Python.Analysis.Documents; +using Microsoft.Python.Analysis.Tests.FluentAssertions; +using Microsoft.Python.Analysis.Types; using Microsoft.Python.Parsing.Tests; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Python.LanguageServer.Tests.FluentAssertions; using TestUtilities; namespace Microsoft.Python.LanguageServer.Tests { @@ -63,5 +70,91 @@ public async Task OpenCloseDocuments() { rdt.GetDocument(uri1).Should().BeNull(); ds.Diagnostics.TryGetValue(uri1, out _).Should().BeFalse(); } + + [TestMethod, Priority(0)] + public async Task LockCount() { + var uri1 = TestData.GetDefaultModuleUri(); + await CreateServicesAsync(PythonVersions.LatestAvailable3X, uri1.AbsolutePath); + var rdt = Services.GetService(); + + rdt.OpenDocument(uri1, "from LockCount1 import *"); + var doc = rdt.GetDocument(uri1); + await doc.GetAstAsync(CancellationToken.None); + await Services.GetService().WaitForCompleteAnalysisAsync(); + + var docLc1 = rdt.First(d => d.Name.Contains("LockCount1")); + var docLc2 = rdt.First(d => d.Name.Contains("LockCount2")); + var docLc3 = rdt.First(d => d.Name.Contains("LockCount3")); + + VerifyLockCount(rdt, docLc1.Uri, 1); + VerifyLockCount(rdt, docLc2.Uri, 1); + VerifyLockCount(rdt, docLc3.Uri, 1); + + rdt.OpenDocument(docLc1.Uri, null); + VerifyLockCount(rdt, docLc1.Uri, 2); + + rdt.OpenDocument(docLc3.Uri, null); + VerifyLockCount(rdt, docLc3.Uri, 2); + + rdt.CloseDocument(docLc1.Uri); + VerifyLockCount(rdt, docLc1.Uri, 1); + + rdt.CloseDocument(docLc3.Uri); + VerifyLockCount(rdt, docLc3.Uri, 1); + } + + [TestMethod, Priority(0)] + public async Task OpenCloseAnalysis() { + var uri = TestData.GetDefaultModuleUri(); + await CreateServicesAsync(PythonVersions.LatestAvailable3X, uri.AbsolutePath); + var rdt = Services.GetService(); + + rdt.OpenDocument(uri, "from LockCount1 import *"); + var doc = rdt.GetDocument(uri); + await doc.GetAstAsync(CancellationToken.None); + await Services.GetService().WaitForCompleteAnalysisAsync(); + + var docLc1 = rdt.First(d => d.Name.Contains("LockCount1")); + var docLc2 = rdt.First(d => d.Name.Contains("LockCount2")); + var docLc3 = rdt.First(d => d.Name.Contains("LockCount3")); + + var ds = GetDiagnosticsService(); + PublishDiagnostics(); + ds.Diagnostics.Count.Should().Be(4); + ds.Diagnostics[uri].Should().BeEmpty(); + ds.Diagnostics[docLc1.Uri].Should().BeEmpty(); + ds.Diagnostics[docLc2.Uri].Should().BeEmpty(); + ds.Diagnostics[docLc3.Uri].Should().BeEmpty(); + + rdt.OpenDocument(docLc1.Uri, null); + PublishDiagnostics(); + ds.Diagnostics[uri].Should().BeEmpty(); + ds.Diagnostics[docLc1.Uri].Count.Should().Be(2); + ds.Diagnostics[docLc2.Uri].Should().BeEmpty(); + ds.Diagnostics[docLc3.Uri].Should().BeEmpty(); + + rdt.CloseDocument(docLc1.Uri); + PublishDiagnostics(); + ds.Diagnostics[uri].Should().BeEmpty(); + ds.Diagnostics[docLc1.Uri].Should().BeEmpty(); + ds.Diagnostics[docLc2.Uri].Should().BeEmpty(); + ds.Diagnostics[docLc3.Uri].Should().BeEmpty(); + + rdt.OpenDocument(docLc1.Uri, null); + var analysis = await docLc1.GetAnalysisAsync(-1); + analysis.Should().HaveVariable("y").OfType(BuiltinTypeId.Int); + + PublishDiagnostics(); + ds.Diagnostics[uri].Should().BeEmpty(); + ds.Diagnostics[docLc1.Uri].Count.Should().Be(2); + ds.Diagnostics[docLc2.Uri].Should().BeEmpty(); + ds.Diagnostics[docLc3.Uri].Should().BeEmpty(); + } + + + private void VerifyLockCount(IRunningDocumentTable rdt, Uri uri, int expected) { + rdt.LockDocument(uri).Should().Be(expected + 1); + rdt.UnlockDocument(uri); + } } } diff --git a/src/UnitTests/TestData/AstAnalysis/LockCount1.py b/src/UnitTests/TestData/AstAnalysis/LockCount1.py new file mode 100644 index 000000000..da267e6d0 --- /dev/null +++ b/src/UnitTests/TestData/AstAnalysis/LockCount1.py @@ -0,0 +1,4 @@ +import LockCount2 +import doestnotexist +y = 1 +x. diff --git a/src/UnitTests/TestData/AstAnalysis/LockCount2.py b/src/UnitTests/TestData/AstAnalysis/LockCount2.py new file mode 100644 index 000000000..062d0ac1f --- /dev/null +++ b/src/UnitTests/TestData/AstAnalysis/LockCount2.py @@ -0,0 +1 @@ +import LockCount3 diff --git a/src/UnitTests/TestData/AstAnalysis/LockCount3.py b/src/UnitTests/TestData/AstAnalysis/LockCount3.py new file mode 100644 index 000000000..9f5f1cf8e --- /dev/null +++ b/src/UnitTests/TestData/AstAnalysis/LockCount3.py @@ -0,0 +1,2 @@ +X = 1 +