diff --git a/src/Analysis/Ast/Impl/Analyzer/Definitions/IExpressionEvaluator.cs b/src/Analysis/Ast/Impl/Analyzer/Definitions/IExpressionEvaluator.cs index 0f89b7572..0f39a3ee9 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Definitions/IExpressionEvaluator.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Definitions/IExpressionEvaluator.cs @@ -67,6 +67,8 @@ public interface IExpressionEvaluator { IPythonModule Module { get; } IPythonInterpreter Interpreter { get; } IServiceContainer Services { get; } + + void ReportDiagnostics(Uri documentUri, DiagnosticsEntry entry); IEnumerable Diagnostics { get; } } } diff --git a/src/Analysis/Ast/Impl/Analyzer/DocumentAnalysis.cs b/src/Analysis/Ast/Impl/Analyzer/DocumentAnalysis.cs index b1f8f8de0..a29c3d0dc 100644 --- a/src/Analysis/Ast/Impl/Analyzer/DocumentAnalysis.cs +++ b/src/Analysis/Ast/Impl/Analyzer/DocumentAnalysis.cs @@ -18,14 +18,11 @@ using System.IO; using System.Linq; using Microsoft.Python.Analysis.Analyzer.Evaluation; -using Microsoft.Python.Analysis.Analyzer.Expressions; using Microsoft.Python.Analysis.Diagnostics; -using Microsoft.Python.Analysis.Types; using Microsoft.Python.Analysis.Documents; using Microsoft.Python.Analysis.Values; using Microsoft.Python.Core; using Microsoft.Python.Core.Diagnostics; -using Microsoft.Python.Core.Text; using Microsoft.Python.Parsing; using Microsoft.Python.Parsing.Ast; @@ -67,6 +64,11 @@ public DocumentAnalysis(IDocument document, int version, IGlobalScope globalScop /// Expression evaluator used in the analysis. /// public IExpressionEvaluator ExpressionEvaluator { get; } + + /// + /// Analysis diagnostics. + /// + public IEnumerable Diagnostics => ExpressionEvaluator.Diagnostics; #endregion } @@ -88,5 +90,4 @@ public EmptyAnalysis(IServiceContainer services, IDocument document) { public IExpressionEvaluator ExpressionEvaluator { get; } public IEnumerable Diagnostics => Enumerable.Empty(); } - } diff --git a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs index df6995e4b..68c130a34 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs @@ -33,7 +33,7 @@ namespace Microsoft.Python.Analysis.Analyzer.Evaluation { /// Helper class that provides methods for looking up variables /// and types in a chain of scopes during analysis. /// - internal sealed partial class ExpressionEval : IExpressionEvaluator { + internal sealed partial class ExpressionEval: IExpressionEvaluator { private readonly Stack _openScopes = new Stack(); private readonly List _diagnostics = new List(); private readonly object _lock = new object(); @@ -229,11 +229,11 @@ private async Task GetValueFromConditionalAsync(ConditionalExpression e return trueValue ?? falseValue; } - private void ReportDiagnostics(Uri documentUri, IEnumerable entries) { + public void ReportDiagnostics(Uri documentUri, DiagnosticsEntry entry) { // Do not add if module is library, etc. Only handle user code. if (Module.ModuleType == ModuleType.User) { lock (_lock) { - _diagnostics.AddRange(entries); + _diagnostics.Add(entry); } } } diff --git a/src/Analysis/Ast/Impl/Analyzer/Handlers/FromImportHandler.cs b/src/Analysis/Ast/Impl/Analyzer/Handlers/FromImportHandler.cs index dd2705b5c..fbf5162c9 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Handlers/FromImportHandler.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Handlers/FromImportHandler.cs @@ -33,7 +33,6 @@ public async Task HandleFromImportAsync(FromImportStatement node, Cancella } var rootNames = node.Root.Names; - IImportSearchResult imports = null; if (rootNames.Count == 1) { var rootName = rootNames[0].Name; if (rootName.EqualsOrdinal("__future__")) { @@ -41,29 +40,29 @@ public async Task HandleFromImportAsync(FromImportStatement node, Cancella } } - imports = ModuleResolution.CurrentPathResolver.FindImports(Module.FilePath, node); - // If we are processing stub, ignore imports of the original module. - // For example, typeshed stub for sys imports sys. - if (Module.ModuleType == ModuleType.Stub && imports is ModuleImport mi && mi.Name == Module.Name) { - return false; - } - + var imports = ModuleResolution.CurrentPathResolver.FindImports(Module.FilePath, node); switch (imports) { + case ModuleImport moduleImport when moduleImport.FullName == Module.Name && Module.ModuleType == ModuleType.Stub: + // If we are processing stub, ignore imports of the original module. + // For example, typeshed stub for 'sys' imports sys. + break; case ModuleImport moduleImport when moduleImport.FullName == Module.Name: ImportMembersFromSelf(node); - return false; + break; case ModuleImport moduleImport: await ImportMembersFromModuleAsync(node, moduleImport.FullName, cancellationToken); - return false; + break; case PossibleModuleImport possibleModuleImport: - await HandlePossibleImportAsync(node, possibleModuleImport, cancellationToken); - return false; + await HandlePossibleImportAsync(possibleModuleImport, possibleModuleImport.PossibleModuleFullName, Eval.GetLoc(node.Root), cancellationToken); + break; case PackageImport packageImports: await ImportMembersFromPackageAsync(node, packageImports, cancellationToken); - return false; - default: - return false; + break; + case ImportNotFound notFound: + MakeUnresolvedImport(null, notFound.FullName, Eval.GetLoc(node.Root)); + break; } + return false; } private void ImportMembersFromSelf(FromImportStatement node) { diff --git a/src/Analysis/Ast/Impl/Analyzer/Handlers/ImportHandler.cs b/src/Analysis/Ast/Impl/Analyzer/Handlers/ImportHandler.cs index b964ceb72..c42040dbe 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Handlers/ImportHandler.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Handlers/ImportHandler.cs @@ -18,13 +18,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Python.Analysis.Core.DependencyResolution; +using Microsoft.Python.Analysis.Diagnostics; using Microsoft.Python.Analysis.Modules; using Microsoft.Python.Analysis.Types; using Microsoft.Python.Analysis.Values; +using Microsoft.Python.Core; +using Microsoft.Python.Parsing; using Microsoft.Python.Parsing.Ast; +using ErrorCodes = Microsoft.Python.Analysis.Diagnostics.ErrorCodes; namespace Microsoft.Python.Analysis.Analyzer.Handlers { - internal sealed partial class ImportHandler: StatementHandler { + internal sealed partial class ImportHandler : StatementHandler { public ImportHandler(AnalysisWalker walker) : base(walker) { } public async Task HandleImportAsync(ImportStatement node, CancellationToken cancellationToken = default) { @@ -54,14 +58,14 @@ public async Task HandleImportAsync(ImportStatement node, CancellationToke Eval.DeclareVariable(memberName, Module, VariableSource.Declaration, location); break; case ModuleImport moduleImport: - module = await HandleImportAsync(node, moduleImport, cancellationToken); + module = await HandleImportAsync(moduleImport, location, cancellationToken); break; case PossibleModuleImport possibleModuleImport: - module = await HandlePossibleImportAsync(node, possibleModuleImport, cancellationToken); + module = await HandlePossibleImportAsync(possibleModuleImport, possibleModuleImport.PossibleModuleFullName, location, cancellationToken); break; default: // TODO: Package import? - MakeUnresolvedImport(memberName, moduleImportExpression); + MakeUnresolvedImport(memberName, moduleImportExpression.MakeString(), Eval.GetLoc(moduleImportExpression)); break; } @@ -72,20 +76,21 @@ public async Task HandleImportAsync(ImportStatement node, CancellationToke return false; } - private async Task HandleImportAsync(ImportStatement node, ModuleImport moduleImport, CancellationToken cancellationToken) { + private async Task HandleImportAsync(ModuleImport moduleImport, LocationInfo location, CancellationToken cancellationToken) { var module = await ModuleResolution.ImportModuleAsync(moduleImport.FullName, cancellationToken); if (module == null) { - MakeUnresolvedImport(moduleImport.FullName, node); + MakeUnresolvedImport(moduleImport.FullName, moduleImport.FullName, location); return null; } return module; } - private async Task HandlePossibleImportAsync(Node node, PossibleModuleImport possibleModuleImport, CancellationToken cancellationToken) { + private async Task HandlePossibleImportAsync( + PossibleModuleImport possibleModuleImport, string moduleName, LocationInfo location, CancellationToken cancellationToken) { var fullName = possibleModuleImport.PrecedingModuleFullName; var module = await ModuleResolution.ImportModuleAsync(possibleModuleImport.PrecedingModuleFullName, cancellationToken); if (module == null) { - MakeUnresolvedImport(possibleModuleImport.PrecedingModuleFullName, node); + MakeUnresolvedImport(possibleModuleImport.PrecedingModuleFullName, moduleName, location); return null; } @@ -95,7 +100,7 @@ private async Task HandlePossibleImportAsync(Node node, PossibleM var childModule = module.GetMember(namePart); if (childModule == null) { var unresolvedModuleName = string.Join(".", nameParts.Take(i + 1).Prepend(fullName)); - MakeUnresolvedImport(unresolvedModuleName, node); + MakeUnresolvedImport(unresolvedModuleName, moduleName, location); return null; } module = childModule; @@ -143,7 +148,12 @@ private void AssignImportedVariables(IPythonModule module, DottedName moduleImpo } } - private void MakeUnresolvedImport(string name, Node node) - => Eval.DeclareVariable(name, new SentinelModule(name, Eval.Services), VariableSource.Import, Eval.GetLoc(node)); + private void MakeUnresolvedImport(string variableName, string moduleName, LocationInfo location) { + if (!string.IsNullOrEmpty(variableName)) { + Eval.DeclareVariable(variableName, new SentinelModule(moduleName, Eval.Services), VariableSource.Import, location); + } + Eval.ReportDiagnostics(Eval.Module.Uri, new DiagnosticsEntry( + Resources.ErrorUnresolvedImport.FormatInvariant(moduleName), location.Span, ErrorCodes.UnresolvedImport, Severity.Warning)); + } } } diff --git a/src/Analysis/Ast/Impl/Definitions/IDocumentAnalysis.cs b/src/Analysis/Ast/Impl/Definitions/IDocumentAnalysis.cs index fb1055c94..0d1b43d86 100644 --- a/src/Analysis/Ast/Impl/Definitions/IDocumentAnalysis.cs +++ b/src/Analysis/Ast/Impl/Definitions/IDocumentAnalysis.cs @@ -13,7 +13,9 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. +using System.Collections.Generic; using Microsoft.Python.Analysis.Analyzer; +using Microsoft.Python.Analysis.Diagnostics; using Microsoft.Python.Analysis.Documents; using Microsoft.Python.Analysis.Values; using Microsoft.Python.Parsing.Ast; @@ -49,5 +51,10 @@ public interface IDocumentAnalysis { /// Expression evaluator used in the analysis. /// IExpressionEvaluator ExpressionEvaluator { get; } + + /// + /// Analysis diagnostics. + /// + IEnumerable Diagnostics { get; } } } diff --git a/src/Analysis/Ast/Impl/Diagnostics/DiagnosticsSeverityMap.cs b/src/Analysis/Ast/Impl/Diagnostics/DiagnosticsSeverityMap.cs new file mode 100644 index 000000000..f0e32fd68 --- /dev/null +++ b/src/Analysis/Ast/Impl/Diagnostics/DiagnosticsSeverityMap.cs @@ -0,0 +1,45 @@ +// 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.Collections.Generic; +using Microsoft.Python.Core; +using Microsoft.Python.Parsing; + +namespace Microsoft.Python.Analysis.Diagnostics { + public sealed class DiagnosticsSeverityMap { + private readonly Dictionary _map = new Dictionary(); + + public DiagnosticsSeverityMap() { } + + public DiagnosticsSeverityMap(string[] errors, string[] warnings, string[] information, string[] disabled) { + _map.Clear(); + // disabled > error > warning > information + foreach (var x in information.MaybeEnumerate()) { + _map[x] = Severity.Information; + } + foreach (var x in warnings.MaybeEnumerate()) { + _map[x] = Severity.Warning; + } + foreach (var x in errors.MaybeEnumerate()) { + _map[x] = Severity.Error; + } + foreach (var x in disabled.MaybeEnumerate()) { + _map[x] = Severity.Suppressed; + } + } + public Severity GetEffectiveSeverity(string code, Severity defaultSeverity) + => _map.TryGetValue(code, out var severity) ? severity : defaultSeverity; + } +} diff --git a/src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs b/src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs index 27e0b7788..6f7289338 100644 --- a/src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs +++ b/src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs @@ -21,5 +21,6 @@ public static class ErrorCodes { public const string UnknownParameterName = "unknown-parameter-name"; public const string ParameterAlreadySpecified = "parameter-already-specified"; public const string ParameterMissing = "parameter-missing"; + public const string UnresolvedImport = "unresolved-import"; } } diff --git a/src/Analysis/Ast/Impl/Diagnostics/IDiagnosticsService.cs b/src/Analysis/Ast/Impl/Diagnostics/IDiagnosticsService.cs index 7de168b73..96d7e3674 100644 --- a/src/Analysis/Ast/Impl/Diagnostics/IDiagnosticsService.cs +++ b/src/Analysis/Ast/Impl/Diagnostics/IDiagnosticsService.cs @@ -38,5 +38,12 @@ public interface IDiagnosticsService { /// the diagnostic publishing to the client. /// int PublishingDelay { get; set; } + + /// + /// Provides map of error codes to severity when user wants + /// to override default severity settings or suppress particular + /// diagnostics completely. + /// + DiagnosticsSeverityMap DiagnosticsSeverityMap { get; set; } } } diff --git a/src/Analysis/Ast/Impl/Modules/PythonModule.cs b/src/Analysis/Ast/Impl/Modules/PythonModule.cs index c0f391e9e..d0cc0cea2 100644 --- a/src/Analysis/Ast/Impl/Modules/PythonModule.cs +++ b/src/Analysis/Ast/Impl/Modules/PythonModule.cs @@ -385,7 +385,7 @@ private void Parse(CancellationToken cancellationToken) { // Do not report issues with libraries or stubs if (sink != null) { - _diagnosticsService?.Replace(Uri, _parseErrors); + _diagnosticsService?.Replace(Uri, _parseErrors.Concat(Analysis.Diagnostics)); } _parsingTask = null; @@ -460,6 +460,11 @@ public virtual bool NotifyAnalysisComplete(IDocumentAnalysis analysis) { OnAnalysisComplete(); ContentState = State.Analyzed; + // Do not report issues with libraries or stubs + if (ModuleType == ModuleType.User) { + _diagnosticsService?.Replace(Uri, _parseErrors.Concat(Analysis.Diagnostics)); + } + var tcs = _analysisTcs; _analysisTcs = null; tcs.TrySetResult(analysis); diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs index 56fd6c779..41f1a2d2f 100644 --- a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs +++ b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs @@ -42,7 +42,7 @@ internal async Task InitializeAsync(CancellationToken cancellationToken = defaul // Initialize built-in var moduleName = BuiltinTypeId.Unknown.GetModuleName(_interpreter.LanguageVersion); - var modulePath = ModuleCache.GetCacheFilePath(_interpreter.Configuration.InterpreterPath ?? "python.exe"); + var modulePath = ModuleCache.GetCacheFilePath(_interpreter.Configuration.InterpreterPath); var b = new BuiltinsPythonModule(moduleName, modulePath, _services); _modules[BuiltinModuleName] = BuiltinsModule = b; diff --git a/src/Analysis/Ast/Test/FluentAssertions/AssertionsFactory.cs b/src/Analysis/Ast/Test/FluentAssertions/AssertionsFactory.cs index 21a2a70a4..9e4d86965 100644 --- a/src/Analysis/Ast/Test/FluentAssertions/AssertionsFactory.cs +++ b/src/Analysis/Ast/Test/FluentAssertions/AssertionsFactory.cs @@ -32,5 +32,8 @@ internal static class AssertionsFactory { public static VariableAssertions Should(this IVariable v) => new VariableAssertions(v); public static RangeAssertions Should(this Range? range) => new RangeAssertions(range); + + public static SourceSpanAssertions Should(this SourceSpan span) => new SourceSpanAssertions(span); + public static SourceSpanAssertions Should(this SourceSpan? span) => new SourceSpanAssertions(span.Value); } } diff --git a/src/LanguageServer/Test/FluentAssertions/SourceSpanAssertions.cs b/src/Analysis/Ast/Test/FluentAssertions/SourceSpanAssertions.cs similarity index 94% rename from src/LanguageServer/Test/FluentAssertions/SourceSpanAssertions.cs rename to src/Analysis/Ast/Test/FluentAssertions/SourceSpanAssertions.cs index 390cd8d51..d506a516b 100644 --- a/src/LanguageServer/Test/FluentAssertions/SourceSpanAssertions.cs +++ b/src/Analysis/Ast/Test/FluentAssertions/SourceSpanAssertions.cs @@ -18,8 +18,8 @@ using Microsoft.Python.Core.Text; using static Microsoft.Python.Analysis.Tests.FluentAssertions.AssertionsUtilities; -namespace Microsoft.Python.LanguageServer.Tests.FluentAssertions { - internal sealed class SourceSpanAssertions { +namespace Microsoft.Python.Analysis.Tests.FluentAssertions { + public sealed class SourceSpanAssertions { public SourceSpan? Subject { get; } public SourceSpanAssertions(SourceSpan? span) { diff --git a/src/Analysis/Ast/Test/ImportTests.cs b/src/Analysis/Ast/Test/ImportTests.cs index 448708991..e6ff6990f 100644 --- a/src/Analysis/Ast/Test/ImportTests.cs +++ b/src/Analysis/Ast/Test/ImportTests.cs @@ -17,8 +17,11 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Python.Analysis.Diagnostics; +using Microsoft.Python.Analysis.Modules; using Microsoft.Python.Analysis.Tests.FluentAssertions; using Microsoft.Python.Analysis.Types; +using Microsoft.Python.Core; using Microsoft.Python.Parsing.Tests; using Microsoft.Python.Tests.Utilities.FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -151,5 +154,59 @@ public async Task OsPathMembers() { analysis.Should().HaveVariable("P") .Which.Should().HaveMembers(@"abspath", @"dirname"); } + + [TestMethod, Priority(0)] + public async Task UnresolvedImport() { + var analysis = await GetAnalysisAsync(@"import nonexistent"); + analysis.Should().HaveVariable("nonexistent") + .Which.Value.GetPythonType().ModuleType.Should().Be(ModuleType.Unresolved); + analysis.Diagnostics.Should().HaveCount(1); + var d = analysis.Diagnostics.First(); + d.ErrorCode.Should().Be(ErrorCodes.UnresolvedImport); + d.SourceSpan.Should().Be(1, 8, 1, 19); + d.Message.Should().Be(Resources.ErrorUnresolvedImport.FormatInvariant("nonexistent")); + } + + [TestMethod, Priority(0)] + public async Task UnresolvedImportAs() { + var analysis = await GetAnalysisAsync(@"import nonexistent as A"); + analysis.Should().HaveVariable("A") + .Which.Value.GetPythonType().ModuleType.Should().Be(ModuleType.Unresolved); + analysis.Diagnostics.Should().HaveCount(1); + var d = analysis.Diagnostics.First(); + d.ErrorCode.Should().Be(ErrorCodes.UnresolvedImport); + d.SourceSpan.Should().Be(1, 8, 1, 19); + d.Message.Should().Be(Resources.ErrorUnresolvedImport.FormatInvariant("nonexistent")); + } + + [TestMethod, Priority(0)] + public async Task UnresolvedFromImport() { + var analysis = await GetAnalysisAsync(@"from nonexistent import A"); + analysis.Diagnostics.Should().HaveCount(1); + var d = analysis.Diagnostics.First(); + d.ErrorCode.Should().Be(ErrorCodes.UnresolvedImport); + d.SourceSpan.Should().Be(1, 6, 1, 17); + d.Message.Should().Be(Resources.ErrorUnresolvedImport.FormatInvariant("nonexistent")); + } + + [TestMethod, Priority(0)] + public async Task UnresolvedFromImportAs() { + var analysis = await GetAnalysisAsync(@"from nonexistent import A as B"); + analysis.Diagnostics.Should().HaveCount(1); + var d = analysis.Diagnostics.First(); + d.ErrorCode.Should().Be(ErrorCodes.UnresolvedImport); + d.SourceSpan.Should().Be(1, 6, 1, 17); + d.Message.Should().Be(Resources.ErrorUnresolvedImport.FormatInvariant("nonexistent")); + } + + [TestMethod, Priority(0)] + public async Task UnresolvedRelativeFromImportAs() { + var analysis = await GetAnalysisAsync(@"from ..nonexistent import A as B"); + analysis.Diagnostics.Should().HaveCount(1); + var d = analysis.Diagnostics.First(); + d.ErrorCode.Should().Be(ErrorCodes.UnresolvedImport); + d.SourceSpan.Should().Be(1, 6, 1, 19); + d.Message.Should().Be(Resources.ErrorUnresolvedImport.FormatInvariant("nonexistent")); + } } } diff --git a/src/Analysis/Ast/Test/TypingTests.cs b/src/Analysis/Ast/Test/TypingTests.cs index 382f5e3f0..b405cb330 100644 --- a/src/Analysis/Ast/Test/TypingTests.cs +++ b/src/Analysis/Ast/Test/TypingTests.cs @@ -673,11 +673,11 @@ def func(self) -> A[_E]: ... .Which.Should().HaveMembers("args", @"with_traceback"); analysis.Should().HaveVariable("x") - .Which.Should().HaveType("A[TypeError]") // TODO: should be A[TypeError] + .Which.Should().HaveType("A[TypeError]") .Which.Should().HaveMembers("args", @"with_traceback"); analysis.Should().HaveVariable("y") - .Which.Should().HaveType("A[TypeError]") // TODO: should be A[[TypeError] + .Which.Should().HaveType("A[TypeError]") .Which.Should().HaveMembers("args", @"with_traceback"); } @@ -703,11 +703,11 @@ class A(Generic[_E]): ... .Which.Should().HaveMembers("args", @"with_traceback"); analysis.Should().HaveVariable("x") - .Which.Should().HaveType("A[TypeError]") // TODO: should be A[TypeError] + .Which.Should().HaveType("A[TypeError]") .Which.Should().HaveMembers("args", @"with_traceback"); analysis.Should().HaveVariable("y") - .Which.Should().HaveType("A[TypeError]") // TODO: should be A[[TypeError] + .Which.Should().HaveType("A[TypeError]") .Which.Should().HaveMembers("args", @"with_traceback"); } diff --git a/src/LanguageServer/Impl/Definitions/ServerSettings.cs b/src/LanguageServer/Impl/Definitions/ServerSettings.cs index 86820b48d..7bf28c3ac 100644 --- a/src/LanguageServer/Impl/Definitions/ServerSettings.cs +++ b/src/LanguageServer/Impl/Definitions/ServerSettings.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using Microsoft.Python.Analysis.Diagnostics; using Microsoft.Python.LanguageServer.Protocol; namespace Microsoft.Python.LanguageServer { @@ -29,31 +30,6 @@ public class PythonAnalysisOptions { public string[] warnings { get; private set; } = Array.Empty(); public string[] information { get; private set; } = Array.Empty(); public string[] disabled { get; private set; } = Array.Empty(); - - public DiagnosticSeverity GetEffectiveSeverity(string code, DiagnosticSeverity defaultSeverity) - => _map.TryGetValue(code, out var severity) ? severity : defaultSeverity; - - public void SetErrorSeverityOptions(string[] errors, string[] warnings, string[] information, string[] disabled) { - _map.Clear(); - // disabled > error > warning > information - foreach (var x in information) { - _map[x] = DiagnosticSeverity.Information; - } - foreach (var x in warnings) { - _map[x] = DiagnosticSeverity.Warning; - } - foreach (var x in errors) { - _map[x] = DiagnosticSeverity.Error; - } - foreach (var x in disabled) { - _map[x] = DiagnosticSeverity.Unspecified; - } - - this.errors = errors; - this.warnings = warnings; - this.information = information; - this.disabled = disabled; - } } public readonly PythonAnalysisOptions analysis = new PythonAnalysisOptions(); diff --git a/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs b/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs index 4211d85f3..b00e10c6a 100644 --- a/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs +++ b/src/LanguageServer/Impl/Diagnostics/DiagnosticsService.cs @@ -30,6 +30,7 @@ internal sealed class DiagnosticsService : IDiagnosticsService, IDisposable { private readonly DisposableBag _disposables = DisposableBag.Create(); private readonly IClientApplication _clientApp; private readonly object _lock = new object(); + private DiagnosticsSeverityMap _severityMap = new DiagnosticsSeverityMap(); private DateTime _lastChangeTime; private bool _changed; @@ -51,7 +52,15 @@ public DiagnosticsService(IServiceContainer services) { public IReadOnlyDictionary> Diagnostics { get { lock (_lock) { - return _diagnostics.ToDictionary(kvp => kvp.Key, kvp => kvp.Value as IReadOnlyList); + 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); } } } @@ -74,6 +83,16 @@ public void Remove(Uri documentUri) { } public int PublishingDelay { get; set; } = 1000; + + public DiagnosticsSeverityMap DiagnosticsSeverityMap { + get => _severityMap; + set { + lock (_lock) { + _severityMap = value; + PublishDiagnostics(); + } + } + } #endregion public void Dispose() { @@ -90,16 +109,19 @@ private void OnIdle(object sender, EventArgs e) { } private void PublishDiagnostics() { + KeyValuePair>[] diagnostics; lock (_lock) { - foreach (var kvp in _diagnostics) { - var parameters = new PublishDiagnosticsParams { - uri = kvp.Key, - diagnostics = kvp.Value.Select(ToDiagnostic).ToArray() - }; - _clientApp.NotifyWithParameterObjectAsync("textDocument/publishDiagnostics", parameters).DoNotWait(); - } + diagnostics = Diagnostics.ToArray(); _changed = false; } + + foreach (var kvp in diagnostics) { + var parameters = new PublishDiagnosticsParams { + uri = kvp.Key, + diagnostics = kvp.Value.Select(ToDiagnostic).ToArray() + }; + _clientApp.NotifyWithParameterObjectAsync("textDocument/publishDiagnostics", parameters).DoNotWait(); + } } private void ClearAllDiagnostics() { diff --git a/src/LanguageServer/Impl/Implementation/Server.Documents.cs b/src/LanguageServer/Impl/Implementation/Server.Documents.cs index d84fe1a5b..ea8de0cba 100644 --- a/src/LanguageServer/Impl/Implementation/Server.Documents.cs +++ b/src/LanguageServer/Impl/Implementation/Server.Documents.cs @@ -68,8 +68,12 @@ public void DidCloseTextDocument(DidCloseTextDocumentParams @params) { private IDocumentAnalysis GetAnalysis(Uri uri, CancellationToken cancellationToken) { var document = _rdt.GetDocument(uri); if (document != null) { - document.GetAnalysisAsync(cancellationToken).Wait(200); - return document.GetAnyAnalysis(); + try { + document.GetAnalysisAsync(cancellationToken).Wait(200); + return document.GetAnyAnalysis(); + } catch (OperationCanceledException) { + return null; + } } _log?.Log(TraceEventType.Error, $"Unable to find document {uri}"); return null; diff --git a/src/LanguageServer/Impl/LanguageServer.cs b/src/LanguageServer/Impl/LanguageServer.cs index bf1cc15a8..71153f0f4 100644 --- a/src/LanguageServer/Impl/LanguageServer.cs +++ b/src/LanguageServer/Impl/LanguageServer.cs @@ -112,16 +112,16 @@ public async Task DidChangeConfiguration(JToken token, CancellationToken cancell _logger.LogLevel = GetLogLevel(analysis).ToTraceEventType(); + HandlePathWatchChange(token, cancellationToken); + var ds = _services.GetService(); ds.PublishingDelay = settings.diagnosticPublishDelay; - HandlePathWatchChange(token, cancellationToken); - - var errors = GetSetting(analysis, "errors", Array.Empty()); - var warnings = GetSetting(analysis, "warnings", Array.Empty()); - var information = GetSetting(analysis, "information", Array.Empty()); - var disabled = GetSetting(analysis, "disabled", Array.Empty()); - settings.analysis.SetErrorSeverityOptions(errors, warnings, information, disabled); + ds.DiagnosticsSeverityMap = new DiagnosticsSeverityMap( + GetSetting(analysis, "errors", Array.Empty()), + GetSetting(analysis, "warnings", Array.Empty()), + GetSetting(analysis, "information", Array.Empty()), + GetSetting(analysis, "disabled", Array.Empty())); await _server.DidChangeConfiguration(new DidChangeConfigurationParams { settings = settings }, cancellationToken); } @@ -404,7 +404,7 @@ private class Prioritizer : IDisposable { public Prioritizer() { _ppc = new PriorityProducerConsumer(4); - Task.Run(ConsumerLoop); + Task.Run(ConsumerLoop).DoNotWait(); } private async Task ConsumerLoop() { diff --git a/src/LanguageServer/Test/DiagnosticsTests.cs b/src/LanguageServer/Test/DiagnosticsTests.cs index 8a03b8f35..6c1b1bf1c 100644 --- a/src/LanguageServer/Test/DiagnosticsTests.cs +++ b/src/LanguageServer/Test/DiagnosticsTests.cs @@ -22,9 +22,11 @@ using Microsoft.Python.Core.Services; using Microsoft.Python.Core.Text; using Microsoft.Python.LanguageServer.Protocol; +using Microsoft.Python.Parsing; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using TestUtilities; +using ErrorCodes = Microsoft.Python.Analysis.Diagnostics.ErrorCodes; namespace Microsoft.Python.LanguageServer.Tests { [TestClass] @@ -158,5 +160,83 @@ public async Task CloseDocument() { ds.Diagnostics.TryGetValue(doc.Uri, out _).Should().BeFalse(); callReceived.Should().BeTrue(); } + + [TestMethod, Priority(0)] + public async Task SeverityMapping() { + const string code = @"import nonexistent"; + + var analysis = await GetAnalysisAsync(code); + var ds = Services.GetService(); + 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; + 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; + }); + + ds.DiagnosticsSeverityMap = new DiagnosticsSeverityMap(new[] { ErrorCodes.UnresolvedImport }, null, null, null); + callReceived.Should().BeTrue(); + + expectedSeverity = DiagnosticSeverity.Information; + callReceived = false; + ds.DiagnosticsSeverityMap = new DiagnosticsSeverityMap(null, null, new[] { ErrorCodes.UnresolvedImport }, null); + ds.Diagnostics[uri][0].Severity.Should().Be(Severity.Information); + callReceived.Should().BeTrue(); + + expectedSeverity = DiagnosticSeverity.Unspecified; + callReceived = false; + 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(); + } + + [TestMethod, Priority(0)] + public async Task SuppressError() { + const string code = @"import nonexistent"; + + var analysis = await GetAnalysisAsync(code); + var ds = Services.GetService(); + 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(); + 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; + }); + + ds.DiagnosticsSeverityMap = new DiagnosticsSeverityMap(null, null, null, new[] { ErrorCodes.UnresolvedImport }); + callReceived.Should().BeTrue(); + } } } diff --git a/src/Parsing/Impl/Severity.cs b/src/Parsing/Impl/Severity.cs index ef01dacdf..2c3c95860 100644 --- a/src/Parsing/Impl/Severity.cs +++ b/src/Parsing/Impl/Severity.cs @@ -16,6 +16,7 @@ namespace Microsoft.Python.Parsing { public enum Severity { + Suppressed, Error, Warning, Information, diff --git a/src/Parsing/Impl/Tokenizer.cs b/src/Parsing/Impl/Tokenizer.cs index 8289016ac..198e3dd71 100644 --- a/src/Parsing/Impl/Tokenizer.cs +++ b/src/Parsing/Impl/Tokenizer.cs @@ -66,7 +66,7 @@ public Tokenizer(PythonLanguageVersion version, ErrorSink errorSink = null, Toke public Tokenizer(PythonLanguageVersion version, ErrorSink errorSink, TokenizerOptions options, Action commentProcessor) { _errors = errorSink ?? ErrorSink.Null; _commentProcessor = commentProcessor; - _state = new State(options, MaxIndent); + _state = new State(options); PrintFunction = false; UnicodeLiterals = false; _names = new Dictionary(new TokenEqualityComparer(this)); @@ -166,9 +166,9 @@ public void Initialize(object state, TextReader reader, SourceLocation initialLo throw new ArgumentException("bad state provided"); } - _state = new State((State)state, Verbatim, MaxIndent); + _state = new State((State)state, Verbatim); } else { - _state = new State(_options, MaxIndent); + _state = new State(_options); } Debug.Assert(_reader == null, "Must uninitialize tokenizer before reinitializing"); @@ -2269,7 +2269,7 @@ struct State : IEquatable { public StringBuilder NextWhiteSpace; public GroupingRecovery GroupingRecovery; - public State(State state, bool verbatim, int maxIndent) { + public State(State state, bool verbatim) { Indent = (int[])state.Indent.Clone(); LastNewLine = state.LastNewLine; BracketLevel = state.BraceLevel; @@ -2287,10 +2287,10 @@ public State(State state, bool verbatim, int maxIndent) { NextWhiteSpace = null; } GroupingRecovery = null; - IndentFormat = new string[maxIndent]; + IndentFormat = new string[MaxIndent]; } - public State(TokenizerOptions options, int maxIndent) { + public State(TokenizerOptions options) { Indent = new int[MaxIndent]; // TODO LastNewLine = true; BracketLevel = ParenLevel = BraceLevel = PendingDedents = IndentLevel = 0; @@ -2304,7 +2304,7 @@ public State(TokenizerOptions options, int maxIndent) { NextWhiteSpace = null; } GroupingRecovery = null; - IndentFormat = new string[maxIndent]; + IndentFormat = new string[MaxIndent]; } public override bool Equals(object obj) {