Repository: dotnet/roslyn
File: src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerPathResolver.cs (C#)

Code before:
        public string GetResolvedAnalyzerPath(string originalAnalyzerPath)
        {
            var analyzerShadowDir = GetAnalyzerShadowDirectory(originalAnalyzerPath);
            var analyzerShadowPath = Path.Combine(analyzerShadowDir, Path.GetFileName(originalAnalyzerPath));
            ShadowCopyFile(originalAnalyzerPath, analyzerShadowPath);
            return analyzerShadowPath;
        }

        public string? GetResolvedSatellitePath(string originalAnalyzerPath, CultureInfo cultureInfo)
        {
            var satelliteFilePath = AnalyzerAssemblyLoader.GetSatelliteAssemblyPath(originalAnalyzerPath, cultureInfo);
            if (satelliteFilePath is null)
            {
                return null;
            }

            var analyzerShadowDir = GetAnalyzerShadowDirectory(originalAnalyzerPath);
            var satelliteFileName = Path.GetFileName(satelliteFilePath);
            var satelliteDirectoryName = Path.GetFileName(Path.GetDirectoryName(satelliteFilePath));
            var shadowSatellitePath = Path.Combine(analyzerShadowDir, satelliteDirectoryName!, satelliteFileName);
            ShadowCopyFile(satelliteFilePath, shadowSatellitePath);
            return shadowSatellitePath;
        }

        /// <summary>
        /// Get the shadow directory for the given original analyzer file path.
        /// </summary>
        private string GetAnalyzerShadowDirectory(string analyzerFilePath)
        {
            var originalDirName = Path.GetDirectoryName(analyzerFilePath)!;
            var shadowDirName = OriginalDirectoryMap.GetOrAdd(originalDirName, _ => Interlocked.Increment(ref _directoryCount)).ToString();
            return Path.Combine(ShadowDirectory, shadowDirName);
        }

        /// <summary>
        /// This type has to account for multiple threads calling into the various resolver APIs. To avoid two threads
        /// writing at the same time this method is used to ensure only one thread _wins_ and both can wait for 
        /// that thread to complete the copy.
        /// </summary>
        private void ShadowCopyFile(string originalFilePath, string shadowCopyPath)
        {
            if (CopyMap.TryGetValue(originalFilePath, out var copyTask))
            {
                copyTask.Wait();
                return;
            }

            var tcs = new TaskCompletionSource<string>();
            var task = CopyMap.GetOrAdd(originalFilePath, tcs.Task);
            if (object.ReferenceEquals(task, tcs.Task))
            {

Review comment (suggestion):
It seems like we shouldn't keep an entry for a cleanup which has finished. It seems like nothing is going to come around later and remove that entry. It will just grow for as many directories we end up running cleanup for in the process. While it's unlikely we will run so many cleanups that it becomes a problem, it just seems strange in principle.

If another caller comes around, after a cleanup has finished for a directory, and wants to try and clean the same directory, it seems fine to let them do that. It's possible I am missing something, though.

Code after:
                                        // Another process beat us to it. Nothing to do.
                                    }
                                }
                            }
                            catch (IOException)
                            {
                                // Retry after clearing the read-only flag
                                ClearReadOnlyFlagOnFiles(subDirectory);
                                Directory.Delete(subDirectory, recursive: true);
                            }
                        }
                    }
                    catch
                    {
                        // If something goes wrong we will leave it to the next run to clean up.
                        // Just swallow the exception and move on.
                    }
                    finally
                    {
                        mutex?.Dispose();
                    }
                }
            }
            finally
            {
                s_directoryCleanupStates[BaseDirectory] = DirectoryCleanupState.Completed;
            }
        }

        public bool IsAnalyzerPathHandled(string analyzerFilePath) => true;

        public string GetResolvedAnalyzerPath(string originalAnalyzerPath)
        {
            var analyzerShadowDir = GetAnalyzerShadowDirectory(originalAnalyzerPath);
            var analyzerShadowPath = Path.Combine(analyzerShadowDir, Path.GetFileName(originalAnalyzerPath));
            ShadowCopyFile(originalAnalyzerPath, analyzerShadowPath);
            return analyzerShadowPath;
        }

        public string? GetResolvedSatellitePath(string originalAnalyzerPath, CultureInfo cultureInfo)
        {
            var satelliteFilePath = AnalyzerAssemblyLoader.GetSatelliteAssemblyPath(originalAnalyzerPath, cultureInfo);
            if (satelliteFilePath is null)
            {
                return null;
            }

            var analyzerShadowDir = GetAnalyzerShadowDirectory(originalAnalyzerPath);
            var satelliteFileName = Path.GetFileName(satelliteFilePath);
            var satelliteDirectoryName = Path.GetFileName(Path.GetDirectoryName(satelliteFilePath));
            var shadowSatellitePath = Path.Combine(analyzerShadowDir, satelliteDirectoryName!, satelliteFileName);