diff --git a/src/Analysis/Ast/Impl/Analyzer/Handlers/FromImportHandler.cs b/src/Analysis/Ast/Impl/Analyzer/Handlers/FromImportHandler.cs index af3e98678..e462c9b17 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Handlers/FromImportHandler.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Handlers/FromImportHandler.cs @@ -16,6 +16,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Microsoft.Python.Analysis.Core.DependencyResolution; using Microsoft.Python.Analysis.Modules; using Microsoft.Python.Analysis.Types; @@ -42,7 +43,7 @@ public bool HandleFromImport(FromImportStatement node, CancellationToken cancell var imports = ModuleResolution.CurrentPathResolver.FindImports(Module.FilePath, node); switch (imports) { case ModuleImport moduleImport when moduleImport.FullName == Module.Name: - ImportMembersFromSelf(node, cancellationToken); + ImportMembersFromSelf(node); break; case ModuleImport moduleImport: ImportMembersFromModule(node, moduleImport.FullName, cancellationToken); @@ -60,7 +61,7 @@ public bool HandleFromImport(FromImportStatement node, CancellationToken cancell return false; } - private void ImportMembersFromSelf(FromImportStatement node, CancellationToken cancellationToken = default) { + private void ImportMembersFromSelf(FromImportStatement node) { var names = node.Names; var asNames = node.AsNames; diff --git a/src/Analysis/Ast/Impl/Diagnostics/IDiagnosticsService.cs b/src/Analysis/Ast/Impl/Diagnostics/IDiagnosticsService.cs index 96d7e3674..0a6a83ce9 100644 --- a/src/Analysis/Ast/Impl/Diagnostics/IDiagnosticsService.cs +++ b/src/Analysis/Ast/Impl/Diagnostics/IDiagnosticsService.cs @@ -29,7 +29,7 @@ public interface IDiagnosticsService { void Replace(Uri documentUri, IEnumerable entries); /// - /// Removes document from the diagnostics report. Typically when document closes. + /// Removes document from the diagnostics report. Typically when document is disposed. /// void Remove(Uri documentUri); diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs index ed7eeb0e0..914817b3a 100644 --- a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs +++ b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs @@ -27,6 +27,7 @@ using Microsoft.Python.Analysis.Documents; using Microsoft.Python.Analysis.Types; using Microsoft.Python.Core; +using Microsoft.Python.Core.IO; using Microsoft.Python.Core.Diagnostics; namespace Microsoft.Python.Analysis.Modules.Resolution { @@ -78,6 +79,15 @@ 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); + if (module != null) { + return module; + } + } + // If there is a stub, make sure it is loaded and attached // First check stub next to the module. if (!TryCreateModuleStub(name, moduleImport.ModulePath, out var stub)) { @@ -85,12 +95,6 @@ protected override IPythonModule CreateModule(string name) { stub = _interpreter.TypeshedResolution.GetOrLoadModule(moduleImport.IsBuiltin ? name : moduleImport.FullName); } - // If stub is created and its path equals to module, return stub instead of module - if (stub != null && stub.FilePath.PathEquals(moduleImport.ModulePath)) { - return stub; - } - - IPythonModule module; if (moduleImport.IsBuiltin) { _log?.Log(TraceEventType.Verbose, "Create built-in compiled (scraped) module: ", name, Configuration.InterpreterPath); module = new CompiledBuiltinPythonModule(name, stub, _services); @@ -98,12 +102,16 @@ protected override IPythonModule CreateModule(string name) { _log?.Log(TraceEventType.Verbose, "Create compiled (scraped): ", moduleImport.FullName, moduleImport.ModulePath, moduleImport.RootPath); module = new CompiledPythonModule(moduleImport.FullName, ModuleType.Compiled, moduleImport.ModulePath, stub, _services); } else { - _log?.Log(TraceEventType.Verbose, "Create: ", moduleImport.FullName, moduleImport.ModulePath); - var rdt = _services.GetService(); - // TODO: handle user code and library module separately. + _log?.Log(TraceEventType.Verbose, "Import: ", moduleImport.FullName, moduleImport.ModulePath); + // Module inside workspace == user code. + + var moduleType = moduleImport.ModulePath.IsUnderRoot(_root, _fs.StringComparison) + ? ModuleType.User + : ModuleType.Library; + var mco = new ModuleCreationOptions { ModuleName = moduleImport.FullName, - ModuleType = ModuleType.Library, + ModuleType = moduleType, FilePath = moduleImport.ModulePath, Stub = stub }; @@ -203,5 +211,8 @@ private bool TryCreateModuleStub(string name, string modulePath, out IPythonModu module = !string.IsNullOrEmpty(stubPath) ? new StubPythonModule(name, stubPath, false, _services) : null; return module != null; } + + protected override void ReportModuleNotFound(string name) + => _log?.Log(TraceEventType.Information, $"Import not found: {name}"); } } diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/ModuleResolutionBase.cs b/src/Analysis/Ast/Impl/Modules/Resolution/ModuleResolutionBase.cs index 6af91641b..c2ccc8df4 100644 --- a/src/Analysis/Ast/Impl/Modules/Resolution/ModuleResolutionBase.cs +++ b/src/Analysis/Ast/Impl/Modules/Resolution/ModuleResolutionBase.cs @@ -159,5 +159,7 @@ public IPythonModule GetOrCreate(string name, ModuleResolutionBase mrb) { } } } + + protected abstract void ReportModuleNotFound(string name); } } \ No newline at end of file diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/TypeshedResolution.cs b/src/Analysis/Ast/Impl/Modules/Resolution/TypeshedResolution.cs index 751289c1e..316f58340 100644 --- a/src/Analysis/Ast/Impl/Modules/Resolution/TypeshedResolution.cs +++ b/src/Analysis/Ast/Impl/Modules/Resolution/TypeshedResolution.cs @@ -139,5 +139,7 @@ private bool IsPackage(string directory) !string.IsNullOrEmpty(ModulePath.GetPackageInitPy(directory)) : _fs.DirectoryExists(directory); + protected override void ReportModuleNotFound(string name) + => _log?.Log(TraceEventType.Verbose, $"Typeshed stub not found for '{name}'"); } } diff --git a/src/Core/Impl/Extensions/IOExtensions.cs b/src/Core/Impl/Extensions/IOExtensions.cs index cfabc6cc4..78ae27ac5 100644 --- a/src/Core/Impl/Extensions/IOExtensions.cs +++ b/src/Core/Impl/Extensions/IOExtensions.cs @@ -124,5 +124,13 @@ public static void WriteTextWithRetry(this IFileSystem fs, string filePath, stri fs.DeleteFile(filePath); } catch (IOException) { } catch (UnauthorizedAccessException) { } } + + public static bool IsUnderRoot(this string path, string root, StringComparison comparison) { + if (!string.IsNullOrEmpty(root)) { + var normalized = PathUtils.NormalizePath(path); + return normalized.StartsWith(root, comparison); + } + return false; + } } } diff --git a/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs b/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs index b00e10c6a..037cc700b 100644 --- a/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs +++ b/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Python.Analysis.Diagnostics; +using Microsoft.Python.Analysis.Documents; using Microsoft.Python.Core; using Microsoft.Python.Core.Disposables; using Microsoft.Python.Core.Idle; @@ -26,17 +27,40 @@ namespace Microsoft.Python.LanguageServer.Diagnostics { internal sealed class DiagnosticsService : IDiagnosticsService, IDisposable { - private readonly Dictionary> _diagnostics = new Dictionary>(); + private sealed class DocumentDiagnostics { + private DiagnosticsEntry[] _entries; + + public DiagnosticsEntry[] Entries { + get => _entries ?? Array.Empty(); + set { + _entries = value; + Changed = true; + } + } + public bool Changed { get; set; } + + public void Clear() { + Changed = _entries.Length > 0; + _entries = Array.Empty(); + } + } + + private readonly Dictionary _diagnostics = new Dictionary(); private readonly DisposableBag _disposables = DisposableBag.Create(); + private readonly IServiceContainer _services; private readonly IClientApplication _clientApp; private readonly object _lock = new object(); private DiagnosticsSeverityMap _severityMap = new DiagnosticsSeverityMap(); + private IRunningDocumentTable _rdt; private DateTime _lastChangeTime; - private bool _changed; + + private IRunningDocumentTable Rdt => _rdt ?? (_rdt = _services.GetService()); public DiagnosticsService(IServiceContainer services) { - var idleTimeService = services.GetService(); + _services = services; + _clientApp = services.GetService(); + var idleTimeService = services.GetService(); if (idleTimeService != null) { idleTimeService.Idle += OnIdle; idleTimeService.Closing += OnClosing; @@ -45,7 +69,6 @@ public DiagnosticsService(IServiceContainer services) { .Add(() => idleTimeService.Idle -= OnIdle) .Add(() => idleTimeService.Idle -= OnClosing); } - _clientApp = services.GetService(); } #region IDiagnosticsService @@ -53,32 +76,31 @@ public IReadOnlyDictionary> Diagnostics { get { lock (_lock) { return _diagnostics.ToDictionary(kvp => kvp.Key, - kvp => kvp.Value - .Where(e => DiagnosticsSeverityMap.GetEffectiveSeverity(e.ErrorCode, e.Severity) != Severity.Suppressed) - .Select(e => new DiagnosticsEntry( - e.Message, - e.SourceSpan, - e.ErrorCode, - DiagnosticsSeverityMap.GetEffectiveSeverity(e.ErrorCode, e.Severity)) - ).ToList() as IReadOnlyList); + kvp => FilterBySeverityMap(kvp.Value).ToList() as IReadOnlyList); } } } public void Replace(Uri documentUri, IEnumerable entries) { lock (_lock) { - _diagnostics[documentUri] = entries.ToList(); + if (!_diagnostics.TryGetValue(documentUri, out var documentDiagnostics)) { + documentDiagnostics = new DocumentDiagnostics(); + _diagnostics[documentUri] = documentDiagnostics; + } + documentDiagnostics.Entries = entries.ToArray(); + documentDiagnostics.Changed = true; _lastChangeTime = DateTime.Now; - _changed = true; } } public void Remove(Uri documentUri) { lock (_lock) { // Before removing the document, make sure we clear its diagnostics. - _diagnostics[documentUri] = new List(); - PublishDiagnostics(); - _diagnostics.Remove(documentUri); + if (_diagnostics.TryGetValue(documentUri, out var d)) { + d.Clear(); + PublishDiagnostics(); + _diagnostics.Remove(documentUri); + } } } @@ -89,6 +111,10 @@ public DiagnosticsSeverityMap DiagnosticsSeverityMap { set { lock (_lock) { _severityMap = value; + foreach (var d in _diagnostics) { + _diagnostics[d.Key].Changed = true; + _lastChangeTime = DateTime.Now; + } PublishDiagnostics(); } } @@ -103,22 +129,29 @@ public void Dispose() { private void OnClosing(object sender, EventArgs e) => Dispose(); private void OnIdle(object sender, EventArgs e) { - if (_changed && (DateTime.Now - _lastChangeTime).TotalMilliseconds > PublishingDelay) { + if ((DateTime.Now - _lastChangeTime).TotalMilliseconds > PublishingDelay) { + ConnectToRdt(); PublishDiagnostics(); } } private void PublishDiagnostics() { - KeyValuePair>[] diagnostics; + var diagnostics = new List>(); lock (_lock) { - diagnostics = Diagnostics.ToArray(); - _changed = false; + foreach (var d in _diagnostics) { + if (d.Value.Changed) { + diagnostics.Add(d); + _diagnostics[d.Key].Changed = false; + } + } } foreach (var kvp in diagnostics) { var parameters = new PublishDiagnosticsParams { uri = kvp.Key, - diagnostics = kvp.Value.Select(ToDiagnostic).ToArray() + diagnostics = Rdt.GetDocument(kvp.Key)?.IsOpen == true + ? FilterBySeverityMap(kvp.Value).Select(ToDiagnostic).ToArray() + : Array.Empty() }; _clientApp.NotifyWithParameterObjectAsync("textDocument/publishDiagnostics", parameters).DoNotWait(); } @@ -127,7 +160,6 @@ private void PublishDiagnostics() { private void ClearAllDiagnostics() { lock (_lock) { _diagnostics.Clear(); - _changed = false; } } @@ -156,5 +188,48 @@ private static Diagnostic ToDiagnostic(DiagnosticsEntry e) { message = e.Message, }; } + + private IEnumerable FilterBySeverityMap(DocumentDiagnostics d) + => d.Entries + .Where(e => DiagnosticsSeverityMap.GetEffectiveSeverity(e.ErrorCode, e.Severity) != Severity.Suppressed) + .Select(e => new DiagnosticsEntry( + e.Message, + e.SourceSpan, + e.ErrorCode, + DiagnosticsSeverityMap.GetEffectiveSeverity(e.ErrorCode, e.Severity)) + ); + + private void ConnectToRdt() { + if (_rdt == null) { + _rdt = _services.GetService(); + if (_rdt != null) { + _rdt.Opened += OnOpenDocument; + _rdt.Closed += OnCloseDocument; + + _disposables + .Add(() => _rdt.Opened -= OnOpenDocument) + .Add(() => _rdt.Closed -= OnCloseDocument); + } + } + } + + private void OnOpenDocument(object sender, DocumentEventArgs e) { + lock (_lock) { + if(_diagnostics.TryGetValue(e.Document.Uri, out var d)) { + d.Changed = d.Entries.Length > 0; + } + } + } + + private void OnCloseDocument(object sender, DocumentEventArgs e) { + lock (_lock) { + // Before removing the document, make sure we clear its diagnostics. + if (_diagnostics.TryGetValue(e.Document.Uri, out var d)) { + d.Clear(); + PublishDiagnostics(); + _diagnostics.Remove(e.Document.Uri); + } + } + } } } diff --git a/src/LanguageServer/Impl/Implementation/Server.Documents.cs b/src/LanguageServer/Impl/Implementation/Server.Documents.cs index 74760817c..2d359eb10 100644 --- a/src/LanguageServer/Impl/Implementation/Server.Documents.cs +++ b/src/LanguageServer/Impl/Implementation/Server.Documents.cs @@ -16,13 +16,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Python.Analysis; using Microsoft.Python.Analysis.Documents; using Microsoft.Python.Core; -using Microsoft.Python.Core.Text; using Microsoft.Python.LanguageServer.Protocol; namespace Microsoft.Python.LanguageServer.Implementation { diff --git a/src/LanguageServer/Test/DiagnosticsTests.cs b/src/LanguageServer/Test/DiagnosticsTests.cs index 29cbba4ff..e0c3941a5 100644 --- a/src/LanguageServer/Test/DiagnosticsTests.cs +++ b/src/LanguageServer/Test/DiagnosticsTests.cs @@ -14,6 +14,7 @@ // permissions and limitations under the License. using System; +using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Python.Analysis.Diagnostics; @@ -45,7 +46,7 @@ public async Task BasicChange() { const string code = @"x = "; var analysis = await GetAnalysisAsync(code); - var ds = Services.GetService(); + var ds = GetDiagnosticsService(); var doc = analysis.Document; ds.Diagnostics[doc.Uri].Count.Should().Be(1); @@ -56,7 +57,7 @@ public async Task BasicChange() { } }); await doc.GetAstAsync(); - await doc.GetAnalysisAsync(0); + await doc.GetAnalysisAsync(1000); ds.Diagnostics[doc.Uri].Count.Should().Be(0); doc.Update(new[] {new DocumentChange { @@ -65,7 +66,7 @@ public async Task BasicChange() { } }); await doc.GetAstAsync(); - await doc.GetAnalysisAsync(0); + await doc.GetAnalysisAsync(1000); ds.Diagnostics[doc.Uri].Count.Should().Be(1); } @@ -76,7 +77,7 @@ public async Task TwoDocuments() { var analysis1 = await GetAnalysisAsync(code1); var analysis2 = await GetNextAnalysisAsync(code2); - var ds = Services.GetService(); + var ds = GetDiagnosticsService(); var doc1 = analysis1.Document; var doc2 = analysis2.Document; @@ -90,7 +91,7 @@ public async Task TwoDocuments() { } }); await doc2.GetAstAsync(); - await doc2.GetAnalysisAsync(0); + await doc2.GetAnalysisAsync(1000); ds.Diagnostics[doc1.Uri].Count.Should().Be(1); ds.Diagnostics[doc2.Uri].Count.Should().Be(0); @@ -100,7 +101,7 @@ public async Task TwoDocuments() { } }); await doc2.GetAstAsync(); - await doc2.GetAnalysisAsync(0); + await doc2.GetAnalysisAsync(1000); ds.Diagnostics[doc2.Uri].Count.Should().Be(1); doc1.Dispose(); @@ -116,26 +117,27 @@ public async Task Publish() { var doc = analysis.Document; var clientApp = Services.GetService(); - var idle = Services.GetService(); - - var expected = 1; + var reported = new List(); clientApp.When(x => x.NotifyWithParameterObjectAsync("textDocument/publishDiagnostics", Arg.Any())) - .Do(x => { - var dp = x.Args()[1] as PublishDiagnosticsParams; - dp.Should().NotBeNull(); - dp.diagnostics.Length.Should().Be(expected); - dp.uri.Should().Be(doc.Uri); - }); - idle.Idle += Raise.EventWith(null, EventArgs.Empty); + .Do(x => reported.Add(x.Args()[1] as PublishDiagnosticsParams)); - expected = 0; + PublishDiagnostics(); + reported.Count.Should().Be(1); + reported[0].diagnostics.Length.Should().Be(1); + reported[0].uri.Should().Be(doc.Uri); + + reported.Clear(); doc.Update(new[] {new DocumentChange { InsertedText = "1", ReplacedSpan = new SourceSpan(1, 5, 1, 5) } }); - await doc.GetAnalysisAsync(0); - idle.Idle += Raise.EventWith(null, EventArgs.Empty); + await doc.GetAstAsync(); + await doc.GetAnalysisAsync(1000); + + PublishDiagnostics(); + reported.Count.Should().Be(1); + reported[0].diagnostics.Length.Should().Be(0); } [TestMethod, Priority(0)] @@ -143,26 +145,21 @@ public async Task CloseDocument() { const string code = @"x = "; var analysis = await GetAnalysisAsync(code); - var ds = Services.GetService(); + var ds = GetDiagnosticsService(); var doc = analysis.Document; ds.Diagnostics[doc.Uri].Count.Should().Be(1); var clientApp = Services.GetService(); - var uri = doc.Uri; - var callReceived = false; + var reported = new List(); clientApp.When(x => x.NotifyWithParameterObjectAsync("textDocument/publishDiagnostics", Arg.Any())) - .Do(x => { - var dp = x.Args()[1] as PublishDiagnosticsParams; - dp.Should().NotBeNull(); - dp.diagnostics.Length.Should().Be(0); - dp.uri.Should().Be(uri); - callReceived = true; - }); + .Do(x => reported.Add(x.Args()[1] as PublishDiagnosticsParams)); doc.Dispose(); ds.Diagnostics.TryGetValue(doc.Uri, out _).Should().BeFalse(); - callReceived.Should().BeTrue(); + reported.Count.Should().Be(1); + reported[0].uri.Should().Be(doc.Uri); + reported[0].diagnostics.Length.Should().Be(0); } [TestMethod, Priority(0)] @@ -170,77 +167,106 @@ public async Task SeverityMapping() { const string code = @"import nonexistent"; var analysis = await GetAnalysisAsync(code); - var ds = Services.GetService(); + var ds = GetDiagnosticsService(); var doc = analysis.Document; var diags = ds.Diagnostics[doc.Uri]; diags.Count.Should().Be(1); diags[0].Severity.Should().Be(Severity.Warning); - var uri = doc.Uri; - var callReceived = false; var clientApp = Services.GetService(); - var expectedSeverity = DiagnosticSeverity.Error; + var reported = new List(); clientApp.When(x => x.NotifyWithParameterObjectAsync("textDocument/publishDiagnostics", Arg.Any())) - .Do(x => { - var dp = x.Args()[1] as PublishDiagnosticsParams; - dp.Should().NotBeNull(); - dp.uri.Should().Be(uri); - dp.diagnostics.Length.Should().Be(expectedSeverity == DiagnosticSeverity.Unspecified ? 0 : 1); - if (expectedSeverity != DiagnosticSeverity.Unspecified) { - dp.diagnostics[0].severity.Should().Be(expectedSeverity); - } - callReceived = true; - }); + .Do(x => reported.Add(x.Args()[1] as PublishDiagnosticsParams)); ds.DiagnosticsSeverityMap = new DiagnosticsSeverityMap(new[] { ErrorCodes.UnresolvedImport }, null, null, null); - callReceived.Should().BeTrue(); + reported.Count.Should().Be(1); + reported[0].uri.Should().Be(doc.Uri); + reported[0].diagnostics.Length.Should().Be(1); + reported[0].diagnostics[0].severity.Should().Be(DiagnosticSeverity.Error); - expectedSeverity = DiagnosticSeverity.Information; - callReceived = false; + reported.Clear(); ds.DiagnosticsSeverityMap = new DiagnosticsSeverityMap(null, null, new[] { ErrorCodes.UnresolvedImport }, null); - ds.Diagnostics[uri][0].Severity.Should().Be(Severity.Information); - callReceived.Should().BeTrue(); + reported.Count.Should().Be(1); + reported[0].diagnostics[0].severity.Should().Be(DiagnosticSeverity.Information); - expectedSeverity = DiagnosticSeverity.Unspecified; - callReceived = false; + reported.Clear(); ds.DiagnosticsSeverityMap = new DiagnosticsSeverityMap(null, null, null, new[] { ErrorCodes.UnresolvedImport }); - ds.Diagnostics[uri].Count.Should().Be(0); - callReceived.Should().BeTrue(); - - expectedSeverity = DiagnosticSeverity.Unspecified; - callReceived = false; - ds.DiagnosticsSeverityMap = new DiagnosticsSeverityMap(new[] { ErrorCodes.UnresolvedImport }, null, null, new[] { ErrorCodes.UnresolvedImport }); - ds.Diagnostics[uri].Count.Should().Be(0); - callReceived.Should().BeTrue(); + reported.Count.Should().Be(1); + reported[0].diagnostics.Length.Should().Be(0); } [TestMethod, Priority(0)] - public async Task SuppressError() { - const string code = @"import nonexistent"; + public async Task OnlyPublishChangedFile() { + const string code1 = @"x = "; + const string code2 = @"y = "; - var analysis = await GetAnalysisAsync(code); - var ds = Services.GetService(); - var doc = analysis.Document; + var analysis1 = await GetAnalysisAsync(code1); + var analysis2 = await GetNextAnalysisAsync(code2); + var ds = GetDiagnosticsService(); - var diags = ds.Diagnostics[doc.Uri]; - diags.Count.Should().Be(1); - diags[0].Severity.Should().Be(Severity.Warning); + var doc1 = analysis1.Document; + var doc2 = analysis2.Document; + + ds.Diagnostics[doc1.Uri].Count.Should().Be(1); + ds.Diagnostics[doc2.Uri].Count.Should().Be(1); + + // Clear diagnostics 'changed' state by forcing publish. + PublishDiagnostics(); - var uri = doc.Uri; - var callReceived = false; + var reported = new List(); var clientApp = Services.GetService(); clientApp.When(x => x.NotifyWithParameterObjectAsync("textDocument/publishDiagnostics", Arg.Any())) .Do(x => { var dp = x.Args()[1] as PublishDiagnosticsParams; - dp.Should().NotBeNull(); - dp.uri.Should().Be(uri); - dp.diagnostics.Length.Should().Be(0); - callReceived = true; + reported.Add(dp); }); - ds.DiagnosticsSeverityMap = new DiagnosticsSeverityMap(null, null, null, new[] { ErrorCodes.UnresolvedImport }); - callReceived.Should().BeTrue(); + + doc1.Update(new[] {new DocumentChange { + InsertedText = "1", + ReplacedSpan = new SourceSpan(1, 5, 1, 5) + } }); + + await doc1.GetAstAsync(); + await doc1.GetAnalysisAsync(1000); + + ds.Diagnostics[doc1.Uri].Count.Should().Be(0); + ds.Diagnostics[doc2.Uri].Count.Should().Be(1); + + PublishDiagnostics(); + reported.Count.Should().Be(1); + reported[0].uri.Should().Be(doc1.Uri); + reported[0].diagnostics.Length.Should().Be(0); + + doc1.Update(new[] {new DocumentChange { + InsertedText = string.Empty, + ReplacedSpan = new SourceSpan(1, 5, 1, 6) + } }); + + await doc1.GetAstAsync(); + await doc1.GetAnalysisAsync(1000); + ds.Diagnostics[doc1.Uri].Count.Should().Be(1); + ds.Diagnostics[doc2.Uri].Count.Should().Be(1); + + reported.Clear(); + PublishDiagnostics(); + + reported.Count.Should().Be(1); + 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/RdtTests.cs b/src/LanguageServer/Test/RdtTests.cs new file mode 100644 index 000000000..09f25f3df --- /dev/null +++ b/src/LanguageServer/Test/RdtTests.cs @@ -0,0 +1,67 @@ +// Copyright(c) Microsoft Corporation +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the License); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +// MERCHANTABILITY OR NON-INFRINGEMENT. +// +// See the Apache Version 2.0 License for specific language governing +// permissions and limitations under the License. + +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Python.Analysis.Diagnostics; +using Microsoft.Python.Analysis.Documents; +using Microsoft.Python.Parsing.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestUtilities; + +namespace Microsoft.Python.LanguageServer.Tests { + [TestClass] + public class RdtTests : LanguageServerTestBase { + public TestContext TestContext { get; set; } + + [TestInitialize] + public void TestInitialize() + => TestEnvironmentImpl.TestInitialize($"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}"); + + [TestCleanup] + public void Cleanup() => TestEnvironmentImpl.TestCleanup(); + + + [TestMethod, Priority(0)] + public async Task OpenCloseDocuments() { + const string code1 = @"x = "; + const string code2 = @"y = "; + var uri1 = TestData.GetDefaultModuleUri(); + var uri2 = TestData.GetNextModuleUri(); + + await CreateServicesAsync(PythonVersions.LatestAvailable3X, uri1.AbsolutePath); + var ds = Services.GetService(); + var rdt = Services.GetService(); + + rdt.OpenDocument(uri1, code1); + rdt.OpenDocument(uri2, code2); + + var doc1 = rdt.GetDocument(uri1); + var doc2 = rdt.GetDocument(uri2); + + await doc1.GetAnalysisAsync(10000); + await doc2.GetAnalysisAsync(10000); + + ds.Diagnostics[doc1.Uri].Count.Should().Be(1); + ds.Diagnostics[doc2.Uri].Count.Should().Be(1); + + rdt.CloseDocument(uri1); + + ds.Diagnostics[uri2].Count.Should().Be(1); + rdt.GetDocument(uri1).Should().BeNull(); + ds.Diagnostics.TryGetValue(uri1, out _).Should().BeFalse(); + } + } +}