diff --git a/src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs b/src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs index 75af09d24cf11..2add61d77ac3f 100644 --- a/src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs @@ -5,19 +5,21 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; using Xunit.Abstractions; +using Xunit.Sdk; using Microsoft.CodeAnalysis.VisualBasic; #if NET @@ -368,32 +370,11 @@ string getExpectedLoadPath(string path) if (path.EndsWith(".resources.dll", StringComparison.Ordinal)) { - return getRealSatelliteLoadPath(path) ?? ""; + return loader.GetRealSatelliteLoadPath(path) ?? ""; } return loader.GetRealAnalyzerLoadPath(path ?? ""); } - // When PreparePathToLoad is overridden this returns the most recent - // real path for the given analyzer satellite assembly path - string? getRealSatelliteLoadPath(string originalSatelliteFullPath) - { - // This is a satellite assembly, need to find the mapped path of the real assembly, then - // adjust that mapped path for the suffix of the satellite assembly - // - // Example of dll and it's corresponding satellite assembly - // - // c:\some\path\en-GB\util.resources.dll - // c:\some\path\util.dll - var assemblyFileName = Path.ChangeExtension(Path.GetFileNameWithoutExtension(originalSatelliteFullPath), ".dll"); - - var assemblyDir = Path.GetDirectoryName(originalSatelliteFullPath)!; - var cultureInfo = CultureInfo.GetCultureInfo(Path.GetFileName(assemblyDir)); - assemblyDir = Path.GetDirectoryName(assemblyDir)!; - - // Real assembly is located in the directory above this one - var assemblyPath = Path.Combine(assemblyDir, assemblyFileName); - return loader.GetRealSatelliteLoadPath(assemblyPath, cultureInfo); - } } private static void VerifyAssemblies(AnalyzerAssemblyLoader loader, IEnumerable assemblies, int? copyCount, params string[] assemblyPaths) diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Core.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Core.cs index d3c5c1b354bd2..6e1864c1d7159 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Core.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Core.cs @@ -144,7 +144,7 @@ public DirectoryLoadContext(string directory, AnalyzerAssemblyLoader loader) var assemblyPath = Path.Combine(Directory, simpleName + ".dll"); if (_loader.IsAnalyzerDependencyPath(assemblyPath)) { - (_, var loadPath) = _loader.GetAssemblyInfoForPath(assemblyPath); + (_, var loadPath, _) = _loader.GetAssemblyInfoForPath(assemblyPath); return loadCore(loadPath); } @@ -156,11 +156,11 @@ public DirectoryLoadContext(string directory, AnalyzerAssemblyLoader loader) // loader has a mode where it loads from Stream though and the runtime will not handle // that automatically. Rather than bifurcate our loading behavior between Disk and // Stream both modes just handle satellite loading directly - if (assemblyName.CultureInfo is not null && simpleName.EndsWith(".resources", StringComparison.Ordinal)) + if (!string.IsNullOrEmpty(assemblyName.CultureName) && simpleName.EndsWith(".resources", StringComparison.Ordinal)) { var analyzerFileName = Path.ChangeExtension(simpleName, ".dll"); var analyzerFilePath = Path.Combine(Directory, analyzerFileName); - var satelliteLoadPath = _loader.GetRealSatelliteLoadPath(analyzerFilePath, assemblyName.CultureInfo); + var satelliteLoadPath = _loader.GetSatelliteInfoForPath(analyzerFilePath, assemblyName.CultureName); if (satelliteLoadPath is not null) { return loadCore(satelliteLoadPath); @@ -173,8 +173,7 @@ public DirectoryLoadContext(string directory, AnalyzerAssemblyLoader loader) // be necessary but msbuild target defaults have caused a number of customers to // fall into this path. See discussion here for where it comes up // https://github.com/dotnet/roslyn/issues/56442 - var (_, bestRealPath) = _loader.GetBestPath(assemblyName); - if (bestRealPath is not null) + if (_loader.GetBestPath(assemblyName).BestRealPath is string bestRealPath) { return loadCore(bestRealPath); } @@ -201,7 +200,7 @@ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) var assemblyPath = Path.Combine(Directory, unmanagedDllName + ".dll"); if (_loader.IsAnalyzerDependencyPath(assemblyPath)) { - (_, var loadPath) = _loader.GetAssemblyInfoForPath(assemblyPath); + (_, var loadPath, _) = _loader.GetAssemblyInfoForPath(assemblyPath); return LoadUnmanagedDllFromPath(loadPath); } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Desktop.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Desktop.cs index 98bb543cdae6a..e0f63768158ef 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Desktop.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Desktop.cs @@ -117,30 +117,11 @@ internal bool EnsureResolvedUnhooked() { try { - const string resourcesExtension = ".resources"; var assemblyName = new AssemblyName(args.Name); - var simpleName = assemblyName.Name; - var isSatelliteAssembly = - assemblyName.CultureInfo is not null && - simpleName.EndsWith(resourcesExtension, StringComparison.Ordinal); - - if (isSatelliteAssembly) - { - // Satellite assemblies should get the best path information using the - // non-resource part of the assembly name. Once the path information is obtained - // GetSatelliteInfoForPath will translate to the resource assembly path. - assemblyName.Name = simpleName[..^resourcesExtension.Length]; - } - - var (originalPath, realPath) = GetBestPath(assemblyName); - if (isSatelliteAssembly && originalPath is not null) - { - realPath = GetRealSatelliteLoadPath(originalPath, assemblyName.CultureInfo); - } - - if (realPath is not null) + string? bestPath = GetBestPath(assemblyName).BestRealPath; + if (bestPath is not null) { - return Assembly.LoadFrom(realPath); + return Assembly.LoadFrom(bestPath); } return null; diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs index 30755244d7869..6dea8d3daaae3 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -48,17 +47,7 @@ internal abstract partial class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader /// /// Access must be guarded by /// - private readonly Dictionary _analyzerAssemblyInfoMap = new(); - - /// - /// Mapping of analyzer dependency original full path and culture to the real satellite - /// assembly path. If the satellite assembly doesn't exist for the original analyzer and - /// culture, the real path value stored will be null. - /// - /// - /// Access must be guarded by - /// - private readonly Dictionary<(string OriginalAnalyzerPath, CultureInfo CultureInfo), string?> _analyzerSatelliteAssemblyRealPaths = new(); + private readonly Dictionary SatelliteCultureNames)?> _analyzerAssemblyInfoMap = new(); /// /// Maps analyzer dependency simple names to the set of original full paths it was loaded from. This _only_ @@ -149,7 +138,7 @@ public void AddDependencyLocation(string fullPath) _knownAssemblyPathsBySimpleName[simpleName] = paths.Add(fullPath); } - // This type assumes the file system is static for the duration of the + // This type assumses the file system is static for the duration of the // it's instance. Repeated calls to this method, even if the underlying // file system contents, should reuse the results of the first call. _ = _analyzerAssemblyInfoMap.TryAdd(fullPath, null); @@ -162,7 +151,7 @@ public Assembly LoadFromPath(string originalAnalyzerPath) CompilerPathUtilities.RequireAbsolutePath(originalAnalyzerPath, nameof(originalAnalyzerPath)); - (AssemblyName? assemblyName, _) = GetAssemblyInfoForPath(originalAnalyzerPath); + (AssemblyName? assemblyName, _, _) = GetAssemblyInfoForPath(originalAnalyzerPath); // Not a managed assembly, nothing else to do if (assemblyName is null) @@ -189,7 +178,7 @@ public Assembly LoadFromPath(string originalAnalyzerPath) /// because we only want information for registered paths. Using unregistered paths inside the /// implementation should result in errors. /// - protected (AssemblyName? AssemblyName, string RealAssemblyPath) GetAssemblyInfoForPath(string originalAnalyzerPath) + protected (AssemblyName? AssemblyName, string RealAssemblyPath, ImmutableHashSet SatelliteCultureNames) GetAssemblyInfoForPath(string originalAnalyzerPath) { CheckIfDisposed(); @@ -206,7 +195,8 @@ public Assembly LoadFromPath(string originalAnalyzerPath) } } - string realPath = PreparePathToLoad(originalAnalyzerPath); + var resourceAssemblyCultureNames = getResourceAssemblyCultureNames(originalAnalyzerPath); + string realPath = PreparePathToLoad(originalAnalyzerPath, resourceAssemblyCultureNames); AssemblyName? assemblyName; try { @@ -222,68 +212,35 @@ public Assembly LoadFromPath(string originalAnalyzerPath) lock (_guard) { - _analyzerAssemblyInfoMap[originalAnalyzerPath] = (assemblyName, realPath); + _analyzerAssemblyInfoMap[originalAnalyzerPath] = (assemblyName, realPath, resourceAssemblyCultureNames); } - return (assemblyName, realPath); - } + return (assemblyName, realPath, resourceAssemblyCultureNames); - /// - /// Get the path a satellite assembly should be loaded from for the given original - /// analyzer path and culture - /// - /// - /// This is used during assembly resolve for satellite assemblies to determine the - /// path from where the satellite assembly should be loaded for the specified culture. - /// This method calls to ensure this path - /// contains the satellite assembly. - /// - internal string? GetRealSatelliteLoadPath(string originalAnalyzerPath, CultureInfo cultureInfo) - { - CheckIfDisposed(); - - string? realSatelliteAssemblyPath = null; - - lock (_guard) + // Discover the culture names for any satellite dlls related to this analyzer. These + // need to be understood when handling the resource loading in certain cases. + static ImmutableHashSet getResourceAssemblyCultureNames(string originalAnalyzerPath) { - if (_analyzerSatelliteAssemblyRealPaths.TryGetValue((originalAnalyzerPath, cultureInfo), out realSatelliteAssemblyPath)) + var path = Path.GetDirectoryName(originalAnalyzerPath)!; + using var enumerator = Directory.EnumerateDirectories(path, "*").GetEnumerator(); + if (!enumerator.MoveNext()) { - return realSatelliteAssemblyPath; + return ImmutableHashSet.Empty; } - } - - var actualCultureName = getSatelliteCultureName(originalAnalyzerPath, cultureInfo); - if (actualCultureName != null) - { - realSatelliteAssemblyPath = PrepareSatelliteAssemblyToLoad(originalAnalyzerPath, actualCultureName); - } - - lock (_guard) - { - _analyzerSatelliteAssemblyRealPaths[(originalAnalyzerPath, cultureInfo)] = realSatelliteAssemblyPath; - } - return realSatelliteAssemblyPath; - - // Discover the most specific culture name to use for the specified analyzer path and culture - static string? getSatelliteCultureName(string originalAnalyzerPath, CultureInfo cultureInfo) - { - var path = Path.GetDirectoryName(originalAnalyzerPath)!; var resourceFileName = GetSatelliteFileName(Path.GetFileName(originalAnalyzerPath)); - - while (cultureInfo != CultureInfo.InvariantCulture) + var builder = ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase); + do { - var resourceFilePath = Path.Combine(path, cultureInfo.Name, resourceFileName); - + var resourceFilePath = Path.Combine(enumerator.Current, resourceFileName); if (File.Exists(resourceFilePath)) { - return cultureInfo.Name; + builder.Add(Path.GetFileName(enumerator.Current)); } - - cultureInfo = cultureInfo.Parent; } + while (enumerator.MoveNext()); - return null; + return builder.ToImmutableHashSet(); } } @@ -293,6 +250,23 @@ public Assembly LoadFromPath(string originalAnalyzerPath) return GetBestPath(assemblyName).BestOriginalPath; } + /// + /// Get the real load path of the satellite assembly given the original path to the analyzer + /// and the desired culture name. + /// + protected string? GetSatelliteInfoForPath(string originalAnalyzerPath, string cultureName) + { + var (_, realAssemblyPath, satelliteCultureNames) = GetAssemblyInfoForPath(originalAnalyzerPath); + if (!satelliteCultureNames.Contains(cultureName)) + { + return null; + } + + var satelliteFileName = GetSatelliteFileName(Path.GetFileName(realAssemblyPath)); + var dir = Path.GetDirectoryName(realAssemblyPath)!; + return Path.Combine(dir, cultureName, satelliteFileName); + } + /// /// Return the best (original, real) path information for loading an assembly with the specified . /// @@ -321,7 +295,7 @@ public Assembly LoadFromPath(string originalAnalyzerPath) AssemblyName? bestName = null; foreach (var candidateOriginalPath in paths.OrderBy(StringComparer.Ordinal)) { - (AssemblyName? candidateName, string candidateRealPath) = GetAssemblyInfoForPath(candidateOriginalPath); + (AssemblyName? candidateName, string candidateRealPath, _) = GetAssemblyInfoForPath(candidateOriginalPath); if (candidateName is null) { continue; @@ -353,18 +327,15 @@ protected static string GetSatelliteFileName(string assemblyFileName) => /// When overridden in a derived class, allows substituting an assembly path after we've /// identified the context to load an assembly in, but before the assembly is actually /// loaded from disk. This is used to substitute out the original path with the shadow-copied version. + /// + /// In the case the is moved to a new location then + /// the resource DLLs for the specified must also be + /// moved _but_ retain their original relative location. /// - protected abstract string PreparePathToLoad(string assemblyFilePath); - - /// - /// When overridden in a derived class, allows substituting a satellite assembly path after we've - /// identified the context to load a satellite assembly in, but before the satellite assembly is actually - /// loaded from disk. This is used to substitute out the original path with the shadow-copied version. - /// - protected abstract string PrepareSatelliteAssemblyToLoad(string assemblyFilePath, string cultureName); + protected abstract string PreparePathToLoad(string assemblyFilePath, ImmutableHashSet resourceAssemblyCultureNames); /// - /// When is overridden this returns the most recent + /// When is overridden this returns the most recent /// real path calculated for the /// internal string GetRealAnalyzerLoadPath(string originalFullPath) @@ -382,6 +353,30 @@ internal string GetRealAnalyzerLoadPath(string originalFullPath) } } + /// + /// When is overridden this returns the most recent + /// real path for the given analyzer satellite assembly path + /// + internal string? GetRealSatelliteLoadPath(string originalSatelliteFullPath) + { + // This is a satellite assembly, need to find the mapped path of the real assembly, then + // adjust that mapped path for the suffix of the satellite assembly + // + // Example of dll and it's corresponding satellite assembly + // + // c:\some\path\en-GB\util.resources.dll + // c:\some\path\util.dll + var assemblyFileName = Path.ChangeExtension(Path.GetFileNameWithoutExtension(originalSatelliteFullPath), ".dll"); + + var assemblyDir = Path.GetDirectoryName(originalSatelliteFullPath)!; + var cultureName = Path.GetFileName(assemblyDir); + assemblyDir = Path.GetDirectoryName(assemblyDir)!; + + // Real assembly is located in the directory above this one + var assemblyPath = Path.Combine(assemblyDir, assemblyFileName); + return GetSatelliteInfoForPath(assemblyPath, cultureName); + } + internal (string OriginalAssemblyPath, string RealAssemblyPath)[] GetPathMapSnapshot() { CheckIfDisposed(); diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.cs index 40f7afa19d91f..5a5b30802ceb3 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.cs @@ -38,18 +38,7 @@ internal DefaultAnalyzerAssemblyLoader(System.Runtime.Loader.AssemblyLoadContext /// /// The default implementation is to simply load in place. /// - protected override string PreparePathToLoad(string fullPath) => fullPath; - - /// - /// The default implementation is to simply load in place. - /// - protected override string PrepareSatelliteAssemblyToLoad(string assemblyFilePath, string cultureName) - { - var directory = Path.GetDirectoryName(assemblyFilePath)!; - var fileName = GetSatelliteFileName(Path.GetFileName(assemblyFilePath)); - - return Path.Combine(directory, cultureName, fileName); - } + protected override string PreparePathToLoad(string fullPath, ImmutableHashSet satelliteCultureNames) => fullPath; /// /// Return an which does not lock assemblies on disk that is diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs index 1df2f9c6b5cf7..2532fe2c37aa0 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs @@ -8,9 +8,8 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using Roslyn.Utilities; using System.Collections.Immutable; -using System.Reflection; +using Roslyn.Utilities; #if NET using System.Runtime.Loader; @@ -37,7 +36,6 @@ internal sealed class ShadowCopyAnalyzerAssemblyLoader : AnalyzerAssemblyLoader private readonly Lazy<(string directory, Mutex)> _shadowCopyDirectoryAndMutex; private readonly ConcurrentDictionary> _mvidPathMap = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary<(Guid, string), Task> _mvidSatelliteAssemblyPathMap = new ConcurrentDictionary<(Guid, string), Task>(); internal string BaseDirectory => _baseDirectory; @@ -129,62 +127,22 @@ private void DeleteLeftoverDirectories() } } - protected override string PreparePathToLoad(string originalAnalyzerPath) + protected override string PreparePathToLoad(string originalAnalyzerPath, ImmutableHashSet cultureNames) { var mvid = AssemblyUtilities.ReadMvid(originalAnalyzerPath); - - return PrepareLoad(_mvidPathMap, mvid, copyAnalyzerContents); - - string copyAnalyzerContents() - { - var analyzerFileName = Path.GetFileName(originalAnalyzerPath); - var shadowDirectory = Path.Combine(_shadowCopyDirectoryAndMutex.Value.directory, mvid.ToString()); - var shadowAnalyzerPath = Path.Combine(shadowDirectory, analyzerFileName); - CopyFile(originalAnalyzerPath, shadowAnalyzerPath); - - return shadowAnalyzerPath; - } - } - - protected override string PrepareSatelliteAssemblyToLoad(string originalAnalyzerPath, string cultureName) - { - var mvid = AssemblyUtilities.ReadMvid(originalAnalyzerPath); - - return PrepareLoad(_mvidSatelliteAssemblyPathMap, (mvid, cultureName), copyAnalyzerContents); - - string copyAnalyzerContents() - { - var analyzerFileName = Path.GetFileName(originalAnalyzerPath); - var shadowDirectory = Path.Combine(_shadowCopyDirectoryAndMutex.Value.directory, mvid.ToString()); - var shadowAnalyzerPath = Path.Combine(shadowDirectory, analyzerFileName); - - var originalDirectory = Path.GetDirectoryName(originalAnalyzerPath)!; - var satelliteFileName = GetSatelliteFileName(analyzerFileName); - - var originalSatellitePath = Path.Combine(originalDirectory, cultureName, satelliteFileName); - var shadowSatellitePath = Path.Combine(shadowDirectory, cultureName, satelliteFileName); - CopyFile(originalSatellitePath, shadowSatellitePath); - - return shadowSatellitePath; - } - } - - private static string PrepareLoad(ConcurrentDictionary> mvidPathMap, TKey mvidKey, Func copyContents) - where TKey : notnull - { - if (mvidPathMap.TryGetValue(mvidKey, out Task? copyTask)) + if (_mvidPathMap.TryGetValue(mvid, out Task? copyTask)) { return copyTask.Result; } var tcs = new TaskCompletionSource(); - var task = mvidPathMap.GetOrAdd(mvidKey, tcs.Task); + var task = _mvidPathMap.GetOrAdd(mvid, tcs.Task); if (object.ReferenceEquals(task, tcs.Task)) { // This thread won and we need to do the copy. try { - var shadowAnalyzerPath = copyContents(); + var shadowAnalyzerPath = copyAnalyzerContents(); tcs.SetResult(shadowAnalyzerPath); return shadowAnalyzerPath; } @@ -199,19 +157,43 @@ private static string PrepareLoad(ConcurrentDictionary> // This thread lost and we need to wait for the winner to finish the copy. return task.Result; } - } - private static void CopyFile(string originalPath, string shadowCopyPath) - { - var directory = Path.GetDirectoryName(shadowCopyPath); - if (directory is null) + string copyAnalyzerContents() { - throw new ArgumentException($"Shadow copy path '{shadowCopyPath}' must not be the root directory"); + var analyzerFileName = Path.GetFileName(originalAnalyzerPath); + var shadowDirectory = Path.Combine(_shadowCopyDirectoryAndMutex.Value.directory, mvid.ToString()); + var shadowAnalyzerPath = Path.Combine(shadowDirectory, analyzerFileName); + copyFile(originalAnalyzerPath, shadowAnalyzerPath); + + if (cultureNames.IsEmpty) + { + return shadowAnalyzerPath; + } + + var originalDirectory = Path.GetDirectoryName(originalAnalyzerPath)!; + var satelliteFileName = GetSatelliteFileName(analyzerFileName); + foreach (var cultureName in cultureNames) + { + var originalSatellitePath = Path.Combine(originalDirectory, cultureName, satelliteFileName); + var shadowSatellitePath = Path.Combine(shadowDirectory, cultureName, satelliteFileName); + copyFile(originalSatellitePath, shadowSatellitePath); + } + + return shadowAnalyzerPath; } - _ = Directory.CreateDirectory(directory); - File.Copy(originalPath, shadowCopyPath); - ClearReadOnlyFlagOnFile(new FileInfo(shadowCopyPath)); + static void copyFile(string originalPath, string shadowCopyPath) + { + var directory = Path.GetDirectoryName(shadowCopyPath); + if (directory is null) + { + throw new ArgumentException($"Shadow copy path '{shadowCopyPath}' must not be the root directory"); + } + + _ = Directory.CreateDirectory(directory); + File.Copy(originalPath, shadowCopyPath); + ClearReadOnlyFlagOnFile(new FileInfo(shadowCopyPath)); + } } private static void ClearReadOnlyFlagOnFiles(string directoryPath) diff --git a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs index 664c25c3a3306..0c6be7ecdf9fd 100644 --- a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs +++ b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs @@ -709,10 +709,7 @@ private static AnalyzerFileReference CreateShadowCopiedAnalyzerReference(TempRoo private class MissingAnalyzerLoader() : AnalyzerAssemblyLoader([]) { - protected override string PreparePathToLoad(string fullPath) - => throw new FileNotFoundException(fullPath); - - protected override string PrepareSatelliteAssemblyToLoad(string fullPath, string cultureName) + protected override string PreparePathToLoad(string fullPath, ImmutableHashSet cultureNames) => throw new FileNotFoundException(fullPath); } diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs index fd36b3e185862..9e6aeeeae79b5 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.IO; using System.Collections.Immutable; +using System.IO; namespace Microsoft.CodeAnalysis.Remote.Diagnostics; @@ -22,15 +22,9 @@ public RemoteAnalyzerAssemblyLoader(string baseDirectory, ImmutableArray cultureNames) { var fixedPath = Path.GetFullPath(Path.Combine(_baseDirectory, Path.GetFileName(fullPath))); return File.Exists(fixedPath) ? fixedPath : fullPath; } - - protected override string PrepareSatelliteAssemblyToLoad(string fullPath, string cultureName) - { - var fixedPath = Path.GetFullPath(Path.Combine(_baseDirectory, cultureName, Path.GetFileName(fullPath))); - return File.Exists(fixedPath) ? fixedPath : fullPath; - } }