diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs index 514ccea1c..290321eec 100644 --- a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs +++ b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs @@ -37,7 +37,6 @@ 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) { } @@ -62,29 +61,6 @@ internal async Task InitializeAsync(CancellationToken cancellationToken = defaul cancellationToken.ThrowIfCancellationRequested(); } - public async Task> GetSearchPathsAsync(CancellationToken cancellationToken = default) { - if (_searchPaths != null) { - return _searchPaths; - } - - _searchPaths = await GetInterpreterSearchPathsAsync(cancellationToken); - Debug.Assert(_searchPaths != null, "Should have search paths"); - _searchPaths = _searchPaths ?? Array.Empty(); - - _log?.Log(TraceEventType.Verbose, "Python search paths:"); - foreach (var s in _searchPaths) { - _log?.Log(TraceEventType.Verbose, $" {s}"); - } - - var configurationSearchPaths = Configuration.SearchPaths ?? Array.Empty(); - - _log?.Log(TraceEventType.Verbose, "Configuration search paths:"); - foreach (var s in configurationSearchPaths) { - _log?.Log(TraceEventType.Verbose, $" {s}"); - } - return _searchPaths; - } - protected override IPythonModule CreateModule(string name) { var moduleImport = CurrentPathResolver.GetModuleImportFromModuleName(name); if (moduleImport == null) { @@ -135,11 +111,11 @@ protected override IPythonModule CreateModule(string name) { return GetRdt().AddModule(mco); } - private async Task> GetInterpreterSearchPathsAsync(CancellationToken cancellationToken = default) { + private async Task> GetInterpreterSearchPathsAsync(CancellationToken cancellationToken = default) { if (!_fs.FileExists(Configuration.InterpreterPath)) { _log?.Log(TraceEventType.Warning, "Interpreter does not exist:", Configuration.InterpreterPath); _ui?.ShowMessageAsync(Resources.InterpreterNotFound, TraceEventType.Error); - return Array.Empty(); + return Array.Empty(); } _log?.Log(TraceEventType.Information, "GetCurrentSearchPaths", Configuration.InterpreterPath); @@ -148,11 +124,11 @@ private async Task> GetInterpreterSearchPathsAsync(Cancell var ps = _services.GetService(); var paths = await PythonLibraryPath.GetSearchPathsAsync(Configuration, fs, ps, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - return paths.MaybeEnumerate().Select(p => p.Path).ToArray(); + return paths.ToArray(); } catch (InvalidOperationException ex) { _log?.Log(TraceEventType.Warning, "Exception getting search paths", ex); _ui?.ShowMessageAsync(Resources.ExceptionGettingSearchPaths, TraceEventType.Error); - return Array.Empty(); + return Array.Empty(); } } @@ -211,16 +187,13 @@ public async Task ReloadAsync(CancellationToken cancellationToken = default) { var addedRoots = new HashSet(); addedRoots.UnionWith(PathResolver.SetRoot(Root)); - InterpreterPaths = await GetSearchPathsAsync(cancellationToken); + var ps = _services.GetService(); - IEnumerable userSearchPaths = Configuration.SearchPaths; - InterpreterPaths = InterpreterPaths.Except(userSearchPaths, StringExtensions.PathsStringComparer).Where(p => !p.PathEquals(Root)); + var paths = await GetInterpreterSearchPathsAsync(cancellationToken); + var (interpreterPaths, userPaths) = PythonLibraryPath.ClassifyPaths(Root, _fs, paths, Configuration.SearchPaths); - if (Root != null) { - var underRoot = userSearchPaths.ToLookup(p => _fs.IsPathUnderRoot(Root, p)); - userSearchPaths = underRoot[true]; - InterpreterPaths = underRoot[false].Concat(InterpreterPaths); - } + InterpreterPaths = interpreterPaths.Select(p => p.Path); + var userSearchPaths = userPaths.Select(p => p.Path); _log?.Log(TraceEventType.Information, "Interpreter search paths:"); foreach (var s in InterpreterPaths) { diff --git a/src/Analysis/Ast/Impl/get_search_paths.py b/src/Analysis/Ast/Impl/get_search_paths.py index 290662f48..53cec1cf0 100644 --- a/src/Analysis/Ast/Impl/get_search_paths.py +++ b/src/Analysis/Ast/Impl/get_search_paths.py @@ -45,6 +45,11 @@ def clean(path): BEFORE_SITE = set(clean(p) for p in BEFORE_SITE) AFTER_SITE = set(clean(p) for p in AFTER_SITE) +try: + SITE_PKGS = set(clean(p) for p in site.getsitepackages()) +except AttributeError: + SITE_PKGS = set() + for prefix in [ sys.prefix, sys.exec_prefix, @@ -69,4 +74,7 @@ def clean(path): if p in BEFORE_SITE: print("%s|stdlib|" % p) elif p in AFTER_SITE: - print("%s||" % p) + if p in SITE_PKGS: + print("%s|site|" % p) + else: + print("%s|pth|" % p) diff --git a/src/Analysis/Ast/Test/PathClassificationTests.cs b/src/Analysis/Ast/Test/PathClassificationTests.cs new file mode 100644 index 000000000..429df72b7 --- /dev/null +++ b/src/Analysis/Ast/Test/PathClassificationTests.cs @@ -0,0 +1,218 @@ +// 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; +using System.IO; +using FluentAssertions; +using Microsoft.Python.Analysis.Core.Interpreter; +using Microsoft.Python.Core.IO; +using Microsoft.Python.Core.OS; +using Microsoft.Python.Tests.Utilities.FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestUtilities; + +namespace Microsoft.Python.Analysis.Tests { + [TestClass] + public class PathClassificationTests { + private readonly FileSystem _fs = new FileSystem(new OSPlatform()); + + public TestContext TestContext { get; set; } + + [TestInitialize] + public void TestInitialize() + => TestEnvironmentImpl.TestInitialize($"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}"); + + [TestCleanup] + public void Cleanup() => TestEnvironmentImpl.TestCleanup(); + + [TestMethod] + public void Plain() { + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + + var venv = Path.Combine(root, "venv"); + var venvLib = Path.Combine(venv, "Lib"); + var venvSitePackages = Path.Combine(venvLib, "site-packages"); + + var fromInterpreter = new[] { + new PythonLibraryPath(venvLib, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venv, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venvSitePackages, PythonLibraryPathType.Site), + }; + + var (interpreterPaths, userPaths) = PythonLibraryPath.ClassifyPaths(root, _fs, fromInterpreter, Array.Empty()); + + interpreterPaths.Should().BeEquivalentToWithStrictOrdering(new[] { + new PythonLibraryPath(venvLib, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venv, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venvSitePackages, PythonLibraryPathType.Site), + }); + + userPaths.Should().BeEmpty(); + } + + [TestMethod] + public void WithSrcDir() { + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + + var venv = Path.Combine(root, "venv"); + var venvLib = Path.Combine(venv, "Lib"); + var venvSitePackages = Path.Combine(venvLib, "site-packages"); + + var src = Path.Combine(root, "src"); + + var fromInterpreter = new[] { + new PythonLibraryPath(venvLib, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venv, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venvSitePackages, PythonLibraryPathType.Site), + }; + + var fromUser = new[] { + "./src", + }; + + var (interpreterPaths, userPaths) = PythonLibraryPath.ClassifyPaths(root, _fs, fromInterpreter, fromUser); + + interpreterPaths.Should().BeEquivalentToWithStrictOrdering(new[] { + new PythonLibraryPath(venvLib, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venv, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venvSitePackages, PythonLibraryPathType.Site), + }); + + userPaths.Should().BeEquivalentToWithStrictOrdering(new[] { + new PythonLibraryPath(src, PythonLibraryPathType.Unspecified), + }); + } + + [TestMethod] + public void NormalizeUser() { + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + + var src = Path.Combine(root, "src"); + + var fromUser = new[] { + "./src/", + }; + + var (interpreterPaths, userPaths) = PythonLibraryPath.ClassifyPaths(root, _fs, Array.Empty(), fromUser); + + interpreterPaths.Should().BeEmpty(); + + userPaths.Should().BeEquivalentToWithStrictOrdering(new[] { + new PythonLibraryPath(src, PythonLibraryPathType.Unspecified), + }); + } + + [TestMethod] + public void NestedUser() { + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + + var src = Path.Combine(root, "src"); + var srcSomething = Path.Combine(src, "something"); + var srcFoo = Path.Combine(src, "foo"); + var srcFooBar = Path.Combine(srcFoo, "bar"); + + var fromUser = new[] { + "./src", + "./src/something", + "./src/foo/bar", + "./src/foo", + }; + + var (interpreterPaths, userPaths) = PythonLibraryPath.ClassifyPaths(root, _fs, Array.Empty(), fromUser); + + interpreterPaths.Should().BeEmpty(); + + userPaths.Should().BeEquivalentToWithStrictOrdering(new[] { + new PythonLibraryPath(src, PythonLibraryPathType.Unspecified), + new PythonLibraryPath(srcSomething, PythonLibraryPathType.Unspecified), + new PythonLibraryPath(srcFooBar, PythonLibraryPathType.Unspecified), + new PythonLibraryPath(srcFoo, PythonLibraryPathType.Unspecified), + }); + } + + [TestMethod] + public void NestedUserOrdering() { + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + + var src = Path.Combine(root, "src"); + var srcSomething = Path.Combine(src, "something"); + var srcFoo = Path.Combine(src, "foo"); + var srcFooBar = Path.Combine(srcFoo, "bar"); + + var fromUser = new[] { + "./src/foo", + "./src/foo/bar", + "./src", + "./src/something", + }; + + var (interpreterPaths, userPaths) = PythonLibraryPath.ClassifyPaths(root, _fs, Array.Empty(), fromUser); + + interpreterPaths.Should().BeEmpty(); + + userPaths.Should().BeEquivalentToWithStrictOrdering(new[] { + new PythonLibraryPath(srcFoo, PythonLibraryPathType.Unspecified), + new PythonLibraryPath(srcFooBar, PythonLibraryPathType.Unspecified), + new PythonLibraryPath(src, PythonLibraryPathType.Unspecified), + new PythonLibraryPath(srcSomething, PythonLibraryPathType.Unspecified), + }); + } + + [TestMethod] + public void InsideStdLib() { + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + + var venv = Path.Combine(root, "venv"); + var venvLib = Path.Combine(venv, "Lib"); + var venvSitePackages = Path.Combine(venvLib, "site-packages"); + var inside = Path.Combine(venvSitePackages, "inside"); + var what = Path.Combine(venvSitePackages, "what"); + + var src = Path.Combine(root, "src"); + + var fromInterpreter = new[] { + new PythonLibraryPath(venvLib, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venv, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venvSitePackages, PythonLibraryPathType.Site), + new PythonLibraryPath(inside, PythonLibraryPathType.Pth), + }; + + var fromUser = new[] { + "./src", + "./venv/Lib/site-packages/what", + }; + + var (interpreterPaths, userPaths) = PythonLibraryPath.ClassifyPaths(root, _fs, fromInterpreter, fromUser); + + interpreterPaths.Should().BeEquivalentToWithStrictOrdering(new[] { + new PythonLibraryPath(venvLib, PythonLibraryPathType.StdLib), + new PythonLibraryPath(venv, PythonLibraryPathType.StdLib), + new PythonLibraryPath(what, PythonLibraryPathType.Unspecified), + new PythonLibraryPath(venvSitePackages, PythonLibraryPathType.Site), + new PythonLibraryPath(inside, PythonLibraryPathType.Pth), + }); + + userPaths.Should().BeEquivalentToWithStrictOrdering(new[] { + new PythonLibraryPath(src, PythonLibraryPathType.Unspecified), + }); + } + } +} diff --git a/src/Analysis/Core/Impl/Interpreter/PythonLibraryPath.cs b/src/Analysis/Core/Impl/Interpreter/PythonLibraryPath.cs index cb9eecb27..2a76ad069 100644 --- a/src/Analysis/Core/Impl/Interpreter/PythonLibraryPath.cs +++ b/src/Analysis/Core/Impl/Interpreter/PythonLibraryPath.cs @@ -18,8 +18,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Python.Core; @@ -28,43 +26,78 @@ using IOPath = System.IO.Path; namespace Microsoft.Python.Analysis.Core.Interpreter { - public sealed class PythonLibraryPath { - private readonly string _modulePrefix; - - private static readonly Regex ParseRegex = new Regex( - @"(?[^|]+)\|(?stdlib)?\|(?[^|]+)?" - ); + public enum PythonLibraryPathType { + Unspecified, + StdLib, + Site, + Pth, + } - public PythonLibraryPath(string path, bool isStandardLibrary, string modulePrefix) { - Path = path; - IsStandardLibrary = isStandardLibrary; - _modulePrefix = modulePrefix; + public sealed class PythonLibraryPath : IEquatable { + public PythonLibraryPath(string path, PythonLibraryPathType type = PythonLibraryPathType.Unspecified, string modulePrefix = null) { + Path = PathUtils.NormalizePathAndTrim(path); + Type = type; + ModulePrefix = modulePrefix ?? string.Empty; } + public PythonLibraryPath(string path, bool isStandardLibrary, string modulePrefix) : + this(path, isStandardLibrary ? PythonLibraryPathType.StdLib : PythonLibraryPathType.Unspecified, modulePrefix) { } + public string Path { get; } - public bool IsStandardLibrary { get; } + public PythonLibraryPathType Type { get; } + + public string ModulePrefix { get; } = string.Empty; + + public bool IsStandardLibrary => Type == PythonLibraryPathType.StdLib; + + public override string ToString() { + var type = string.Empty; - public string ModulePrefix => _modulePrefix ?? string.Empty; + switch (Type) { + case PythonLibraryPathType.StdLib: + type = "stdlib"; + break; + case PythonLibraryPathType.Site: + type = "site"; + break; + case PythonLibraryPathType.Pth: + type = "pth"; + break; + } - public override string ToString() - => "{0}|{1}|{2}".FormatInvariant(Path, IsStandardLibrary ? "stdlib" : "", _modulePrefix ?? ""); + return "{0}|{1}|{2}".FormatInvariant(Path, type, ModulePrefix); + } public static PythonLibraryPath Parse(string s) { if (string.IsNullOrEmpty(s)) { throw new ArgumentNullException("source"); } - - var m = ParseRegex.Match(s); - if (!m.Success || !m.Groups["path"].Success) { + + var parts = s.Split(new[] { '|' }, 3); + if (parts.Length < 3) { throw new FormatException(); } - - return new PythonLibraryPath( - m.Groups["path"].Value, - m.Groups["stdlib"].Success, - m.Groups["prefix"].Success ? m.Groups["prefix"].Value : null - ); + + var path = parts[0]; + var ty = parts[1]; + var prefix = parts[2]; + + PythonLibraryPathType type = PythonLibraryPathType.Unspecified; + + switch (ty) { + case "stdlib": + type = PythonLibraryPathType.StdLib; + break; + case "site": + type = PythonLibraryPathType.Site; + break; + case "pth": + type = PythonLibraryPathType.Pth; + break; + } + + return new PythonLibraryPath(path, type, prefix); } /// @@ -80,16 +113,16 @@ public static List GetDefaultSearchPaths(string library) { return result; } - result.Add(new PythonLibraryPath(library, true, null)); + result.Add(new PythonLibraryPath(library, PythonLibraryPathType.StdLib)); var sitePackages = IOPath.Combine(library, "site-packages"); if (!Directory.Exists(sitePackages)) { return result; } - result.Add(new PythonLibraryPath(sitePackages, false, null)); + result.Add(new PythonLibraryPath(sitePackages)); result.AddRange(ModulePath.ExpandPathFiles(sitePackages) - .Select(p => new PythonLibraryPath(p, false, null)) + .Select(p => new PythonLibraryPath(p)) ); return result; @@ -152,7 +185,7 @@ public static async Task> GetSearchPathsFromInterpreterA try { var output = await ps.ExecuteAndCaptureOutputAsync(startInfo, cancellationToken); - return output.Split(new [] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).Select(s => { + return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).Select(s => { if (s.PathStartsWith(tempWorkingDir)) { return null; } @@ -170,5 +203,88 @@ public static async Task> GetSearchPathsFromInterpreterA fs.DeleteDirectory(tempWorkingDir, true); } } + + public static (IReadOnlyList interpreterPaths, IReadOnlyList userPaths) ClassifyPaths( + string root, + IFileSystem fs, + IEnumerable fromInterpreter, + IEnumerable fromUser + ) { +#if DEBUG + Debug.Assert(root == null || root.PathEquals(PathUtils.NormalizePathAndTrim(root))); + Debug.Assert(!fromInterpreter.Any(p => !p.Path.PathEquals(PathUtils.NormalizePathAndTrim(p.Path)))); +#endif + + // Clean up user configured paths. + // 1) Normalize paths. + // 2) If a path isn't rooted, then root it relative to the workspace root. If there is no root, just continue. + // 3) Trim off any ending separators for consistency. + // 4) Remove any empty paths, FS root paths (bad idea), or paths equal to the root. + // 5) Deduplicate, preserving the order specified by the user. + var fromUserList = fromUser + .Select(PathUtils.NormalizePath) + .Select(p => root == null || IOPath.IsPathRooted(p) ? p : IOPath.GetFullPath(IOPath.Combine(root, p))) // TODO: Replace with GetFullPath(p, root) when .NET Standard 2.1 is out. + .Select(PathUtils.TrimEndSeparator) + .Where(p => !string.IsNullOrWhiteSpace(p) && p != "/" && !p.PathEquals(root)) + .Distinct(PathEqualityComparer.Instance) + .ToList(); + + // Remove any interpreter paths specified in the user config so they can be reclassified. + // The user list is usually small; List.Contains should not be too slow. + fromInterpreter.Where(p => !fromUserList.Contains(p.Path, PathEqualityComparer.Instance)) + .Split(p => p.Type == PythonLibraryPathType.StdLib, out var stdlib, out var withoutStdlib); + + // Pull out stdlib paths, and make them always be interpreter paths. + var interpreterPaths = new List(stdlib); + + var userPaths = new List(); + + var allPaths = fromUserList.Select(p => new PythonLibraryPath(p)) + .Concat(withoutStdlib.Where(p => !p.Path.PathEquals(root))); + + foreach (var p in allPaths) { + // If path is within a stdlib path, then treat it as interpreter. + if (stdlib.Any(s => fs.IsPathUnderRoot(s.Path, p.Path))) { + interpreterPaths.Add(p); + continue; + } + + // If path is outside the workspace, then treat it as interpreter. + if (root == null || !fs.IsPathUnderRoot(root, p.Path)) { + interpreterPaths.Add(p); + continue; + } + + userPaths.Add(p); + } + + return (interpreterPaths, userPaths.ToList()); + } + + public override bool Equals(object obj) => obj is PythonLibraryPath other && Equals(other); + + public override int GetHashCode() { + // TODO: Replace with HashCode.Combine when .NET Standard 2.1 is out. + unchecked { + var hashCode = Path.GetHashCode(); + hashCode = (hashCode * 397) ^ Type.GetHashCode(); + hashCode = (hashCode * 397) ^ ModulePrefix.GetHashCode(); + return hashCode; + } + } + + public bool Equals(PythonLibraryPath other) { + if (other is null) { + return false; + } + + return Path.PathEquals(other.Path) + && Type == other.Type + && ModulePrefix == other.ModulePrefix; + } + + public static bool operator ==(PythonLibraryPath left, PythonLibraryPath right) => left?.Equals(right) ?? right is null; + + public static bool operator !=(PythonLibraryPath left, PythonLibraryPath right) => !(left?.Equals(right) ?? right is null); } } diff --git a/src/Core/Impl/Extensions/StringExtensions.cs b/src/Core/Impl/Extensions/StringExtensions.cs index 88ee18f16..b27ed2d29 100644 --- a/src/Core/Impl/Extensions/StringExtensions.cs +++ b/src/Core/Impl/Extensions/StringExtensions.cs @@ -204,6 +204,9 @@ public static bool EqualsOrdinal(this string s, string other) public static bool PathEquals(this string s, string other) => string.Equals(s, other, PathsStringComparison); + public static int PathCompare(this string s, string other) + => string.Compare(s, other, PathsStringComparison); + 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; public static bool ContainsOrdinal(this string s, string value, bool ignoreCase = false) diff --git a/src/Core/Impl/IO/PathUtils.cs b/src/Core/Impl/IO/PathUtils.cs index a9513ee59..a5140c1a0 100644 --- a/src/Core/Impl/IO/PathUtils.cs +++ b/src/Core/Impl/IO/PathUtils.cs @@ -493,5 +493,7 @@ public static string NormalizePath(string path) { ); return isDir ? EnsureEndSeparator(newPath) : newPath; } + + public static string NormalizePathAndTrim(string path) => TrimEndSeparator(NormalizePath(path)); } } diff --git a/src/LanguageServer/Impl/Implementation/Server.cs b/src/LanguageServer/Impl/Implementation/Server.cs index 61324ece6..7bb4571a6 100644 --- a/src/LanguageServer/Impl/Implementation/Server.cs +++ b/src/LanguageServer/Impl/Implementation/Server.cs @@ -115,8 +115,7 @@ public async Task InitializeAsync(InitializeParams @params, Ca _rootDir = @params.rootUri != null ? @params.rootUri.ToAbsolutePath() : @params.rootPath; if (_rootDir != null) { - _rootDir = PathUtils.NormalizePath(_rootDir); - _rootDir = PathUtils.TrimEndSeparator(_rootDir); + _rootDir = PathUtils.NormalizePathAndTrim(_rootDir); } Version.TryParse(@params.initializationOptions.interpreter.properties?.Version, out var version); @@ -125,19 +124,10 @@ public async Task InitializeAsync(InitializeParams @params, Ca interpreterPath: @params.initializationOptions.interpreter.properties?.InterpreterPath, version: version ) { - // 1) Split on ';' to support older VS Code extension versions which send paths as a single entry separated by ';'. TODO: Eventually remove. - // 2) Normalize paths. - // 3) If a path isn't rooted, then root it relative to the workspace root. If _rootDir is null, then accept the path as-is. - // 4) Trim off any ending separator for a consistent style. - // 5) Filter out any entries which are the same as the workspace root; they are redundant. Also ignore "/" to work around the extension (for now). - // 6) Remove duplicates. + // Split on ';' to support older VS Code extension versions which send paths as a single entry separated by ';'. TODO: Eventually remove. + // Note that the actual classification of these paths as user/library is done later in MainModuleResolution.ReloadAsync. SearchPaths = @params.initializationOptions.searchPaths .Select(p => p.Split(';', StringSplitOptions.RemoveEmptyEntries)).SelectMany() - .Select(PathUtils.NormalizePath) - .Select(p => _rootDir == null || Path.IsPathRooted(p) ? p : Path.GetFullPath(p, _rootDir)) - .Select(PathUtils.TrimEndSeparator) - .Where(p => !string.IsNullOrWhiteSpace(p) && p != "/" && !p.PathEquals(_rootDir)) - .Distinct(PathEqualityComparer.Instance) .ToList(), TypeshedPath = @params.initializationOptions.typeStubSearchPaths.FirstOrDefault() };