// Licensed to the .NET Foundation under one or more agreements.
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Symbols;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis
{
    internal readonly struct BuildPaths
    {
        /// <summary>
        /// The path which contains the compiler binaries and response files.
        /// </summary>
        internal string ClientDirectory { get; }

        /// <summary>
        /// The path in which the compilation takes place. This is also referred to as "baseDirectory" in
        /// the code base.
        /// </summary>
        internal string WorkingDirectory { get; }

        /// <summary>
        /// The path which contains mscorlib.  This can be null when specified by the user or running in a
        /// CoreClr environment.
        /// </summary>
        internal string? SdkDirectory { get; }

        /// <summary>
        /// The temporary directory a compilation should use instead of Path.GetTempPath.  The latter
        /// relies on global state individual compilations should ignore.
        /// </summary>
        internal string? TempDirectory { get; }

        internal BuildPaths(string clientDir, string workingDir, string? sdkDir, string? tempDir)
        {
            ClientDirectory = clientDir;
            WorkingDirectory = workingDir;
            SdkDirectory = sdkDir;
            TempDirectory = tempDir;
        }
    }

    /// <summary>
    /// Base class for csc.exe, csi.exe, vbc.exe and vbi.exe implementations.
    /// </summary>
    internal abstract partial class CommonCompiler
    {
        internal const int Failed = 1;
        internal const int Succeeded = 0;

        /// <summary>
        /// Fallback encoding that is lazily retrieved if needed. If <see cref="EncodedStringText.CreateFallbackEncoding"/> is
        /// evaluated and stored, the value is used if a PDB is created for this compilation.
        /// </summary>
        private readonly Lazy<Encoding> _fallbackEncoding = new Lazy<Encoding>(EncodedStringText.CreateFallbackEncoding);

        public CommonMessageProvider MessageProvider { get; }
        public CommandLineArguments Arguments { get; }
        public IAnalyzerAssemblyLoader AssemblyLoader { get; private set; }
        public GeneratorDriverCache? GeneratorDriverCache { get; }
        public abstract DiagnosticFormatter DiagnosticFormatter { get; }

        /// <summary>
        /// The set of source file paths that are in the set of embedded paths.
        /// This is used to prevent reading source files that are embedded twice.
        /// </summary>
        public IReadOnlySet<string> EmbeddedSourcePaths { get; }

        /// <summary>
        /// The <see cref="ICommonCompilerFileSystem"/> used to access the file system inside this instance.
        /// </summary>
        internal ICommonCompilerFileSystem FileSystem { get; set; }

        private readonly HashSet<Diagnostic> _reportedDiagnostics = new HashSet<Diagnostic>();

        public abstract Compilation? CreateCompilation(
            TextWriter consoleOutput,
            TouchedFileLogger? touchedFilesLogger,
            ErrorLogger? errorLoggerOpt,
            ImmutableArray<AnalyzerConfigOptionsResult> analyzerConfigOptions,
            AnalyzerConfigOptionsResult globalConfigOptions);

        public abstract void PrintLogo(TextWriter consoleOutput);
        public abstract void PrintHelp(TextWriter consoleOutput);
        public abstract void PrintLangVersions(TextWriter consoleOutput);

        /// <summary>
        /// Print compiler version
        /// </summary>
        /// <param name="consoleOutput"></param>
        public virtual void PrintVersion(TextWriter consoleOutput)
        {
            consoleOutput.WriteLine(GetCompilerVersion());
        }

        protected abstract bool TryGetCompilerDiagnosticCode(string diagnosticId, out uint code);

        protected abstract void ResolveAnalyzersFromArguments(
            List<DiagnosticInfo> diagnostics,
            CommonMessageProvider messageProvider,
            CompilationOptions compilationOptions,
            bool skipAnalyzers,
            out ImmutableArray<DiagnosticAnalyzer> analyzers,
            out ImmutableArray<ISourceGenerator> generators);

        public CommonCompiler(CommandLineParser parser, string? responseFile, string[] args, BuildPaths buildPaths, string? additionalReferenceDirectories, IAnalyzerAssemblyLoader assemblyLoader, GeneratorDriverCache? driverCache, ICommonCompilerFileSystem? fileSystem)
        {
            IEnumerable<string> allArgs = args;

            Debug.Assert(null == responseFile || PathUtilities.IsAbsolute(responseFile));
            if (!SuppressDefaultResponseFile(args) && File.Exists(responseFile))
            {
                allArgs = new[] { "@" + responseFile }.Concat(allArgs);
            }

            this.Arguments = parser.Parse(allArgs, buildPaths.WorkingDirectory, buildPaths.SdkDirectory, additionalReferenceDirectories);
            this.MessageProvider = parser.MessageProvider;
            this.AssemblyLoader = assemblyLoader;
            this.GeneratorDriverCache = driverCache;
            this.EmbeddedSourcePaths = GetEmbeddedSourcePaths(Arguments);
            this.FileSystem = fileSystem ?? StandardFileSystem.Instance;
        }

        internal abstract bool SuppressDefaultResponseFile(IEnumerable<string> args);

        /// <summary>
        /// The type of the compiler class for version information in /help and /version.
        /// We don't simply use this.GetType() because that would break mock subclasses.
        /// </summary>
        internal abstract Type Type { get; }

        /// <summary>
        /// The version of this compiler with commit hash, used in logo and /version output.
        /// </summary>
        internal string GetCompilerVersion()
        {
            return GetProductVersion(Type);
        }

        internal static string GetProductVersion(Type type)
        {
            string? assemblyVersion = GetInformationalVersionWithoutHash(type);
            string? hash = type.Assembly.GetCustomAttribute<CommitHashAttribute>()?.Hash;
            return $"{assemblyVersion} ({hash})";
        }

        private static string? GetInformationalVersionWithoutHash(Type type)
        {
            // The attribute stores a SemVer2-formatted string: `A.B.C(-...)?(+...)?`
            // We remove the section after the + (if any is present)
            return type.Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split('+')[0];
        }

        internal static string GetAssemblyLocation(Type type)
        {
#if NET
            if (!RuntimeFeature.IsDynamicCodeSupported)
            {
                return "<unknown>";
            }
#endif

            var location = type.Assembly.Location;
            return string.IsNullOrEmpty(location) ? "<unknown>" : location;
        }

        /// <summary>
        /// Tool name used, along with assembly version, for error logging.
        /// </summary>
        internal abstract string GetToolName();

        /// <summary>
        /// Tool version identifier used for error logging.
        /// </summary>
        internal Version? GetAssemblyVersion()
        {
            return Type.GetTypeInfo().Assembly.GetName().Version;
        }

        internal string GetCultureName()
        {
            return Culture.Name;
        }

        internal virtual Func<string, MetadataReferenceProperties, PortableExecutableReference> GetMetadataProvider()
        {
            return (path, properties) =>
            {
                var peStream = FileSystem.OpenFileWithNormalizedException(path, FileMode.Open, FileAccess.Read, FileShare.Read);
                return MetadataReference.CreateFromFile(peStream, path, PEStreamOptions.PrefetchEntireImage, properties, documentation: null);
            };
        }

        internal virtual MetadataReferenceResolver GetCommandLineMetadataReferenceResolver(TouchedFileLogger? loggerOpt)
        {
            var pathResolver = new CompilerRelativePathResolver(FileSystem, Arguments.ReferencePaths, Arguments.BaseDirectory);
            return new LoggingMetadataFileReferenceResolver(pathResolver, GetMetadataProvider(), loggerOpt);
        }

        /// <summary>
        /// Resolves metadata references stored in command line arguments and reports errors for those that can't be resolved.
        /// </summary>
        internal List<MetadataReference> ResolveMetadataReferences(
            List<DiagnosticInfo> diagnostics,
            TouchedFileLogger? touchedFiles,
            out MetadataReferenceResolver referenceDirectiveResolver)
        {
            var commandLineReferenceResolver = GetCommandLineMetadataReferenceResolver(touchedFiles);

            List<MetadataReference> resolved = new List<MetadataReference>();
            Arguments.ResolveMetadataReferences(commandLineReferenceResolver, diagnostics, this.MessageProvider, resolved);

            if (Arguments.IsScriptRunner)
            {
                referenceDirectiveResolver = commandLineReferenceResolver;
            }
            else
            {
                // when compiling into an assembly (csc/vbc) we only allow #r that match references given on command line:
                referenceDirectiveResolver = new ExistingReferencesResolver(commandLineReferenceResolver, resolved.ToImmutableArray());
            }

            return resolved;
        }

        /// <summary>
        /// Reads content of a source file.
        /// </summary>
        /// <param name="file">Source file information.</param>
        /// <param name="diagnostics">Storage for diagnostics.</param>
        /// <returns>File content or null on failure.</returns>
        internal SourceText? TryReadFileContent(CommandLineSourceFile file, IList<DiagnosticInfo> diagnostics)
        {
            return TryReadFileContent(file, diagnostics, out _);
        }

        /// <summary>
        /// Reads content of a source file.
        /// </summary>
        /// <param name="file">Source file information.</param>
        /// <param name="diagnostics">Storage for diagnostics.</param>
        /// <param name="normalizedFilePath">If given <paramref name="file"/> opens successfully, set to normalized absolute path of the file, null otherwise.</param>
        /// <returns>File content or null on failure.</returns>
        internal SourceText? TryReadFileContent(CommandLineSourceFile file, IList<DiagnosticInfo> diagnostics, out string? normalizedFilePath)
        {
            var filePath = file.Path;
            try
            {
                if (file.IsInputRedirected)
                {
                    using var data = Console.OpenStandardInput();
                    normalizedFilePath = filePath;
                    return EncodedStringText.Create(data, _fallbackEncoding, Arguments.Encoding, Arguments.ChecksumAlgorithm, canBeEmbedded: EmbeddedSourcePaths.Contains(file.Path));
                }
                else
                {
                    using var data = OpenFileForReadWithSmallBufferOptimization(filePath, out normalizedFilePath);
                    return EncodedStringText.Create(data, _fallbackEncoding, Arguments.Encoding, Arguments.ChecksumAlgorithm, canBeEmbedded: EmbeddedSourcePaths.Contains(file.Path));
                }
            }
            catch (Exception e)
            {
                diagnostics.Add(ToFileReadDiagnostics(this.MessageProvider, e, filePath));
                normalizedFilePath = null;
                return null;
            }
        }

        /// <summary>
        /// Read all analyzer config files from the given paths.
        /// </summary>
        internal bool TryGetAnalyzerConfigSet(
            ImmutableArray<string> analyzerConfigPaths,
            DiagnosticBag diagnostics,
            [NotNullWhen(true)] out AnalyzerConfigSet? analyzerConfigSet)
        {
            var configs = ArrayBuilder<AnalyzerConfig>.GetInstance(analyzerConfigPaths.Length);

            var processedDirs = PooledHashSet<string>.GetInstance();

            foreach (var configPath in analyzerConfigPaths)
            {
                // The editorconfig spec requires all paths use '/' as the directory separator.
                // Since no known system allows directory separators as part of the file name,
                // we can replace every instance of the directory separator with a '/'
                string? fileContent = TryReadFileContent(configPath, diagnostics, out string? normalizedPath);
                if (fileContent is null)
                {
                    // Error reading a file. Bail out and report error.
                    break;
                }

                Debug.Assert(normalizedPath is object);
                var directory = Path.GetDirectoryName(normalizedPath) ?? normalizedPath;
                var editorConfig = AnalyzerConfig.Parse(fileContent, normalizedPath);

                if (!editorConfig.IsGlobal)
                {
                    if (processedDirs.Contains(directory))
                    {
                        diagnostics.Add(Diagnostic.Create(
                            MessageProvider,
                            MessageProvider.ERR_MultipleAnalyzerConfigsInSameDir,
                            directory));
                        break;
                    }
                    processedDirs.Add(directory);
                }
                configs.Add(editorConfig);
            }

            processedDirs.Free();

            if (diagnostics.HasAnyErrors())
            {
                configs.Free();
                analyzerConfigSet = null;
                return false;
            }

            analyzerConfigSet = AnalyzerConfigSet.Create(configs, out var setDiagnostics);
            configs.Free();
            diagnostics.AddRange(setDiagnostics);
            return true;
        }

        /// <summary>
        /// Returns the fallback encoding for parsing source files, if used, or null
        /// if not used
        /// </summary>
        internal Encoding? GetFallbackEncoding()
        {
            if (_fallbackEncoding.IsValueCreated)
            {
                return _fallbackEncoding.Value;
            }

            return null;
        }

        /// <summary>
        /// Read a UTF-8 encoded file and return the text as a string.
        /// </summary>
        private string? TryReadFileContent(string filePath, DiagnosticBag diagnostics, out string? normalizedPath)
        {
            try
            {
                var data = OpenFileForReadWithSmallBufferOptimization(filePath, out normalizedPath);
                using (var reader = new StreamReader(data, Encoding.UTF8))
                {
                    return reader.ReadToEnd();
                }
            }
            catch (Exception e)
            {
                diagnostics.Add(Diagnostic.Create(ToFileReadDiagnostics(MessageProvider, e, filePath)));
                normalizedPath = null;
                return null;
            }
        }

        private Stream OpenFileForReadWithSmallBufferOptimization(string filePath, out string normalizedFilePath)
            // PERF: Using a very small buffer size for the FileStream opens up an optimization within EncodedStringText/EmbeddedText where
            // we read the entire FileStream into a byte array in one shot. For files that are actually smaller than the buffer
            // size, FileStream.Read still allocates the internal buffer.
            => FileSystem.OpenFileEx(
                filePath,
                FileMode.Open,
                FileAccess.Read,
                FileShare.ReadWrite,
                bufferSize: 1,
                options: FileOptions.None,
                out normalizedFilePath);

        internal EmbeddedText? TryReadEmbeddedFileContent(string filePath, DiagnosticBag diagnostics)
        {
            try
            {
                using (var stream = OpenFileForReadWithSmallBufferOptimization(filePath, out _))
                {
                    const int LargeObjectHeapLimit = 80 * 1024;
                    if (stream.Length < LargeObjectHeapLimit)
                    {
                        ArraySegment<byte> bytes;
                        if (EncodedStringText.TryGetBytesFromStream(stream, out bytes))
                        {
                            return EmbeddedText.FromBytes(filePath, bytes, Arguments.ChecksumAlgorithm);
                        }
                    }

                    return EmbeddedText.FromStream(filePath, stream, Arguments.ChecksumAlgorithm);
                }
            }
            catch (Exception e)
            {
                diagnostics.Add(MessageProvider.CreateDiagnostic(ToFileReadDiagnostics(this.MessageProvider, e, filePath)));
                return null;
            }
        }

        private ImmutableArray<EmbeddedText?> AcquireEmbeddedTexts(Compilation compilation, DiagnosticBag diagnostics)
        {
            Debug.Assert(compilation.Options.SourceReferenceResolver is object);
            if (Arguments.EmbeddedFiles.IsEmpty)
            {
                return ImmutableArray<EmbeddedText?>.Empty;
            }

            var embeddedTreeMap = new Dictionary<string, SyntaxTree>(Arguments.EmbeddedFiles.Length);
            var embeddedFileOrderedSet = new OrderedSet<string>(Arguments.EmbeddedFiles.Select(e => e.Path));

            foreach (var tree in compilation.SyntaxTrees)
            {
                // Skip trees that will not have their text embedded.
                if (!EmbeddedSourcePaths.Contains(tree.FilePath))
                {
                    continue;
                }

                // Skip trees with duplicated paths. (VB allows this and "first tree wins" is same as PDB emit policy.)
                if (embeddedTreeMap.ContainsKey(tree.FilePath))
                {
                    continue;
                }

                // map embedded file path to corresponding source tree
                embeddedTreeMap.Add(tree.FilePath, tree);

                // also embed the text of any #line directive targets of embedded tree
                ResolveEmbeddedFilesFromExternalSourceDirectives(tree, compilation.Options.SourceReferenceResolver, embeddedFileOrderedSet, diagnostics);
            }

            var embeddedTextBuilder = ImmutableArray.CreateBuilder<EmbeddedText?>(embeddedFileOrderedSet.Count);
            foreach (var path in embeddedFileOrderedSet)
            {
                SyntaxTree? tree;
                EmbeddedText? text;

                if (embeddedTreeMap.TryGetValue(path, out tree))
                {
                    text = EmbeddedText.FromSource(path, tree.GetText());
                    Debug.Assert(text != null);
                }
                else
                {
                    text = TryReadEmbeddedFileContent(path, diagnostics);
                    Debug.Assert(text != null || diagnostics.HasAnyErrors());
                }

                // We can safely add nulls because result will be ignored if any error is produced.
                // This allows the MoveToImmutable to work below in all cases.
                embeddedTextBuilder.Add(text);
            }

            return embeddedTextBuilder.MoveToImmutable();
        }

        protected abstract void ResolveEmbeddedFilesFromExternalSourceDirectives(
            SyntaxTree tree,
            SourceReferenceResolver resolver,
            OrderedSet<string> embeddedFiles,
            DiagnosticBag diagnostics);

        private static IReadOnlySet<string> GetEmbeddedSourcePaths(CommandLineArguments arguments)
        {
            if (arguments.EmbeddedFiles.IsEmpty)
            {
                return SpecializedCollections.EmptyReadOnlySet<string>();
            }

            // Note that we require an exact match between source and embedded file paths (case-sensitive
            // and without normalization). If two files are the same but spelled differently, they will
            // be handled as separate files, meaning the embedding pass will read the content a second
            // time. This can also lead to more than one document entry in the PDB for the same document
            // if the PDB document de-duping policy in emit (normalize + case-sensitive in C#,
            // normalize + case-insensitive in VB) is not enough to converge them.
            var set = new HashSet<string>(arguments.EmbeddedFiles.Select(f => f.Path));
            set.IntersectWith(arguments.SourceFiles.Select(f => f.Path));
            return SpecializedCollections.StronglyTypedReadOnlySet(set);
        }

        internal static DiagnosticInfo ToFileReadDiagnostics(CommonMessageProvider messageProvider, Exception e, string filePath)
        {
            DiagnosticInfo diagnosticInfo;

            if (e is FileNotFoundException || e is DirectoryNotFoundException)
            {
                diagnosticInfo = new DiagnosticInfo(messageProvider, messageProvider.ERR_FileNotFound, filePath);
            }
            else if (e is InvalidDataException)
            {
                diagnosticInfo = new DiagnosticInfo(messageProvider, messageProvider.ERR_BinaryFile, filePath);
            }
            else
            {
                diagnosticInfo = new DiagnosticInfo(messageProvider, messageProvider.ERR_NoSourceFile, filePath, e.Message);
            }

            return diagnosticInfo;
        }

        /// <summary>Returns true if there were any errors, false otherwise.</summary>
        internal bool ReportDiagnostics(IEnumerable<Diagnostic> diagnostics, TextWriter consoleOutput, ErrorLogger? errorLoggerOpt, Compilation? compilation)
        {
            bool hasErrors = false;
            foreach (var diag in diagnostics)
            {
                reportDiagnostic(diag, compilation == null ? null : diag.GetSuppressionInfo(compilation));
            }

            return hasErrors;

            // Local functions
            void reportDiagnostic(Diagnostic diag, SuppressionInfo? suppressionInfo)
            {
                if (_reportedDiagnostics.Contains(diag))
                {
                    // TODO: This invariant fails (at least) in the case where we see a member declaration "x = 1;".
                    // First we attempt to parse a member declaration starting at "x".  When we see the "=", we
                    // create an IncompleteMemberSyntax with return type "x" and an error at the location of the "x".
                    // Then we parse a member declaration starting at "=".  This is an invalid member declaration start
                    // so we attach an error to the "=" and attach it (plus following tokens) to the IncompleteMemberSyntax
                    // we previously created.
                    //this assert isn't valid if we change the design to not bail out after each phase.
                    //System.Diagnostics.Debug.Assert(diag.Severity != DiagnosticSeverity.Error);
                    return;
                }
                else if (diag.Severity == DiagnosticSeverity.Hidden)
                {
                    // Not reported from the command-line compiler.
                    return;
                }

                // We want to report diagnostics with source suppression in the error log file.
                // However, these diagnostics should not be reported on the console output.
                errorLoggerOpt?.LogDiagnostic(diag, suppressionInfo);

                // If the diagnostic was suppressed by one or more DiagnosticSuppressor(s), then we report info diagnostics for each suppression
                // so that the suppression information is available in the binary logs and verbose build logs.
                if (diag.ProgrammaticSuppressionInfo != null)
                {
                    foreach (var suppression in diag.ProgrammaticSuppressionInfo.Suppressions)
                    {
                        var suppressionDiag = new SuppressionDiagnostic(diag, suppression.Descriptor.Id, suppression.Descriptor.Justification);
                        if (_reportedDiagnostics.Add(suppressionDiag))
                        {
                            PrintError(suppressionDiag, consoleOutput);
                        }
                    }

                    _reportedDiagnostics.Add(diag);
                    return;
                }

                if (diag.IsSuppressed)
                {
                    return;
                }

                // Diagnostics that aren't suppressed will be reported to the console output and, if they are errors,
                // they should fail the run
                if (diag.Severity == DiagnosticSeverity.Error)
                {
                    hasErrors = true;
                }

                PrintError(diag, consoleOutput);

                _reportedDiagnostics.Add(diag);
            }
        }

        /// <summary>Returns true if there were any errors, false otherwise.</summary>
        private bool ReportDiagnostics(DiagnosticBag diagnostics, TextWriter consoleOutput, ErrorLogger? errorLoggerOpt, Compilation? compilation)
            => ReportDiagnostics(diagnostics.ToReadOnly(), consoleOutput, errorLoggerOpt, compilation);

        /// <summary>Returns true if there were any errors, false otherwise.</summary>
        internal bool ReportDiagnostics(IEnumerable<DiagnosticInfo> diagnostics, TextWriter consoleOutput, ErrorLogger? errorLoggerOpt, Compilation? compilation)
            => ReportDiagnostics(diagnostics.Select(info => Diagnostic.Create(info)), consoleOutput, errorLoggerOpt, compilation);

        /// <summary>
        /// Reports all IVT information for the given compilation and references, to aid in troubleshooting otherwise inexplicable IVT failures.
        /// </summary>
        private void ReportIVTInfos(TextWriter consoleOutput, ErrorLogger? errorLogger, Compilation compilation, ImmutableArray<Diagnostic> diagnostics)
        {
            // Annotate any bad accesses with what assemblies they came from, if they are from a foreign assembly
            DiagnoseBadAccesses(consoleOutput, errorLogger, compilation, diagnostics);

            consoleOutput.WriteLine();

            // Printing 'InternalsVisibleToAttribute' information for the current compilation and all referenced assemblies.
            consoleOutput.WriteLine(CodeAnalysisResources.InternalsVisibleToHeaderSummary);

            var currentAssembly = compilation.Assembly;
            var currentAssemblyInternal = compilation.GetSymbolInternal<IAssemblySymbolInternal>(currentAssembly);

            // Current assembly: '{0}'
            consoleOutput.WriteLine(string.Format(CodeAnalysisResources.InternalsVisibleToCurrentAssembly, currentAssembly.Identity.GetDisplayName(fullKey: true)));

            consoleOutput.WriteLine();

            // Now, go through each of the referenced assemblies and print their IVT information.
            foreach (var assembly in currentAssembly.Modules.First().ReferencedAssemblySymbols.OrderBy(a => a.Name))
            {
                // Assembly reference: '{0}'
                //   Grants IVT to current assembly: {1}
                //   Grants IVTs to:

                var assemblyInternal = compilation.GetSymbolInternal<IAssemblySymbolInternal>(assembly);
                bool grantsIvt = currentAssemblyInternal.AreInternalsVisibleToThisAssembly(assemblyInternal);

                consoleOutput.WriteLine(string.Format(CodeAnalysisResources.InternalsVisibleToReferencedAssembly, assembly.Identity.GetDisplayName(fullKey: true), grantsIvt));

                var enumerable = assemblyInternal.GetInternalsVisibleToAssemblyNames();

                if (enumerable.Any())
                {
                    foreach (var simpleName in enumerable.OrderBy<string, string>(n => n))
                    {
                        //     Assembly name: '{0}'
                        //     Public Keys:
                        consoleOutput.WriteLine(string.Format(CodeAnalysisResources.InternalsVisibleToReferencedAssemblyDetails, simpleName));
                        foreach (var key in assemblyInternal.GetInternalsVisibleToPublicKeys(simpleName).Select(k => AssemblyIdentity.PublicKeyToString(k)).OrderBy(k => k))
                        {
                            consoleOutput.Write("      ");
                            consoleOutput.WriteLine(key);
                        }
                    }
                }
                else
                {
                    // Nothing
                    consoleOutput.Write("    ");
                    consoleOutput.WriteLine(CodeAnalysisResources.Nothing);
                }

                consoleOutput.WriteLine();
            }
        }

        private protected abstract void DiagnoseBadAccesses(TextWriter consoleOutput, ErrorLogger? errorLogger, Compilation compilation, ImmutableArray<Diagnostic> diagnostics);

        /// <summary>
        /// Returns true if there are any error diagnostics in the bag which cannot be suppressed and
        /// are guaranteed to break the build.
        /// Only diagnostics which have default severity error and are tagged as NotConfigurable fall in this bucket.
        /// This includes all compiler error diagnostics and specific analyzer error diagnostics that are marked as not configurable by the analyzer author.
        /// </summary>
        internal static bool HasUnsuppressableErrors(DiagnosticBag diagnostics)
        {
            foreach (var diag in diagnostics.AsEnumerable())
            {
                if (diag.IsUnsuppressableError())
                {
                    return true;
                }
            }
            return false;
        }

        internal static bool HasSuppressableWarningsOrErrors(DiagnosticBag diagnostics)
        {
            foreach (var diag in diagnostics.AsEnumerable())
            {
                if (!diag.IsUnsuppressableError())
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Returns true if the bag has any diagnostics with effective Severity=Error. Also returns true for warnings or informationals
        /// or warnings promoted to error via /warnaserror which are not suppressed.
        /// </summary>
        internal static bool HasUnsuppressedErrors(DiagnosticBag diagnostics)
        {
            foreach (Diagnostic diagnostic in diagnostics.AsEnumerable())
            {
                if (diagnostic.IsUnsuppressedError)
                {
                    return true;
                }
            }

            return false;
        }

        protected virtual void PrintError(Diagnostic diagnostic, TextWriter consoleOutput)
        {
            consoleOutput.WriteLine(DiagnosticFormatter.Format(diagnostic, Culture));
        }

        public SarifErrorLogger? GetErrorLogger(TextWriter consoleOutput)
        {
            Debug.Assert(Arguments.ErrorLogOptions?.Path != null);

            var diagnostics = DiagnosticBag.GetInstance();
            var errorLog = OpenFile(Arguments.ErrorLogOptions.Path,
                                    diagnostics,
                                    FileMode.Create,
                                    FileAccess.Write,
                                    FileShare.ReadWrite | FileShare.Delete);

            SarifErrorLogger? logger;
            if (errorLog == null)
            {
                Debug.Assert(diagnostics.HasAnyErrors());
                logger = null;
            }
            else
            {
                string toolName = GetToolName();
                string compilerVersion = GetCompilerVersion();
                Version assemblyVersion = GetAssemblyVersion() ?? new Version();

                if (Arguments.ErrorLogOptions.SarifVersion == SarifVersion.Sarif1)
                {
                    logger = new SarifV1ErrorLogger(errorLog, toolName, compilerVersion, assemblyVersion, Culture);
                }
                else
                {
                    logger = new SarifV2ErrorLogger(errorLog, toolName, compilerVersion, assemblyVersion, Culture);
                }
            }

            ReportDiagnostics(diagnostics.ToReadOnlyAndFree(), consoleOutput, errorLoggerOpt: logger, compilation: null);
            return logger;
        }

        /// <summary>
        /// csc.exe and vbc.exe entry point.
        /// </summary>
        public virtual int Run(TextWriter consoleOutput, CancellationToken cancellationToken = default)
        {
            var saveUICulture = CultureInfo.CurrentUICulture;
            SarifErrorLogger? errorLogger = null;

            try
            {
                // Messages from exceptions can be used as arguments for errors and they are often localized.
                // Ensure they are localized to the right language.
                var culture = this.Culture;
                if (culture != null)
                {
                    CultureInfo.CurrentUICulture = culture;
                }

                if (Arguments.ErrorLogOptions?.Path != null)
                {
                    errorLogger = GetErrorLogger(consoleOutput);
                    if (errorLogger == null)
                    {
                        return Failed;
                    }
                }

                return RunCore(consoleOutput, errorLogger, cancellationToken);
            }
            catch (OperationCanceledException)
            {
                var errorCode = MessageProvider.ERR_CompileCancelled;
                if (errorCode > 0)
                {
                    var diag = new DiagnosticInfo(MessageProvider, errorCode);
                    ReportDiagnostics(new[] { diag }, consoleOutput, errorLogger, compilation: null);
                }

                return Failed;
            }
            finally
            {
                CultureInfo.CurrentUICulture = saveUICulture;
                errorLogger?.Dispose();
            }
        }

        /// <summary>
        /// Perform source generation, if the compiler supports it.
        /// </summary>
        /// <param name="input">The compilation before any source generation has occurred.</param>
        /// <param name="generatedFilesBaseDirectory">The base directory for the <see cref="SyntaxTree.FilePath"/> of generated files.</param>
        /// <param name="parseOptions">The <see cref="ParseOptions"/> to use when parsing any generated sources.</param>
        /// <param name="generators">The generators to run</param>
        /// <param name="analyzerConfigOptionsProvider">A provider that returns analyzer config options.</param>
        /// <param name="additionalTexts">Any additional texts that should be passed to the generators when run.</param>
        /// <param name="generatorDiagnostics">Any diagnostics that were produced during generation.</param>
        /// <returns>A compilation that represents the original compilation with any additional, generated texts added to it.</returns>
        private protected (Compilation Compilation, GeneratorDriverTimingInfo DriverTimingInfo) RunGenerators(
            Compilation input,
            string generatedFilesBaseDirectory,
            ParseOptions parseOptions,
            ImmutableArray<ISourceGenerator> generators,
            AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider,
            ImmutableArray<AdditionalText> additionalTexts,
            DiagnosticBag generatorDiagnostics)
        {
            Debug.Assert(generatedFilesBaseDirectory is not null);

            GeneratorDriver? driver = null;
            string cacheKey = string.Empty;
            bool disableCache =
                !Arguments.ParseOptions.HasFeature(Feature.EnableGeneratorCache) ||
                string.IsNullOrWhiteSpace(Arguments.OutputFileName);
            if (this.GeneratorDriverCache is object && !disableCache)
            {
                cacheKey = deriveCacheKey();
                driver = this.GeneratorDriverCache.TryGetDriver(cacheKey)?
                                                  .WithUpdatedParseOptions(parseOptions)
                                                  .WithUpdatedAnalyzerConfigOptions(analyzerConfigOptionsProvider)
                                                  .ReplaceAdditionalTexts(additionalTexts);
            }

            driver ??= CreateGeneratorDriver(generatedFilesBaseDirectory, parseOptions, generators, analyzerConfigOptionsProvider, additionalTexts, Arguments.ChecksumAlgorithm);
            driver = driver.RunGeneratorsAndUpdateCompilation(input, out var compilationOut, out var diagnostics);
            generatorDiagnostics.AddRange(diagnostics);

            // We only cache the generator driver if it produced any generated files. While it's possible that it was expensive
            // to calculate that nothing needed to be generated, real world usage has found that generators are generally only
            // expensive when actually producing source. By only caching those with results, we help to keep memory usage down
            // when it probably wouldn't improve the performance anyway.
            if (!disableCache && driver.GetRunResult().GeneratedTrees.Any())
            {
                this.GeneratorDriverCache?.CacheGenerator(cacheKey, driver);
            }

            return (compilationOut, driver.GetTimingInfo());

            string deriveCacheKey()
            {
                Debug.Assert(!string.IsNullOrWhiteSpace(Arguments.OutputFileName));

                // CONSIDER: The only piece of the cache key that is required for correctness is the generators that were used.
                //           We set up the graph statically based on the generators, so as long as the generator inputs haven't
                //           changed we can technically run any project against another's cache and still get the correct results.
                //           Obviously that would remove the point of the cache, so we also key off of the output file name
                //           and output path so that collisions are unlikely and we'll usually get the correct cache for any
                //           given compilation.

                PooledStringBuilder sb = PooledStringBuilder.GetInstance();
                sb.Builder.Append(Arguments.GetOutputFilePath(Arguments.OutputFileName));
                foreach (var generator in generators)
                {
                    // append the generator FQN and the MVID of the assembly it came from, so any changes will invalidate the cache
                    var type = generator.GetGeneratorType();
                    sb.Builder.Append(type.AssemblyQualifiedName);
                    sb.Builder.Append(type.Assembly.ManifestModule.ModuleVersionId.ToString());
                }
                return sb.ToStringAndFree();
            }
        }

        private protected abstract GeneratorDriver CreateGeneratorDriver(string baseDirectory, ParseOptions parseOptions, ImmutableArray<ISourceGenerator> generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray<AdditionalText> additionalTexts, SourceHashAlgorithm checksumAlgorithm);

        private int RunCore(TextWriter consoleOutput, ErrorLogger? errorLogger, CancellationToken cancellationToken)
        {
            Debug.Assert(!Arguments.IsScriptRunner);

            cancellationToken.ThrowIfCancellationRequested();

            if (Arguments.DisplayVersion)
            {
                PrintVersion(consoleOutput);
                return Succeeded;
            }

            if (Arguments.DisplayLangVersions)
            {
                PrintLangVersions(consoleOutput);
                return Succeeded;
            }

            if (Arguments.DisplayLogo)
            {
                PrintLogo(consoleOutput);
            }

            if (Arguments.DisplayHelp)
            {
                PrintHelp(consoleOutput);
                return Succeeded;
            }

            if (ReportDiagnostics(Arguments.Errors, consoleOutput, errorLogger, compilation: null))
            {
                return Failed;
            }

            var diagnostics = DiagnosticBag.GetInstance();
            var result = compileAndReport();
            diagnostics.Free();
            return result;

            int compileAndReport()
            {
                var touchedFilesLogger = (Arguments.TouchedFilesPath != null) ? new TouchedFileLogger() : null;

                AnalyzerConfigSet? analyzerConfigSet = null;
                ImmutableArray<AnalyzerConfigOptionsResult> sourceFileAnalyzerConfigOptions = default;
                AnalyzerConfigOptionsResult globalConfigOptions = default;

                if (Arguments.AnalyzerConfigPaths.Length > 0)
                {
                    if (!TryGetAnalyzerConfigSet(Arguments.AnalyzerConfigPaths, diagnostics, out analyzerConfigSet))
                    {
                        var hadErrors = ReportDiagnostics(diagnostics, consoleOutput, errorLogger, compilation: null);
                        Debug.Assert(hadErrors);
                        return Failed;
                    }

                    globalConfigOptions = analyzerConfigSet.GlobalConfigOptions;
                    sourceFileAnalyzerConfigOptions = Arguments.SourceFiles.SelectAsArray(f => analyzerConfigSet.GetOptionsForSourcePath(f.Path));

                    foreach (var sourceFileAnalyzerConfigOption in sourceFileAnalyzerConfigOptions)
                    {
                        diagnostics.AddRange(sourceFileAnalyzerConfigOption.Diagnostics);
                    }
                }

                Compilation? compilation = CreateCompilation(consoleOutput, touchedFilesLogger, errorLogger, sourceFileAnalyzerConfigOptions, globalConfigOptions);
                if (compilation == null)
                {
                    return Failed;
                }

                var diagnosticInfos = new List<DiagnosticInfo>();
                ResolveAnalyzersFromArguments(diagnosticInfos, MessageProvider, compilation.Options, Arguments.SkipAnalyzers, out var analyzers, out var generators);
                var additionalTextFiles = ResolveAdditionalFilesFromArguments(diagnosticInfos, MessageProvider, touchedFilesLogger);
                if (ReportDiagnostics(diagnosticInfos, consoleOutput, errorLogger, compilation))
                {
                    return Failed;
                }

                ImmutableArray<EmbeddedText?> embeddedTexts = AcquireEmbeddedTexts(compilation, diagnostics);
                if (ReportDiagnostics(diagnostics, consoleOutput, errorLogger, compilation))
                {
                    return Failed;
                }

                var additionalTexts = ImmutableArray<AdditionalText>.CastUp(additionalTextFiles);

                var cachedExitCode = CheckCache(compilation, analyzers, generators, additionalTexts, cancellationToken, out var cacheState);
                if (cachedExitCode.HasValue)
                {
                    consoleOutput.WriteLine("Compilation result restored from cache.");
                    return cachedExitCode.Value;
                }

                CompileAndEmit(
                    touchedFilesLogger,
                    ref compilation,
                    analyzers,
                    generators,
                    additionalTexts,
                    analyzerConfigSet,
                    sourceFileAnalyzerConfigOptions,
                    embeddedTexts,
                    diagnostics,
                    errorLogger,
                    cancellationToken,
                    out CancellationTokenSource? analyzerCts,
                    out var analyzerDriver,
                    out var driverTimingInfo);

                // At this point analyzers are already complete in which case this is a no-op.  Or they are
                // still running because the compilation failed before all of the compilation events were
                // raised.  In the latter case the driver, and all its associated state, will be waiting around
                // for events that are never coming.  Cancel now and let the clean up process begin.
                if (analyzerCts != null)
                {
                    analyzerCts.Cancel();
                }

                var exitCode = ReportDiagnostics(diagnostics, consoleOutput, errorLogger, compilation)
                    ? Failed
                    : Succeeded;

                // The act of reporting errors can cause more errors to appear in
                // additional files due to forcing all additional files to fetch text
                foreach (var additionalFile in additionalTextFiles)
                {
                    if (ReportDiagnostics(additionalFile.Diagnostics, consoleOutput, errorLogger, compilation))
                    {
                        exitCode = Failed;
                    }
                }

                if (Arguments.ReportAnalyzer)
                {
                    ReportAnalyzerUtil.Report(consoleOutput, analyzerDriver, driverTimingInfo, Culture, compilation.Options.ConcurrentBuild);
                }

                if (Arguments.ReportInternalsVisibleToAttributes)
                {
                    ReportIVTInfos(consoleOutput, errorLogger, compilation, diagnostics.ToReadOnly());
                }

                if (exitCode == Succeeded)
                {
                    OnCompilationSucceeded(compilation, analyzers, generators, additionalTexts, cacheState, cancellationToken);
                }

                return exitCode;
            }
        }

        private static CompilerAnalyzerConfigOptionsProvider UpdateAnalyzerConfigOptionsProvider(
            CompilerAnalyzerConfigOptionsProvider existing,
            IEnumerable<SyntaxTree> syntaxTrees,
            ImmutableArray<AnalyzerConfigOptionsResult> sourceFileAnalyzerConfigOptions,
            ImmutableArray<AdditionalText> additionalFiles = default,
            ImmutableArray<AnalyzerConfigOptionsResult> additionalFileOptions = default)
        {
            var builder = ImmutableDictionary.CreateBuilder<object, AnalyzerConfigOptions>();
            int i = 0;
            foreach (var syntaxTree in syntaxTrees)
            {

                var options = sourceFileAnalyzerConfigOptions[i].AnalyzerOptions;

                // Optimization: don't create a bunch of entries pointing to a no-op
                if (options.Count > 0)
                {
                    Debug.Assert(existing.GetOptions(syntaxTree) == DictionaryAnalyzerConfigOptions.Empty);
                    builder.Add(syntaxTree, new DictionaryAnalyzerConfigOptions(options));
                }
                i++;
            }

            if (!additionalFiles.IsDefault)
            {
                for (i = 0; i < additionalFiles.Length; i++)
                {
                    var options = additionalFileOptions[i].AnalyzerOptions;

                    // Optimization: don't create a bunch of entries pointing to a no-op
                    if (options.Count > 0)
                    {
                        Debug.Assert(existing.GetOptions(additionalFiles[i]) == DictionaryAnalyzerConfigOptions.Empty);
                        builder.Add(additionalFiles[i], new DictionaryAnalyzerConfigOptions(options));
                    }
                }
            }

            return existing.WithAdditionalTreeOptions(builder.ToImmutable());
        }

        private CompilerAnalyzerConfigOptionsProvider GetCompilerAnalyzerConfigOptionsProvider(
            AnalyzerConfigSet? analyzerConfigSet,
            ImmutableArray<AdditionalText> additionalTextFiles,
            DiagnosticBag diagnostics,
            Compilation compilation,
            ImmutableArray<AnalyzerConfigOptionsResult> sourceFileAnalyzerConfigOptions)
        {
            var analyzerConfigProvider = CompilerAnalyzerConfigOptionsProvider.Empty;
            if (Arguments.AnalyzerConfigPaths.Length > 0)
            {
                Debug.Assert(analyzerConfigSet is object);
                analyzerConfigProvider = analyzerConfigProvider.WithGlobalOptions(new DictionaryAnalyzerConfigOptions(analyzerConfigSet.GetOptionsForSourcePath(string.Empty).AnalyzerOptions));

                // https://github.com/dotnet/roslyn/issues/31916: The compiler currently doesn't support
                // configuring diagnostic reporting on additional text files individually.
                ImmutableArray<AnalyzerConfigOptionsResult> additionalFileAnalyzerOptions =
                    additionalTextFiles.SelectAsArray(f => analyzerConfigSet.GetOptionsForSourcePath(f.Path));

                foreach (var result in additionalFileAnalyzerOptions)
                {
                    diagnostics.AddRange(result.Diagnostics);
                }

                analyzerConfigProvider = UpdateAnalyzerConfigOptionsProvider(
                    analyzerConfigProvider,
                    compilation.SyntaxTrees,
                    sourceFileAnalyzerConfigOptions,
                    additionalTextFiles,
                    additionalFileAnalyzerOptions);
            }

            return analyzerConfigProvider;
        }
        /// <summary>
        /// Perform all the work associated with actual compilation
        /// (parsing, binding, compile, emit), resulting in diagnostics
        /// and analyzer output.
        /// </summary>
        private void CompileAndEmit(
            TouchedFileLogger? touchedFilesLogger,
            ref Compilation compilation,
            ImmutableArray<DiagnosticAnalyzer> analyzers,
            ImmutableArray<ISourceGenerator> generators,
            ImmutableArray<AdditionalText> additionalTextFiles,
            AnalyzerConfigSet? analyzerConfigSet,
            ImmutableArray<AnalyzerConfigOptionsResult> sourceFileAnalyzerConfigOptions,
            ImmutableArray<EmbeddedText?> embeddedTexts,
            DiagnosticBag diagnostics,
            ErrorLogger? errorLogger,
            CancellationToken cancellationToken,
            out CancellationTokenSource? analyzerCts,
            out AnalyzerDriver? analyzerDriver,
            out GeneratorDriverTimingInfo? generatorTimingInfo)
        {
            analyzerCts = null;
            analyzerDriver = null;
            generatorTimingInfo = null;

            // Print the diagnostics produced during the parsing stage and exit if there are any unsuppressible errors.
            compilation.GetDiagnostics(CompilationStage.Parse, includeEarlierStages: false, diagnostics, cancellationToken);

            DiagnosticBag? analyzerExceptionDiagnostics = null;

            // If there are parsing errors, we want to return immediately.
            // But first, we need to check two things: 
            // 1. Whether there are any suppressible warnings,
            // 2. Whether there are any diagnostic suppressors that could potentially suppress them.
            // If both conditions are true, run diagnostic suppressors before exiting from this method.
            if (HasUnsuppressableErrors(diagnostics))
            {
                if (HasSuppressableWarningsOrErrors(diagnostics) && analyzers.Any(a => a is DiagnosticSuppressor))
                {
                    var analyzerConfigProvider = GetCompilerAnalyzerConfigOptionsProvider(analyzerConfigSet, additionalTextFiles, diagnostics, compilation, sourceFileAnalyzerConfigOptions);

                    AnalyzerOptions analyzerOptions = CreateAnalyzerOptions(additionalTextFiles, analyzerConfigProvider);

                    (analyzerCts, analyzerExceptionDiagnostics, analyzerDriver) = initializeAnalyzerDriver(analyzerOptions, ref compilation);

                    analyzerDriver.ApplyProgrammaticSuppressions(diagnostics, compilation, analyzerCts.Token);
                }
                return;
            }

            if (!analyzers.IsEmpty || !generators.IsEmpty)
            {
                var analyzerConfigProvider =
                    GetCompilerAnalyzerConfigOptionsProvider(analyzerConfigSet, additionalTextFiles, diagnostics, compilation, sourceFileAnalyzerConfigOptions);

                if (!generators.IsEmpty)
                {
                    // At this point we have a compilation with nothing yet computed.
                    // We pass it to the generators, which will realize any symbols they require.
                    var explicitGeneratedOutDir = Arguments.GeneratedFilesOutputDirectory;
                    var hasExplicitGeneratedOutDir = !string.IsNullOrWhiteSpace(explicitGeneratedOutDir);
                    var baseDirectory = hasExplicitGeneratedOutDir ? explicitGeneratedOutDir! : Arguments.OutputDirectory;
                    (compilation, generatorTimingInfo) = RunGenerators(compilation, baseDirectory, Arguments.ParseOptions, generators, analyzerConfigProvider, additionalTextFiles, diagnostics);

                    bool hasAnalyzerConfigs = !Arguments.AnalyzerConfigPaths.IsEmpty;
                    var generatedSyntaxTrees = compilation.SyntaxTrees.Skip(Arguments.SourceFiles.Length).ToList();
                    var analyzerOptionsBuilder = hasAnalyzerConfigs ? ArrayBuilder<AnalyzerConfigOptionsResult>.GetInstance(generatedSyntaxTrees.Count) : null;
                    var embeddedTextBuilder = ArrayBuilder<EmbeddedText>.GetInstance(generatedSyntaxTrees.Count);
                    try
                    {
                        foreach (var tree in generatedSyntaxTrees)
                        {
                            Debug.Assert(!string.IsNullOrWhiteSpace(tree.FilePath));
                            cancellationToken.ThrowIfCancellationRequested();

                            var sourceText = tree.GetText(cancellationToken);

                            // embed the generated text and get analyzer options for it if needed
                            embeddedTextBuilder.Add(EmbeddedText.FromSource(tree.FilePath, sourceText));
                            if (analyzerOptionsBuilder is object)
                            {
                                analyzerOptionsBuilder.Add(analyzerConfigSet!.GetOptionsForSourcePath(tree.FilePath));
                            }

                            // write out the file if an output path was explicitly provided
                            if (hasExplicitGeneratedOutDir)
                            {
                                var path = tree.FilePath;
                                Debug.Assert(path.StartsWith(explicitGeneratedOutDir!));
                                if (Directory.Exists(explicitGeneratedOutDir))
                                {
                                    Directory.CreateDirectory(Path.GetDirectoryName(path)!);
                                }

                                var fileStream = OpenFile(path, diagnostics, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
                                if (fileStream is object)
                                {
                                    Debug.Assert(tree.Encoding is object);

                                    using var disposer = new NoThrowStreamDisposer(fileStream, path, diagnostics, MessageProvider);
                                    using var writer = new StreamWriter(fileStream, tree.Encoding);

                                    sourceText.Write(writer, cancellationToken);
                                    touchedFilesLogger?.AddWritten(path);
                                }
                            }
                        }

                        embeddedTexts = embeddedTexts.AddRange(embeddedTextBuilder);
                        if (analyzerOptionsBuilder is object)
                        {
                            analyzerConfigProvider = UpdateAnalyzerConfigOptionsProvider(
                               analyzerConfigProvider,
                               generatedSyntaxTrees,
                               analyzerOptionsBuilder.ToImmutable());
                        }
                    }
                    finally
                    {
                        analyzerOptionsBuilder?.Free();
                        embeddedTextBuilder.Free();
                    }
                }

                AnalyzerOptions analyzerOptions = CreateAnalyzerOptions(
                      additionalTextFiles, analyzerConfigProvider);

                if (!analyzers.IsEmpty)
                {
                    (analyzerCts, analyzerExceptionDiagnostics, analyzerDriver) = initializeAnalyzerDriver(analyzerOptions, ref compilation);
                }
            }

            compilation.GetDiagnostics(CompilationStage.Declare, includeEarlierStages: false, diagnostics, cancellationToken);

            // If there are unsuppressable declaration errors, we want to exit early from this method.
            // But before we do so, we need to run diagnostic suppressors (if any) on all suppressable warnings/errors (if any).
            if (HasUnsuppressableErrors(diagnostics))
            {
                if (analyzerDriver == null || !analyzerDriver.HasDiagnosticSuppressors || !HasSuppressableWarningsOrErrors(diagnostics))
                {
                    return;
                }

                analyzerDriver.ApplyProgrammaticSuppressions(diagnostics, compilation, cancellationToken);
                return;
            }

            cancellationToken.ThrowIfCancellationRequested();

            // Given a compilation and a destination directory, determine three names:
            //   1) The name with which the assembly should be output.
            //   2) The path of the assembly/module file (default = destination directory + compilation output name).
            //   3) The path of the pdb file (default = assembly/module path with ".pdb" extension).
            string outputName = GetOutputFileName(compilation, cancellationToken)!;
            var finalPeFilePath = Arguments.GetOutputFilePath(outputName);
            var finalPdbFilePath = Arguments.GetPdbFilePath(outputName);
            var finalXmlFilePath = Arguments.DocumentationPath;

            NoThrowStreamDisposer? sourceLinkStreamDisposerOpt = null;

            try
            {
                // NOTE: Unlike the PDB path, the XML doc path is not embedded in the assembly, so we don't need to pass it to emit.
                var emitOptions = Arguments.EmitOptions.
                    WithOutputNameOverride(outputName).
                    WithPdbFilePath(PathUtilities.NormalizePathPrefix(finalPdbFilePath, Arguments.PathMap));

                // TODO(https://github.com/dotnet/roslyn/issues/19592):
                // This feature flag is being maintained until our next major release to avoid unnecessary
                // compat breaks with customers.
                if (Arguments.ParseOptions.HasFeature(Feature.PdbPathDeterminism) && !string.IsNullOrEmpty(emitOptions.PdbFilePath))
                {
                    emitOptions = emitOptions.WithPdbFilePath(Path.GetFileName(emitOptions.PdbFilePath));
                }

                if (Arguments.SourceLink != null)
                {
                    var sourceLinkStreamOpt = OpenFile(
                        Arguments.SourceLink,
                        diagnostics,
                        FileMode.Open,
                        FileAccess.Read,
                        FileShare.Read);

                    if (sourceLinkStreamOpt != null)
                    {
                        sourceLinkStreamDisposerOpt = new NoThrowStreamDisposer(
                            sourceLinkStreamOpt,
                            Arguments.SourceLink,
                            diagnostics,
                            MessageProvider);
                    }
                }

                if (Arguments.ParseOptions.HasFeature(Feature.DebugDeterminism))
                {
                    EmitDeterminismKey(
                        compilation,
                        FileSystem,
                        additionalTextFiles,
                        analyzers,
                        generators,
                        Arguments.PathMap,
                        emitOptions,
                        sourceLinkStreamDisposerOpt?.Stream,
                        Arguments.ManifestResources);
                }

                // Need to ensure the PDB file path validation is done on the original path as that is the
                // file we will write out to disk, there is no guarantee that the file paths emitted into
                // the PE / PDB are valid file paths because pathmap can be used to create deliberately
                // illegal names
                if (!PathUtilities.IsValidFilePath(finalPdbFilePath))
                {
                    diagnostics.Add(MessageProvider.CreateDiagnostic(MessageProvider.FTL_InvalidInputFileName, Location.None, finalPdbFilePath));
                }

                var moduleBeingBuilt = compilation.CheckOptionsAndCreateModuleBuilder(
                    diagnostics,
                    Arguments.ManifestResources,
                    emitOptions,
                    debugEntryPoint: null,
                    sourceLinkStream: sourceLinkStreamDisposerOpt?.Stream,
                    embeddedTexts: embeddedTexts,
                    testData: null,
                    cancellationToken: cancellationToken);

                if (moduleBeingBuilt != null)
                {
                    bool success;

                    try
                    {
                        success = compilation.CompileMethods(
                            moduleBeingBuilt,
                            Arguments.EmitPdb,
                            diagnostics,
                            filterOpt: null,
                            cancellationToken: cancellationToken);

                        // Prior to generating the xml documentation file,
                        // we apply programmatic suppressions for compiler warnings from diagnostic suppressors.
                        // If there are still any unsuppressed errors or warnings escalated to errors
                        // then we bail out from generating the documentation file.
                        // This maintains the compiler invariant that xml documentation file should not be
                        // generated in presence of diagnostics that break the build.
                        if (analyzerDriver != null && !diagnostics.IsEmptyWithoutResolution)
                        {
                            analyzerDriver.ApplyProgrammaticSuppressions(diagnostics, compilation, cancellationToken);
                        }

                        if (HasUnsuppressedErrors(diagnostics))
                        {
                            success = false;
                        }

                        if (success)
                        {
                            // NOTE: as native compiler does, we generate the documentation file
                            // NOTE: 'in place', replacing the contents of the file if it exists
                            NoThrowStreamDisposer? xmlStreamDisposerOpt = null;

                            if (finalXmlFilePath != null)
                            {
                                var xmlStreamOpt = OpenFile(finalXmlFilePath,
                                                            diagnostics,
                                                            FileMode.OpenOrCreate,
                                                            FileAccess.Write,
                                                            FileShare.ReadWrite | FileShare.Delete);

                                if (xmlStreamOpt == null)
                                {
                                    return;
                                }

                                try
                                {
                                    xmlStreamOpt.SetLength(0);
                                }
                                catch (Exception e)
                                {
                                    MessageProvider.ReportStreamWriteException(e, finalXmlFilePath, diagnostics);
                                    return;
                                }
                                xmlStreamDisposerOpt = new NoThrowStreamDisposer(
                                    xmlStreamOpt,
                                    finalXmlFilePath,
                                    diagnostics,
                                    MessageProvider);
                            }

                            using (xmlStreamDisposerOpt)
                            {
                                using (var win32ResourceStreamOpt = GetWin32Resources(FileSystem, MessageProvider, Arguments, compilation, diagnostics))
                                {
                                    if (HasUnsuppressableErrors(diagnostics))
                                    {
                                        return;
                                    }

                                    success =
                                        compilation.GenerateResources(moduleBeingBuilt, win32ResourceStreamOpt, useRawWin32Resources: false, diagnostics, cancellationToken) &&
                                        compilation.GenerateDocumentationComments(xmlStreamDisposerOpt?.Stream, emitOptions.OutputNameOverride, diagnostics, cancellationToken);
                                }
                            }

                            if (xmlStreamDisposerOpt?.HasFailedToDispose == true)
                            {
                                return;
                            }

                            // only report unused usings if we have success.
                            if (success)
                            {
                                compilation.ReportUnusedImports(diagnostics, cancellationToken);
                            }
                        }

                        compilation.CompleteTrees(null);

                        if (analyzerDriver != null)
                        {
                            // GetDiagnosticsAsync is called after ReportUnusedImports
                            // since that method calls EventQueue.TryComplete. Without
                            // TryComplete, we may miss diagnostics.
                            var hostDiagnostics = analyzerDriver.GetDiagnosticsAsync(compilation, cancellationToken).Result;
                            diagnostics.AddRange(hostDiagnostics);

                            if (!diagnostics.IsEmptyWithoutResolution)
                            {
                                // Apply diagnostic suppressions for analyzer and/or compiler diagnostics from diagnostic suppressors.
                                analyzerDriver.ApplyProgrammaticSuppressions(diagnostics, compilation, cancellationToken);
                            }

                            if (errorLogger != null)
                            {
                                var descriptorsWithInfo = analyzerDriver.GetAllDiagnosticDescriptorsWithInfo(cancellationToken, out var totalAnalyzerExecutionTime);
                                AddAnalyzerDescriptorsAndExecutionTime(errorLogger, descriptorsWithInfo, totalAnalyzerExecutionTime);
                            }
                        }
                    }
                    finally
                    {
                        moduleBeingBuilt.CompilationFinished();
                    }

                    if (HasUnsuppressedErrors(diagnostics))
                    {
                        success = false;
                    }

                    if (success)
                    {
                        var peStreamProvider = new CompilerEmitStreamProvider(this, finalPeFilePath);
                        var pdbStreamProviderOpt = Arguments.EmitPdbFile ? new CompilerEmitStreamProvider(this, finalPdbFilePath) : null;

                        string? finalRefPeFilePath = Arguments.OutputRefFilePath;
                        var refPeStreamProviderOpt = finalRefPeFilePath != null ? new CompilerEmitStreamProvider(this, finalRefPeFilePath) : null;

                        RSAParameters? privateKeyOpt = null;
                        if (compilation.Options.StrongNameProvider != null && compilation.SignUsingBuilder && !compilation.Options.PublicSign)
                        {
                            privateKeyOpt = compilation.StrongNameKeys.PrivateKey;
                        }

                        // If we serialize to a PE stream we need to record the fallback encoding if it was used
                        // so the compilation can be recreated.
                        emitOptions = emitOptions.WithFallbackSourceFileEncoding(GetFallbackEncoding());

                        success = compilation.SerializeToPeStream(
                            moduleBeingBuilt,
                            peStreamProvider,
                            refPeStreamProviderOpt,
                            pdbStreamProviderOpt,
                            rebuildData: null,
                            testSymWriterFactory: null,
                            diagnostics: diagnostics,
                            emitOptions: emitOptions,
                            privateKeyOpt: privateKeyOpt,
                            cancellationToken: cancellationToken);

                        peStreamProvider.Close(diagnostics);
                        refPeStreamProviderOpt?.Close(diagnostics);
                        pdbStreamProviderOpt?.Close(diagnostics);

                        if (success && touchedFilesLogger != null)
                        {
                            if (pdbStreamProviderOpt != null)
                            {
                                touchedFilesLogger.AddWritten(finalPdbFilePath);
                            }
                            if (refPeStreamProviderOpt != null)
                            {
                                touchedFilesLogger.AddWritten(finalRefPeFilePath!);
                            }
                            touchedFilesLogger.AddWritten(finalPeFilePath);
                        }
                    }
                }

                if (HasUnsuppressableErrors(diagnostics))
                {
                    return;
                }
            }
            finally
            {
                sourceLinkStreamDisposerOpt?.Dispose();
            }

            if (sourceLinkStreamDisposerOpt?.HasFailedToDispose == true)
            {
                return;
            }

            cancellationToken.ThrowIfCancellationRequested();

            if (analyzerExceptionDiagnostics != null)
            {
                diagnostics.AddRange(analyzerExceptionDiagnostics);
                if (HasUnsuppressableErrors(analyzerExceptionDiagnostics))
                {
                    return;
                }
            }

            cancellationToken.ThrowIfCancellationRequested();

            if (!WriteTouchedFiles(diagnostics, touchedFilesLogger, finalXmlFilePath))
            {
                return;
            }

            (CancellationTokenSource, DiagnosticBag, AnalyzerDriver) initializeAnalyzerDriver(AnalyzerOptions analyzerOptions, ref Compilation compilation)
            {
                var analyzerCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
                var analyzerExceptionDiagnostics = new DiagnosticBag();

                // PERF: Avoid executing analyzers that report only Hidden and/or Info diagnostics, which don't appear in the build output.
                //  1. Always filter out 'Hidden' analyzer diagnostics in build.
                //  2. Filter out 'Info' analyzer diagnostics if they are not required to be logged in errorlog.
                var severityFilter = SeverityFilter.Hidden;
                if (Arguments.ErrorLogPath == null)
                    severityFilter |= SeverityFilter.Info;

                var analyzerDriver = AnalyzerDriver.CreateAndAttachToCompilation(
                    compilation,
                    analyzers,
                    analyzerOptions,
                    new AnalyzerManager(analyzers),
                    analyzerExceptionDiagnostics.Add,
                    reportAnalyzer: Arguments.ReportAnalyzer || errorLogger != null,
                    severityFilter,
                    trackSuppressedDiagnosticIds: errorLogger != null,
                    out compilation,
                    analyzerCts.Token);

                return (analyzerCts, analyzerExceptionDiagnostics, analyzerDriver);
            }
        }

        // virtual for testing
        protected virtual Diagnostics.AnalyzerOptions CreateAnalyzerOptions(
            ImmutableArray<AdditionalText> additionalTextFiles,
            AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider)
            => new Diagnostics.AnalyzerOptions(additionalTextFiles, analyzerConfigOptionsProvider);
        protected virtual void AddAnalyzerDescriptorsAndExecutionTime(ErrorLogger errorLogger, ImmutableArray<(DiagnosticDescriptor Descriptor, DiagnosticDescriptorErrorLoggerInfo Info)> descriptorsWithInfo, double totalAnalyzerExecutionTime)
            => errorLogger.AddAnalyzerDescriptorsAndExecutionTime(descriptorsWithInfo, totalAnalyzerExecutionTime);

        private bool WriteTouchedFiles(DiagnosticBag diagnostics, TouchedFileLogger? touchedFilesLogger, string? finalXmlFilePath)
        {
            if (Arguments.TouchedFilesPath != null)
            {
                Debug.Assert(touchedFilesLogger != null);

                if (finalXmlFilePath != null)
                {
                    touchedFilesLogger.AddWritten(finalXmlFilePath);
                }

                string readFilesPath = Arguments.TouchedFilesPath + ".read";
                string writtenFilesPath = Arguments.TouchedFilesPath + ".write";

                var readStream = OpenFile(readFilesPath, diagnostics, mode: FileMode.OpenOrCreate);
                var writtenStream = OpenFile(writtenFilesPath, diagnostics, mode: FileMode.OpenOrCreate);

                if (readStream == null || writtenStream == null)
                {
                    return false;
                }

                string? filePath = null;
                try
                {
                    filePath = readFilesPath;
                    using (var writer = new StreamWriter(readStream))
                    {
                        touchedFilesLogger.WriteReadPaths(writer);
                    }

                    filePath = writtenFilesPath;
                    using (var writer = new StreamWriter(writtenStream))
                    {
                        touchedFilesLogger.WriteWrittenPaths(writer);
                    }
                }
                catch (Exception e)
                {
                    Debug.Assert(filePath != null);
                    MessageProvider.ReportStreamWriteException(e, filePath, diagnostics);
                    return false;
                }
            }

            return true;
        }

        protected virtual ImmutableArray<AdditionalTextFile> ResolveAdditionalFilesFromArguments(List<DiagnosticInfo> diagnostics, CommonMessageProvider messageProvider, TouchedFileLogger? touchedFilesLogger)
        {
            var builder = ArrayBuilder<AdditionalTextFile>.GetInstance();
            var filePaths = new HashSet<string>(PathUtilities.Comparer);

            foreach (var file in Arguments.AdditionalFiles)
            {
                Debug.Assert(PathUtilities.IsAbsolute(file.Path));
                if (filePaths.Add(PathUtilities.ExpandAbsolutePathWithRelativeParts(file.Path)))
                {
                    builder.Add(new AdditionalTextFile(file, this));
                }
            }

            return builder.ToImmutableAndFree();
        }

        /// <summary>
        /// Returns the name with which the assembly should be output
        /// </summary>
        protected abstract string GetOutputFileName(Compilation compilation, CancellationToken cancellationToken);

        private Stream? OpenFile(
            string filePath,
            DiagnosticBag diagnostics,
            FileMode mode = FileMode.Open,
            FileAccess access = FileAccess.ReadWrite,
            FileShare share = FileShare.None)
        {
            try
            {
                return FileSystem.OpenFile(filePath, mode, access, share);
            }
            catch (Exception e)
            {
                MessageProvider.ReportStreamWriteException(e, filePath, diagnostics);
                return null;
            }
        }

        // internal for testing
        internal static Stream? GetWin32ResourcesInternal(
            ICommonCompilerFileSystem fileSystem,
            CommonMessageProvider messageProvider,
            CommandLineArguments arguments,
            Compilation compilation,
            out IEnumerable<DiagnosticInfo> errors)
        {
            var diagnostics = DiagnosticBag.GetInstance();
            var stream = GetWin32Resources(fileSystem, messageProvider, arguments, compilation, diagnostics);
            errors = diagnostics.ToReadOnlyAndFree().SelectAsArray(diag => new DiagnosticInfo(messageProvider, diag.IsWarningAsError, diag.Code, (object[])diag.Arguments));
            return stream;
        }

        private static Stream? GetWin32Resources(
            ICommonCompilerFileSystem fileSystem,
            CommonMessageProvider messageProvider,
            CommandLineArguments arguments,
            Compilation compilation,
            DiagnosticBag diagnostics)
        {
            if (arguments.Win32ResourceFile != null)
            {
                return OpenStream(fileSystem, messageProvider, arguments.Win32ResourceFile, arguments.BaseDirectory, messageProvider.ERR_CantOpenWin32Resource, diagnostics);
            }

            using (Stream? manifestStream = OpenManifestStream(fileSystem, messageProvider, compilation.Options.OutputKind, arguments, diagnostics))
            {
                using (Stream? iconStream = OpenStream(fileSystem, messageProvider, arguments.Win32Icon, arguments.BaseDirectory, messageProvider.ERR_CantOpenWin32Icon, diagnostics))
                {
                    try
                    {
                        return compilation.CreateDefaultWin32Resources(true, arguments.NoWin32Manifest, manifestStream, iconStream);
                    }
                    catch (Exception ex)
                    {
                        diagnostics.Add(messageProvider.CreateDiagnostic(messageProvider.ERR_ErrorBuildingWin32Resource, Location.None, ex.Message));
                    }
                }
            }

            return null;
        }

        private static Stream? OpenManifestStream(ICommonCompilerFileSystem fileSystem, CommonMessageProvider messageProvider, OutputKind outputKind, CommandLineArguments arguments, DiagnosticBag diagnostics)
        {
            return outputKind.IsNetModule()
                ? null
                : OpenStream(fileSystem, messageProvider, arguments.Win32Manifest, arguments.BaseDirectory, messageProvider.ERR_CantOpenWin32Manifest, diagnostics);
        }

        private static Stream? OpenStream(ICommonCompilerFileSystem fileSystem, CommonMessageProvider messageProvider, string? path, string? baseDirectory, int errorCode, DiagnosticBag diagnostics)
        {
            if (path == null)
            {
                return null;
            }

            string? fullPath = ResolveRelativePath(messageProvider, path, baseDirectory, diagnostics);
            if (fullPath == null)
            {
                return null;
            }

            try
            {
                return fileSystem.OpenFile(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            }
            catch (Exception ex)
            {
                diagnostics.Add(messageProvider.CreateDiagnostic(errorCode, Location.None, fullPath, ex.Message));
            }

            return null;
        }

        private static string? ResolveRelativePath(CommonMessageProvider messageProvider, string path, string? baseDirectory, DiagnosticBag diagnostics)
        {
            string? fullPath = FileUtilities.ResolveRelativePath(path, baseDirectory);
            if (fullPath == null)
            {
                diagnostics.Add(messageProvider.CreateDiagnostic(messageProvider.FTL_InvalidInputFileName, Location.None, path ?? ""));
            }

            return fullPath;
        }

        internal static bool TryGetCompilerDiagnosticCode(string diagnosticId, string expectedPrefix, out uint code)
        {
            code = 0;
            return diagnosticId.StartsWith(expectedPrefix, StringComparison.Ordinal) && uint.TryParse(diagnosticId.Substring(expectedPrefix.Length), out code);
        }

        /// <summary>
        ///   When overridden by a derived class, this property can override the current thread's
        ///   CurrentUICulture property for diagnostic message resource lookups.
        /// </summary>
        protected virtual CultureInfo Culture
        {
            get
            {
                return Arguments.PreferredUILang ?? CultureInfo.CurrentUICulture;
            }
        }

        /// <summary>
        /// Attempts to satisfy this compilation from a cache, using already-computed compilation
        /// inputs. Override in server compiler subclasses to provide caching. Return a non-null
        /// value to short-circuit compilation with a cached exit code.
        /// </summary>
        /// <param name="cacheState">Opaque state to be passed to <see cref="OnCompilationSucceeded"/> on success.</param>
        protected virtual int? CheckCache(
            Compilation compilation,
            ImmutableArray<DiagnosticAnalyzer> analyzers,
            ImmutableArray<ISourceGenerator> generators,
            ImmutableArray<AdditionalText> additionalTexts,
            CancellationToken cancellationToken,
            out object? cacheState)
        {
            cacheState = null;
            return null;
        }

        /// <summary>
        /// Notifies the compiler that a compilation completed successfully. Override in server
        /// compiler subclasses to store the result in a cache.
        /// </summary>
        /// <param name="cacheState">Opaque state returned from <see cref="CheckCache"/>.</param>
        protected virtual void OnCompilationSucceeded(
            Compilation compilation,
            ImmutableArray<DiagnosticAnalyzer> analyzers,
            ImmutableArray<ISourceGenerator> generators,
            ImmutableArray<AdditionalText> additionalTexts,
            object? cacheState,
            CancellationToken cancellationToken)
        {
        }

        private void EmitDeterminismKey(
            Compilation compilation,
            ICommonCompilerFileSystem fileSystem,
            ImmutableArray<AdditionalText> additionalTexts,
            ImmutableArray<DiagnosticAnalyzer> analyzers,
            ImmutableArray<ISourceGenerator> generators,
            ImmutableArray<KeyValuePair<string, string>> pathMap,
            EmitOptions? emitOptions,
            Stream? sourceLinkStream,
            ImmutableArray<ResourceDescription> resources)
        {
            var key = compilation.GetDeterministicKey(
                additionalTexts,
                analyzers,
                generators,
                pathMap,
                emitOptions,
                sourceLinkStream,
                resources);

            var filePath = Path.Combine(Arguments.OutputDirectory, Arguments.OutputFileName + ".key");
            using var stream = fileSystem.OpenFile(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
            var bytes = Encoding.UTF8.GetBytes(key);
            stream.Write(bytes, 0, bytes.Length);
        }
    }
}
