diff --git a/src/Analysis/Engine/Impl/AnalysisHashSet.cs b/src/Analysis/Engine/Impl/AnalysisHashSet.cs index a612be922..65143d677 100644 --- a/src/Analysis/Engine/Impl/AnalysisHashSet.cs +++ b/src/Analysis/Engine/Impl/AnalysisHashSet.cs @@ -275,7 +275,19 @@ public bool SetEquals(IAnalysisSet other) { return true; } + // Only access Count once to prevent wasted time in bucket locks. + var lCount = Count; + var rCount = other.Count; + + if (lCount == 0 && rCount == 0) { + return true; + } + if (Comparer == other.Comparer) { + if (lCount != rCount) { + return false; + } + // Quick check for any unmatched hashcodes. // This can conclusively prove the sets are not equal, but cannot // prove equality. @@ -290,17 +302,16 @@ public bool SetEquals(IAnalysisSet other) { } } - var otherHc = new HashSet(other, _comparer); - foreach (var key in this) { - if (!otherHc.Remove(key)) { - return false; - } - } - if (otherHc.Any()) { + if (lCount == 0 || rCount == 0) { return false; } - return true; + if (lCount == 1 && rCount == 1) { + return _comparer.Equals(this.First(), other.First()); + } + + var hs = new HashSet(other, _comparer); + return hs.SetEquals(this); } /// diff --git a/src/Analysis/Engine/Impl/AnalysisSet.cs b/src/Analysis/Engine/Impl/AnalysisSet.cs index efd8a5a38..821f8ba7c 100644 --- a/src/Analysis/Engine/Impl/AnalysisSet.cs +++ b/src/Analysis/Engine/Impl/AnalysisSet.cs @@ -534,6 +534,8 @@ public static bool ContainsAny(this IAnalysisSet set, IAnalysisSet values) { sealed class ObjectComparer : IEqualityComparer, IEqualityComparer { public static readonly ObjectComparer Instance = new ObjectComparer(); + private ObjectComparer() { } + public bool Equals(AnalysisValue x, AnalysisValue y) { #if FULL_VALIDATION if (x != null && y != null) { @@ -574,7 +576,7 @@ sealed class UnionComparer : IEqualityComparer, IEqualityComparer public readonly int Strength; - public UnionComparer(int strength = 0) { + private UnionComparer(int strength = 0) { Strength = strength; } diff --git a/src/Analysis/Engine/Impl/Analyzer/DDG.cs b/src/Analysis/Engine/Impl/Analyzer/DDG.cs index a579ff4c0..34a99aab6 100644 --- a/src/Analysis/Engine/Impl/Analyzer/DDG.cs +++ b/src/Analysis/Engine/Impl/Analyzer/DDG.cs @@ -296,6 +296,9 @@ public override bool Walk(FromImportStatement node) { case ImportNotFound notFound: MakeUnresolvedImport(notFound.FullName, node.Root); return false; + case NoKnownParentPackage _: + MakeNoKnownParentPackageImport(node.Root); + return false; default: return false; } @@ -464,6 +467,10 @@ private void MakeUnresolvedImport(string name, Node spanNode) { ProjectState.AddDiagnostic(spanNode, _unit, ErrorMessages.UnresolvedImport(name), DiagnosticSeverity.Warning, ErrorMessages.UnresolvedImportCode); } + private void MakeNoKnownParentPackageImport(Node spanNode) { + ProjectState.AddDiagnostic(spanNode, _unit, Resources.ErrorRelativeImportNoPackage, DiagnosticSeverity.Warning, ErrorMessages.UnresolvedImportCode); + } + private void ImportModule(in ImportStatement node, in IModule module, in DottedName moduleImportExpression, in NameExpression asNameExpression) { // "import fob.oar as baz" is handled as // baz = import_module('fob.oar') diff --git a/src/Analysis/Engine/Impl/DependencyResolution/NoKnownParentPackage.cs b/src/Analysis/Engine/Impl/DependencyResolution/NoKnownParentPackage.cs new file mode 100644 index 000000000..1d275bf49 --- /dev/null +++ b/src/Analysis/Engine/Impl/DependencyResolution/NoKnownParentPackage.cs @@ -0,0 +1,19 @@ +// Python Tools for Visual Studio +// 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. + +namespace Microsoft.PythonTools.Analysis.DependencyResolution { + internal class NoKnownParentPackage : IImportSearchResult { } +} diff --git a/src/Analysis/Engine/Impl/DependencyResolution/PathResolverSnapshot.cs b/src/Analysis/Engine/Impl/DependencyResolution/PathResolverSnapshot.cs index 66d4c7fee..679d189d2 100644 --- a/src/Analysis/Engine/Impl/DependencyResolution/PathResolverSnapshot.cs +++ b/src/Analysis/Engine/Impl/DependencyResolution/PathResolverSnapshot.cs @@ -187,16 +187,11 @@ public IImportSearchResult GetImportsFromRelativePath(in string modulePath, in i return default; } - if (parentCount > lastEdge.PathLength) { - // Can't get outside of the root - return default; - } - var fullNameList = relativePath.ToList(); if (lastEdge.IsNonRooted) { // Handle relative imports only for modules in the same folder if (parentCount > 1) { - return default; + return new NoKnownParentPackage(); } if (parentCount == 1 && fullNameList.Count == 1 && lastEdge.Start.TryGetChild(fullNameList[0], out var nameNode)) { @@ -209,12 +204,27 @@ public IImportSearchResult GetImportsFromRelativePath(in string modulePath, in i .ToString()); } + var relativeInWorkDirectory = false; + if (parentCount > lastEdge.PathLength - 2) { + relativeInWorkDirectory = _workDirectory.EqualsOrdinal(lastEdge.FirstEdge.End.Name, IgnoreCaseInPaths); + + // Relative path must be only inside package + // Exception for working directory cause it can be a root directory of the package + if (!relativeInWorkDirectory) { + return new NoKnownParentPackage(); + } + } + var relativeParentEdge = lastEdge.GetPrevious(parentCount); var rootEdges = new List(); - for (var i = 0; i < _roots.Count; i++) { - if (RootContains(i, relativeParentEdge, out var rootEdge)) { - rootEdges.Add(rootEdge); + if (relativeInWorkDirectory) { + rootEdges.Add(lastEdge.FirstEdge); + } else { + for (var i = 0; i < _roots.Count; i++) { + if (RootContains(i, relativeParentEdge, out var rootEdge)) { + rootEdges.Add(rootEdge); + } } } @@ -226,7 +236,13 @@ public IImportSearchResult GetImportsFromRelativePath(in string modulePath, in i return default; } - var fullName = GetFullModuleNameBuilder(relativeParentEdge).Append(".", fullNameList).ToString(); + var fullNameBuilder = GetFullModuleNameBuilder(relativeParentEdge); + if (!relativeParentEdge.IsFirst) { + AppendName(fullNameBuilder, relativeParentEdge.End.Name); + fullNameBuilder.Append("."); + } + var fullName = fullNameBuilder.Append(".", fullNameList).ToString(); + return new ImportNotFound(fullName); } @@ -250,7 +266,7 @@ private static bool TryCreateModuleImport(Edge lastEdge, out ModuleImport module => TryCreateModuleImport(lastEdge.FirstEdge.End, lastEdge.End, out moduleImport); private static bool TryCreateModuleImport(Node rootNode, Node moduleNode, out ModuleImport moduleImport) { - if (moduleNode.TryGetChild("__init__", out var initPyNode) && initPyNode.IsModule) { + if (IsPackageWithInitPy(moduleNode, out var initPyNode)) { moduleImport = new ModuleImport(moduleNode.Name, initPyNode.FullModuleName, rootNode.Name, initPyNode.ModulePath, false); return true; } @@ -517,7 +533,7 @@ private static bool TryFindImport(IEnumerable rootEdges, List full private static bool TryFindName(in Edge edge, in IEnumerable nameParts, out Edge lastEdge) { lastEdge = edge; foreach (var name in nameParts) { - if (lastEdge.End.IsModule) { + if (lastEdge.End.IsModule && !IsPackageWithInitPy(lastEdge.End, out _)) { return false; } var index = lastEdge.End.GetChildIndex(name); @@ -653,7 +669,7 @@ private static StringBuilder GetFullModuleNameBuilder(in Edge lastEdge) { while (edge != lastEdge) { AppendName(sb, edge.End.Name); edge = edge.Next; - }; + } return sb; } @@ -744,6 +760,9 @@ private static int GetModuleNameStart(string rootedModulePath) private static int GetModuleNameEnd(string rootedModulePath) => IsPythonCompiled(rootedModulePath) ? rootedModulePath.IndexOf('.', GetModuleNameStart(rootedModulePath)) : rootedModulePath.LastIndexOf('.'); + private static bool IsPackageWithInitPy(Node node, out Node initPyNode) + => node.TryGetChild("__init__", out initPyNode) && initPyNode.IsModule; + private static bool IsNotInitPy(string name) => !name.EqualsOrdinal("__init__"); diff --git a/src/Analysis/Engine/Impl/Infrastructure/Extensions/StringExtensions.cs b/src/Analysis/Engine/Impl/Infrastructure/Extensions/StringExtensions.cs index 6c0eceb39..bd3ca29a9 100644 --- a/src/Analysis/Engine/Impl/Infrastructure/Extensions/StringExtensions.cs +++ b/src/Analysis/Engine/Impl/Infrastructure/Extensions/StringExtensions.cs @@ -195,6 +195,9 @@ public static bool EqualsIgnoreCase(this string s, string other) public static bool EqualsOrdinal(this string s, string other) => string.Equals(s, other, StringComparison.Ordinal); + public static bool EqualsOrdinal(this string s, string other, bool ignoreCase) + => string.Equals(s, other, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + public static bool EqualsOrdinal(this string s, int index, string other, int otherIndex, int length, bool ignoreCase = false) => string.Compare(s, index, other, otherIndex, length, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) == 0; diff --git a/src/Analysis/Engine/Impl/Intellisense/AnalysisQueue.cs b/src/Analysis/Engine/Impl/Intellisense/AnalysisQueue.cs index 8cc4d0a54..0d5cfa1ff 100644 --- a/src/Analysis/Engine/Impl/Intellisense/AnalysisQueue.cs +++ b/src/Analysis/Engine/Impl/Intellisense/AnalysisQueue.cs @@ -67,7 +67,7 @@ private async Task ConsumerLoop() { return; } catch (Exception ex) when (!ex.IsCriticalException()) { UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, false)); - Dispose(); + throw; } } RaiseEventOnThreadPool(AnalysisComplete); @@ -208,7 +208,7 @@ public async Task Handler(CancellationToken cancellationToken) { } private sealed class QueueItemComparer : IEqualityComparer { - public static IEqualityComparer Instance { get; } = new QueueItemComparer(); + public static readonly IEqualityComparer Instance = new QueueItemComparer(); private QueueItemComparer() { } public bool Equals(QueueItem x, QueueItem y) => Equals(x.Key, y.Key); diff --git a/src/Analysis/Engine/Impl/Interpreter/Ast/AstModuleResolution.cs b/src/Analysis/Engine/Impl/Interpreter/Ast/AstModuleResolution.cs index 4ccf04d6b..5c5784e42 100644 --- a/src/Analysis/Engine/Impl/Interpreter/Ast/AstModuleResolution.cs +++ b/src/Analysis/Engine/Impl/Interpreter/Ast/AstModuleResolution.cs @@ -205,6 +205,12 @@ public async Task TryImportModuleAsync(string name, PathR } catch (OperationCanceledException) { _log?.Log(TraceLevel.Error, "ImportTimeout", name, "ImportFromSearchPaths"); return TryImportModuleResult.Timeout; + } catch (Exception ex) when ( + ex is IOException // FileNotFoundException, DirectoryNotFoundException, PathTooLongException, etc + || ex is UnauthorizedAccessException + ) { + _log?.Log(TraceLevel.Error, "ImportException", name, "ImportFromSearchPaths", ex.GetType().Name, ex.Message); + return TryImportModuleResult.NeedRetry; } } diff --git a/src/Analysis/Engine/Impl/LocationInfo.cs b/src/Analysis/Engine/Impl/LocationInfo.cs index b850e5cf7..d38ce2930 100644 --- a/src/Analysis/Engine/Impl/LocationInfo.cs +++ b/src/Analysis/Engine/Impl/LocationInfo.cs @@ -83,9 +83,12 @@ public bool Equals(ILocationInfo other) { /// Provides an IEqualityComparer that compares line, column and project entries. By /// default locations are equaitable based upon only line/project entry. /// - public static IEqualityComparer FullComparer { get; } = new FullLocationComparer(); + public static IEqualityComparer FullComparer => FullLocationComparer.Instance; sealed class FullLocationComparer : IEqualityComparer, IEqualityComparer { + public static readonly FullLocationComparer Instance = new FullLocationComparer(); + + private FullLocationComparer() { } public bool Equals(LocationInfo x, LocationInfo y) => EqualsImpl(x, y); public bool Equals(ILocationInfo x, ILocationInfo y) => EqualsImpl(x, y); diff --git a/src/Analysis/Engine/Impl/OverloadResult.cs b/src/Analysis/Engine/Impl/OverloadResult.cs index f6191eab3..a38d7440e 100644 --- a/src/Analysis/Engine/Impl/OverloadResult.cs +++ b/src/Analysis/Engine/Impl/OverloadResult.cs @@ -411,8 +411,8 @@ internal ParameterResult GetParameterResultFromParameterInfo(IParameterInfo para } class OverloadResultComparer : EqualityComparer { - public static IEqualityComparer Instance = new OverloadResultComparer(false); - public static IEqualityComparer WeakInstance = new OverloadResultComparer(true); + public static readonly IEqualityComparer Instance = new OverloadResultComparer(false); + public static readonly IEqualityComparer WeakInstance = new OverloadResultComparer(true); private readonly bool _weak; diff --git a/src/Analysis/Engine/Impl/PythonAnalyzer.cs b/src/Analysis/Engine/Impl/PythonAnalyzer.cs index 5694a89ee..8c1fa16c0 100644 --- a/src/Analysis/Engine/Impl/PythonAnalyzer.cs +++ b/src/Analysis/Engine/Impl/PythonAnalyzer.cs @@ -586,10 +586,11 @@ internal AnalysisValue GetCached(object key, Func maker) { internal BuiltinInstanceInfo GetInstance(IPythonType type) => GetBuiltinType(type).Instance; - internal BuiltinClassInfo GetBuiltinType(IPythonType type) => - (BuiltinClassInfo)GetCached(type, - () => MakeBuiltinType(type) - ) ?? ClassInfos[BuiltinTypeId.Object]; + internal BuiltinClassInfo GetBuiltinType(IPythonType type) + // Cached value may or may not be a class info. Previous calls to GetAnalysisValueFromObjects + // may have cached a different object for the type. For example, IPythonFunction would cache + // BuiltinFunctionInfo and not BuiltinClassInfo. Therefore, don't use direct cast. + => GetCached(type, () => MakeBuiltinType(type)) as BuiltinClassInfo ?? MakeBuiltinType(type); private BuiltinClassInfo MakeBuiltinType(IPythonType type) { switch (type.TypeId) { @@ -903,7 +904,9 @@ private AggregateProjectEntry GetAggregateWorker(IProjectEntry[] all) { } class AggregateComparer : IEqualityComparer { - public static AggregateComparer Instance = new AggregateComparer(); + public static readonly AggregateComparer Instance = new AggregateComparer(); + + private AggregateComparer() { } public bool Equals(IProjectEntry[] x, IProjectEntry[] y) { if (x.Length != y.Length) { diff --git a/src/Analysis/Engine/Impl/Resources.Designer.cs b/src/Analysis/Engine/Impl/Resources.Designer.cs index 87df9a494..6386a9c57 100644 --- a/src/Analysis/Engine/Impl/Resources.Designer.cs +++ b/src/Analysis/Engine/Impl/Resources.Designer.cs @@ -105,6 +105,15 @@ internal static string ErrorNotCallableEmpty { } } + /// + /// Looks up a localized string similar to attempted relative import with no known parent package. + /// + internal static string ErrorRelativeImportNoPackage { + get { + return ResourceManager.GetString("ErrorRelativeImportNoPackage", resourceCulture); + } + } + /// /// Looks up a localized string similar to unresolved import '{0}'. /// diff --git a/src/Analysis/Engine/Impl/Resources.resx b/src/Analysis/Engine/Impl/Resources.resx index c67372ff4..04b144aca 100644 --- a/src/Analysis/Engine/Impl/Resources.resx +++ b/src/Analysis/Engine/Impl/Resources.resx @@ -312,6 +312,9 @@ unresolved import '{0}' + + attempted relative import with no known parent package + '{0}' used before definition diff --git a/src/Analysis/Engine/Impl/Values/BuiltinNamespace.cs b/src/Analysis/Engine/Impl/Values/BuiltinNamespace.cs index 8c5bb60fb..4ff075462 100644 --- a/src/Analysis/Engine/Impl/Values/BuiltinNamespace.cs +++ b/src/Analysis/Engine/Impl/Values/BuiltinNamespace.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.PythonTools.Analysis.Infrastructure; using Microsoft.PythonTools.Interpreter; using Microsoft.PythonTools.Parsing.Ast; @@ -105,7 +106,7 @@ public TMemberContainer ContainedValue { public virtual ILocatedMember GetLocatedMember() => null; - public override IEnumerable Locations => GetLocatedMember()?.Locations.MaybeEnumerate(); + public override IEnumerable Locations => GetLocatedMember()?.Locations ?? Enumerable.Empty(); public override bool Equals(object obj) { if (obj is BuiltinNamespace bn && GetType() == bn.GetType()) { diff --git a/src/Analysis/Engine/Impl/Values/ClassInfo.cs b/src/Analysis/Engine/Impl/Values/ClassInfo.cs index ef6e14ec1..b313f334f 100644 --- a/src/Analysis/Engine/Impl/Values/ClassInfo.cs +++ b/src/Analysis/Engine/Impl/Values/ClassInfo.cs @@ -831,6 +831,10 @@ public static IAnalysisSet GetMemberFromMroNoReferences(IEnumerable 0) { + break; + } } return result; } diff --git a/src/Analysis/Engine/Impl/Values/SpecializedNamespace.cs b/src/Analysis/Engine/Impl/Values/SpecializedNamespace.cs index 2b12d7669..8da4c6506 100644 --- a/src/Analysis/Engine/Impl/Values/SpecializedNamespace.cs +++ b/src/Analysis/Engine/Impl/Values/SpecializedNamespace.cs @@ -209,15 +209,15 @@ public override bool IsOfType(IAnalysisSet klass) { return _original.IsOfType(klass); } - public override IEnumerable Locations => _original?.Locations.MaybeEnumerate(); + public override IEnumerable Locations => _original?.Locations ?? Enumerable.Empty(); public override string Name => _original == null ? base.Name : _original.Name; - public override IEnumerable Overloads => _original?.Overloads.MaybeEnumerate(); + public override IEnumerable Overloads => _original?.Overloads ?? Enumerable.Empty(); public override IPythonType PythonType => _original?.PythonType; - internal override IEnumerable References => _original?.References.MaybeEnumerate(); + internal override IEnumerable References => _original?.References ?? Enumerable.Empty(); public override PythonMemberType MemberType => _original == null ? PythonMemberType.Unknown : _original.MemberType; diff --git a/src/Analysis/Engine/Test/AddTestSpecificSearchPathAttribute.cs b/src/Analysis/Engine/Test/AddTestSpecificSearchPathAttribute.cs new file mode 100644 index 000000000..939052196 --- /dev/null +++ b/src/Analysis/Engine/Test/AddTestSpecificSearchPathAttribute.cs @@ -0,0 +1,28 @@ +// Python Tools for Visual Studio +// 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; + +namespace AnalysisTests { + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class AddTestSpecificSearchPathAttribute : Attribute { + public string RelativeSearchPath { get; } + + public AddTestSpecificSearchPathAttribute(string relativeSearchPath) { + RelativeSearchPath = relativeSearchPath; + } + } +} diff --git a/src/Analysis/Engine/Test/EventTaskSources.cs b/src/Analysis/Engine/Test/EventTaskSources.cs index d24ca8145..9b5f283d9 100644 --- a/src/Analysis/Engine/Test/EventTaskSources.cs +++ b/src/Analysis/Engine/Test/EventTaskSources.cs @@ -37,6 +37,11 @@ public static class Server { new EventTaskSource( (o, e) => o.OnParseComplete += e, (o, e) => o.OnParseComplete -= e); + + public static readonly EventTaskSource OnPublishDiagnostics = + new EventTaskSource( + (o, e) => o.OnPublishDiagnostics += e, + (o, e) => o.OnPublishDiagnostics -= e); } } } diff --git a/src/Analysis/Engine/Test/ImportTests.cs b/src/Analysis/Engine/Test/ImportTests.cs index 4f1ad0acd..84b9dfc0e 100644 --- a/src/Analysis/Engine/Test/ImportTests.cs +++ b/src/Analysis/Engine/Test/ImportTests.cs @@ -19,11 +19,15 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Python.LanguageServer; using Microsoft.Python.LanguageServer.Implementation; using Microsoft.PythonTools.Analysis; +using Microsoft.PythonTools.Analysis.Analyzer; using Microsoft.PythonTools.Analysis.FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using TestUtilities; +using Resources = Microsoft.PythonTools.Analysis.Resources; namespace AnalysisTests { [TestClass] @@ -251,6 +255,145 @@ import projectB.foo.baz completion.Should().HaveLabels("foo"); } + [ServerTestMethod(LatestAvailable3X = true, TestSpecificRootUri = true), Priority(0)] + [AddTestSpecificSearchPath("src")] + public async Task Diagnostics_InvalidRelativeImportInUserSearchPath(Server server) { + var modulePath = "src/module.py"; + var moduleCode = "TEST = 0"; + var appPath = "src/app.py"; + var appCode = "from .module import TEST"; + var args = new PublishDiagnosticsEventArgs(); + server.OnPublishDiagnostics += (o, e) => args = e; + + await server.OpenDocumentAndGetUriAsync(modulePath, moduleCode); + await server.OpenDocumentAndGetUriAsync(appPath, appCode); + await server.WaitForCompleteAnalysisAsync(CancellationToken.None); + + args.diagnostics.Should().ContainSingle() + .Which.message.Should().Be(Resources.ErrorRelativeImportNoPackage); + } + + [ServerTestMethod(LatestAvailable3X = true, TestSpecificRootUri = true), Priority(0)] + public async Task Diagnostics_RelativeImportInWorkingDirectory(Server server) { + var modulePath = "module.py"; + var moduleCode = "TEST = 0"; + var appPath = "app.py"; + var appCode = "from .module import *"; + var args = new PublishDiagnosticsEventArgs(); + server.OnPublishDiagnostics += (o, e) => args = e; + + await server.OpenDocumentAndGetUriAsync(modulePath, moduleCode); + await server.OpenDocumentAndGetUriAsync(appPath, appCode); + await server.WaitForCompleteAnalysisAsync(CancellationToken.None); + + args.diagnostics.Should().BeEmpty(); + } + + [ServerTestMethod(LatestAvailable3X = true, TestSpecificRootUri = true), Priority(0)] + [AddTestSpecificSearchPath("src")] + public async Task Diagnostics_InvalidRelativeImportInWorkingDirectory(Server server) { + var modulePath = "src/module.py"; + var moduleCode = "TEST = 0"; + var appPath = "app.py"; + var appCode = "from .module import TEST"; + var args = new PublishDiagnosticsEventArgs(); + server.OnPublishDiagnostics += (o, e) => args = e; + + await server.OpenDocumentAndGetUriAsync(modulePath, moduleCode); + await server.OpenDocumentAndGetUriAsync(appPath, appCode); + await server.WaitForCompleteAnalysisAsync(CancellationToken.None); + + args.diagnostics.Should().ContainSingle() + .Which.message.Should().Be(ErrorMessages.UnresolvedImport("module")); + } + + [ServerTestMethod(LatestAvailable3X = true), Priority(0)] + public async Task Diagnostics_RelativeImportInNonRooted(Server server) { + var modulePath = "module.py"; + var moduleCode = "TEST = 0"; + var appPath = "app.py"; + var appCode = "from .module import TEST"; + var args = new PublishDiagnosticsEventArgs(); + server.OnPublishDiagnostics += (o, e) => args = e; + + await server.OpenDocumentAndGetUriAsync(modulePath, moduleCode); + await server.OpenDocumentAndGetUriAsync(appPath, appCode); + await server.WaitForCompleteAnalysisAsync(CancellationToken.None); + + args.diagnostics.Should().BeEmpty(); + } + + [ServerTestMethod(LatestAvailable3X = true, TestSpecificRootUri = true), Priority(0)] + public async Task Diagnostics_RelativeImportInPackage(Server server) { + var module2Path = "package/module2.py"; + var module2Code = "TEST = 0"; + var module1Path = "package/module1.py"; + var module1Code = "from .module2 import TEST"; + var initPath = "package/__init__.py"; + var args = new PublishDiagnosticsEventArgs(); + server.OnPublishDiagnostics += (o, e) => args = e; + + await server.OpenDocumentAndGetUriAsync(initPath, string.Empty); + await server.OpenDocumentAndGetUriAsync(module1Path, module1Code); + await server.OpenDocumentAndGetUriAsync(module2Path, module2Code); + await server.WaitForCompleteAnalysisAsync(CancellationToken.None); + + args.diagnostics.Should().BeEmpty(); + } + + [ServerTestMethod(LatestAvailable3X = true, TestSpecificRootUri = true), Priority(0)] + public async Task Diagnostics_RelativeImportInPackage_ModuleWithNameOfFolderWithInitPy(Server server) { + var initPyPath = "package/module/__init__.py"; + var module1Path = "package/module.py"; + var module1Code = "TEST = 1"; + var module2Path = "package/module/sub/module.py"; + var module2Code = "TEST = 2"; + var testPath = "package/test.py"; + var testCode = "from .module.sub.module import TEST"; + var args = new PublishDiagnosticsEventArgs(); + + var testUri = await server.OpenDocumentAndGetUriAsync(testPath, testCode); + server.OnPublishDiagnostics += (o, e) => { + if (e.uri == testUri) { + args = e; + } + }; + + await server.OpenDocumentAndGetUriAsync(initPyPath, string.Empty); + await server.OpenDocumentAndGetUriAsync(module1Path, module1Code); + await server.OpenDocumentAndGetUriAsync(module2Path, module2Code); + + await server.WaitForCompleteAnalysisAsync(CancellationToken.None); + + args.diagnostics.Should().BeEmpty(); + } + + [ServerTestMethod(LatestAvailable3X = true, TestSpecificRootUri = true), Priority(0)] + public async Task Diagnostics_InvalidRelativeImportInPackage_ModuleWithNameOfFolder(Server server) { + var module2Path = "package/module/submodule.py"; + var module2Code = "TEST = 2"; + var module1Path = "package/module.py"; + var module1Code = "TEST = 1"; + var testPath = "package/test.py"; + var testCode = "from .module.submodule import TEST"; + var args = new PublishDiagnosticsEventArgs(); + + var testUri = await server.OpenDocumentAndGetUriAsync(testPath, testCode); + server.OnPublishDiagnostics += (o, e) => { + if (e.uri == testUri) { + args = e; + } + }; + + await server.OpenDocumentAndGetUriAsync(module1Path, module1Code); + await server.OpenDocumentAndGetUriAsync(module2Path, module2Code); + + await server.WaitForCompleteAnalysisAsync(CancellationToken.None); + + args.diagnostics.Should().ContainSingle() + .Which.message.Should().Be(ErrorMessages.UnresolvedImport("package.module.submodule")); + } + [ServerTestMethod(LatestAvailable3X = true), Priority(0)] public async Task Completions_SysModuleChain(Server server) { var content1 = @"import module2.mod as mod diff --git a/src/Analysis/Engine/Test/ServerTestMethodAttribute.cs b/src/Analysis/Engine/Test/ServerTestMethodAttribute.cs index d2dffff01..a12a4470d 100644 --- a/src/Analysis/Engine/Test/ServerTestMethodAttribute.cs +++ b/src/Analysis/Engine/Test/ServerTestMethodAttribute.cs @@ -15,6 +15,7 @@ // permissions and limitations under the License. using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.Python.LanguageServer.Implementation; using Microsoft.PythonTools.Analysis; @@ -45,6 +46,7 @@ public override TestResult[] Execute(ITestMethod testMethod) { private TestResult ExecuteWithServer(ITestMethod testMethod) { var arguments = ExtendArguments(testMethod.Arguments); var filesToCreate = testMethod.GetAttributes(false); + var searchPathToAdd = testMethod.GetAttributes(false); TestEnvironmentImpl.AddBeforeAfterTest(async () => { var interpreterConfiguration = GetInterpreterConfiguration(arguments); @@ -53,7 +55,8 @@ private TestResult ExecuteWithServer(ITestMethod testMethod) { await TestData.CreateTestSpecificFileAsync(file.RelativeFilePath, file.Content); } - var server = await new Server().InitializeAsync(interpreterConfiguration, rootUri); + var searchPaths = searchPathToAdd.Select(a => TestData.GetTestSpecificPath(a.RelativeSearchPath)); + var server = await new Server().InitializeAsync(interpreterConfiguration, rootUri, searchPaths); if (DefaultTypeshedPath) { var limits = server.Analyzer.Limits; limits.UseTypeStubPackages = true; diff --git a/src/LanguageServer/Impl/LanguageServer.Lifetime.cs b/src/LanguageServer/Impl/LanguageServer.Lifetime.cs index 4b8f7aef4..1c2ea8b62 100644 --- a/src/LanguageServer/Impl/LanguageServer.Lifetime.cs +++ b/src/LanguageServer/Impl/LanguageServer.Lifetime.cs @@ -64,14 +64,14 @@ public async Task Shutdown() { // https://microsoft.github.io/language-server-protocol/specification#shutdown await _server.Shutdown(); _shutdown = true; - _idleTimeTracker.Dispose(); + _idleTimeTracker?.Dispose(); } [JsonRpcMethod("exit")] public async Task Exit() { await _server.Exit(); _sessionTokenSource.Cancel(); - _idleTimeTracker.Dispose(); + _idleTimeTracker?.Dispose(); // Per https://microsoft.github.io/language-server-protocol/specification#exit Environment.Exit(_shutdown ? 0 : 1); } diff --git a/src/LanguageServer/Impl/LanguageServer.cs b/src/LanguageServer/Impl/LanguageServer.cs index bbba9bb4f..398a697a5 100644 --- a/src/LanguageServer/Impl/LanguageServer.cs +++ b/src/LanguageServer/Impl/LanguageServer.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -164,7 +165,16 @@ public async Task DidChangeConfiguration(JToken token, CancellationToken cancell await _server.DidChangeConfiguration(new DidChangeConfigurationParams { settings = settings }, cancellationToken); if (!_filesLoaded) { - await LoadDirectoryFiles(); + try { + await LoadDirectoryFiles(); + } catch (Exception ex) when ( + ex is IOException // FileNotFoundException, DirectoryNotFoundException, PathTooLongException, etc + || ex is UnauthorizedAccessException + ) { + // These exceptions are not caused by the LS, but by invalid/inaccessible user-specified paths. + _server.ShowMessage(MessageType.Error, $"Failed to load files in {_initParams.rootPath} - {ex.GetType().Name}: {ex.Message}"); + return; + } } _filesLoaded = true; }