File: CommandLine\CommandLineArguments.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// 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.Globalization;
using System.IO;
using System.Text;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// The base class for representing command line arguments to a
    /// <see cref="CommonCompiler"/>.
    /// </summary>
    public abstract class CommandLineArguments
    {
        internal bool IsScriptRunner { get; set; }
 
        /// <summary>
        /// Drop to an interactive loop. If a script is specified in <see cref="SourceFiles"/> executes the script first.
        /// </summary>
        public bool InteractiveMode { get; internal set; }
 
        /// <summary>
        /// Directory used to resolve relative paths stored in the arguments.
        /// </summary>
        /// <remarks>
        /// Except for paths stored in <see cref="MetadataReferences"/>, all
        /// paths stored in the properties of this class are resolved and
        /// absolute. This is the directory that relative paths specified on
        /// command line were resolved against.
        /// </remarks>
        public string? BaseDirectory { get; internal set; }
 
        /// <summary>
        /// A list of pairs of paths. This stores the value of the command-line compiler
        /// option /pathMap:X1=Y1;X2=Y2... which causes a prefix of X1 followed by a path
        /// separator to be replaced by Y1 followed by a path separator, and so on for each following pair.
        /// </summary>
        /// <remarks>
        /// This option is used to help get build-to-build determinism even when the build
        /// directory is different from one build to the next.  The prefix matching is case sensitive.
        /// </remarks>
        public ImmutableArray<KeyValuePair<string, string>> PathMap { get; internal set; }
 
        /// <summary>
        /// Sequence of absolute paths used to search for references.
        /// </summary>
        public ImmutableArray<string> ReferencePaths { get; internal set; }
 
        /// <summary>
        /// Sequence of absolute paths used to search for sources specified as #load directives.
        /// </summary>
        public ImmutableArray<string> SourcePaths { get; internal set; }
 
        /// <summary>
        /// Sequence of absolute paths used to search for key files.
        /// </summary>
        public ImmutableArray<string> KeyFileSearchPaths { get; internal set; }
 
        /// <summary>
        /// If true, use UTF-8 for output.
        /// </summary>
        public bool Utf8Output { get; internal set; }
 
        /// <summary>
        /// Compilation name or null if not specified.
        /// </summary>
        public string? CompilationName { get; internal set; }
 
        /// <summary>
        /// Gets the emit options.
        /// </summary>
        public EmitOptions EmitOptions { get; internal set; } = null!; // initialized by Parse
 
        /// <summary>
        /// Name of the output file or null if not specified.
        /// </summary>
        public string? OutputFileName { get; internal set; }
 
        /// <summary>
        /// Path of the output ref assembly or null if not specified.
        /// </summary>
        public string? OutputRefFilePath { get; internal set; }
 
        /// <summary>
        /// Path of the PDB file or null if same as output binary path with .pdb extension.
        /// </summary>
        public string? PdbPath { get; internal set; }
 
        /// <summary>
        /// Path of the file containing information linking the compilation to source server that stores 
        /// a snapshot of the source code included in the compilation.
        /// </summary>
        public string? SourceLink { get; internal set; }
 
        /// <summary>
        /// Absolute path of the .ruleset file or null if not specified.
        /// </summary>
        public string? RuleSetPath { get; internal set; }
 
        /// <summary>
        /// True to emit PDB information (to a standalone PDB file or embedded into the PE file).
        /// </summary>
        public bool EmitPdb { get; internal set; }
 
        /// <summary>
        /// Absolute path of the output directory (could only be null if there is an error reported).
        /// </summary>
        public string OutputDirectory { get; internal set; } = null!; // initialized by Parse
 
        /// <summary>
        /// Absolute path of the documentation comment XML file or null if not specified.
        /// </summary>
        public string? DocumentationPath { get; internal set; }
 
        /// <summary>
        /// Absolute path of the directory to place generated files in, or <c>null</c> to not emit any generated files.
        /// </summary>
        public string? GeneratedFilesOutputDirectory { get; internal set; }
 
        /// <summary>
        /// Options controlling the generation of a SARIF log file containing compilation or
        /// analysis diagnostics, or null if no log file is desired.
        /// </summary>
        public ErrorLogOptions? ErrorLogOptions { get; internal set; }
 
        /// <summary>
        /// Options controlling the generation of a SARIF log file containing compilation or
        /// analysis diagnostics, or null if no log file is desired.
        /// </summary>
        public string? ErrorLogPath => ErrorLogOptions?.Path;
 
        /// <summary>
        /// An absolute path of the app.config file or null if not specified.
        /// </summary>
        public string? AppConfigPath { get; internal set; }
 
        /// <summary>
        /// Errors while parsing the command line arguments.
        /// </summary>
        public ImmutableArray<Diagnostic> Errors { get; internal set; }
 
        /// <summary>
        /// References to metadata supplied on the command line. 
        /// Includes assemblies specified via /r and netmodules specified via /addmodule.
        /// </summary>
        public ImmutableArray<CommandLineReference> MetadataReferences { get; internal set; }
 
        /// <summary>
        /// References to analyzers supplied on the command line.
        /// </summary>
        public ImmutableArray<CommandLineAnalyzerReference> AnalyzerReferences { get; internal set; }
 
        /// <summary>
        /// A set of paths to EditorConfig-compatible analyzer config files.
        /// </summary>
        public ImmutableArray<string> AnalyzerConfigPaths { get; internal set; }
 
        /// <summary>
        /// A set of additional non-code text files that can be used by analyzers.
        /// </summary>
        public ImmutableArray<CommandLineSourceFile> AdditionalFiles { get; internal set; }
 
        /// <summary>
        /// A set of files to embed in the PDB.
        /// </summary>
        public ImmutableArray<CommandLineSourceFile> EmbeddedFiles { get; internal set; }
 
        /// <value>
        /// Report additional information related to analyzers, such as analyzer execution time.
        /// </value>
        public bool ReportAnalyzer { get; internal set; }
 
        /// <summary>
        /// Report additional information related to InternalsVisibleToAttributes for all assemblies the compiler sees in this compilation.
        /// </summary>
        public bool ReportInternalsVisibleToAttributes { get; internal set; }
 
        /// <value>
        /// Skip execution of <see cref="DiagnosticAnalyzer"/>s.
        /// </value>
        public bool SkipAnalyzers { get; internal set; }
 
        /// <summary>
        /// If true, prepend the command line header logo during 
        /// <see cref="CommonCompiler.Run"/>.
        /// </summary>
        public bool DisplayLogo { get; internal set; }
 
        /// <summary>
        /// If true, append the command line help during
        /// <see cref="CommonCompiler.Run"/>
        /// </summary>
        public bool DisplayHelp { get; internal set; }
 
        /// <summary>
        /// If true, append the compiler version during
        /// <see cref="CommonCompiler.Run"/>
        /// </summary>
        public bool DisplayVersion { get; internal set; }
 
        /// <summary>
        /// If true, prepend the compiler-supported language versions during
        /// <see cref="CommonCompiler.Run"/>
        /// </summary>
        public bool DisplayLangVersions { get; internal set; }
 
        /// <summary>
        /// The path to a Win32 resource.
        /// </summary>
        public string? Win32ResourceFile { get; internal set; }
 
        /// <summary>
        /// The path to a .ico icon file.
        /// </summary>
        public string? Win32Icon { get; internal set; }
 
        /// <summary>
        /// The path to a Win32 manifest file to embed
        /// into the output portable executable (PE) file.
        /// </summary>
        public string? Win32Manifest { get; internal set; }
 
        /// <summary>
        /// If true, do not embed any Win32 manifest, including
        /// one specified by <see cref="Win32Manifest"/> or any
        /// default manifest.
        /// </summary>
        public bool NoWin32Manifest { get; internal set; }
 
        /// <summary>
        /// Resources specified as arguments to the compilation.
        /// </summary>
        public ImmutableArray<ResourceDescription> ManifestResources { get; internal set; }
 
        /// <summary>
        /// Encoding to be used for source files or 'null' for autodetect/default.
        /// </summary>
        public Encoding? Encoding { get; internal set; }
 
        /// <summary>
        /// Hash algorithm to use to calculate source file debug checksums and PDB checksum.
        /// </summary>
        public SourceHashAlgorithm ChecksumAlgorithm { get; internal set; }
 
        /// <summary>
        /// Arguments following a script file or separator "--". Null if the command line parser is not interactive.
        /// </summary>
        public ImmutableArray<string> ScriptArguments { get; internal set; }
 
        /// <summary>
        /// Source file paths.
        /// </summary>
        /// <remarks>
        /// Includes files specified directly on command line as well as files matching patterns specified 
        /// on command line using '*' and '?' wildcards or /recurse option.
        /// </remarks>
        public ImmutableArray<CommandLineSourceFile> SourceFiles { get; internal set; }
 
        /// <summary>
        /// Full path of a log of file paths accessed by the compiler, or null if file logging should be suppressed.
        /// </summary>
        /// <remarks>
        /// Two log files will be created: 
        /// One with path <see cref="TouchedFilesPath"/> and extension ".read" logging the files read,
        /// and second with path <see cref="TouchedFilesPath"/> and extension ".write" logging the files written to during compilation.
        /// </remarks>
        public string? TouchedFilesPath { get; internal set; }
 
        /// <summary>
        /// If true, prints the full path of the file containing errors or
        /// warnings in diagnostics.
        /// </summary>
        public bool PrintFullPaths { get; internal set; }
 
        /// <summary>
        /// Options to the <see cref="CommandLineParser"/>.
        /// </summary>
        /// <returns></returns>
        public ParseOptions ParseOptions
        {
            get { return ParseOptionsCore; }
        }
 
        /// <summary>
        /// Options to the <see cref="Compilation"/>.
        /// </summary>
        public CompilationOptions CompilationOptions
        {
            get { return CompilationOptionsCore; }
        }
 
        protected abstract ParseOptions ParseOptionsCore { get; }
        protected abstract CompilationOptions CompilationOptionsCore { get; }
 
        /// <summary>
        /// Specify the preferred output language name.
        /// </summary>
        public CultureInfo? PreferredUILang { get; internal set; }
 
        internal StrongNameProvider GetStrongNameProvider(StrongNameFileSystem fileSystem)
            => new DesktopStrongNameProvider(KeyFileSearchPaths, fileSystem);
 
        internal CommandLineArguments()
        {
        }
 
        /// <summary>
        /// Returns a full path of the file that the compiler will generate the assembly to if compilation succeeds.
        /// </summary>
        /// <remarks>
        /// The method takes <paramref name="outputFileName"/> rather than using the value of <see cref="OutputFileName"/> 
        /// since the latter might be unspecified, in which case actual output path can't be determined for C# command line
        /// without creating a compilation and finding an entry point. VB does not allow <see cref="OutputFileName"/> to 
        /// be unspecified.
        /// </remarks>
        public string GetOutputFilePath(string outputFileName)
        {
            if (outputFileName == null)
            {
                throw new ArgumentNullException(nameof(outputFileName));
            }
 
            return Path.Combine(OutputDirectory, outputFileName);
        }
 
        /// <summary>
        /// Returns a full path of the PDB file that the compiler will generate the debug symbols to 
        /// if <see cref="EmitPdbFile"/> is true and the compilation succeeds.
        /// </summary>
        /// <remarks>
        /// The method takes <paramref name="outputFileName"/> rather than using the value of <see cref="OutputFileName"/> 
        /// since the latter might be unspecified, in which case actual output path can't be determined for C# command line
        /// without creating a compilation and finding an entry point. VB does not allow <see cref="OutputFileName"/> to 
        /// be unspecified.
        /// </remarks>
        public string GetPdbFilePath(string outputFileName)
        {
            if (outputFileName == null)
            {
                throw new ArgumentNullException(nameof(outputFileName));
            }
 
            return PdbPath ?? Path.Combine(OutputDirectory, Path.ChangeExtension(outputFileName, ".pdb"));
        }
 
        /// <summary>
        /// Returns true if the PDB is generated to a PDB file, as opposed to embedded to the output binary and not generated at all.
        /// </summary>
        public bool EmitPdbFile
            => EmitPdb && EmitOptions.DebugInformationFormat != DebugInformationFormat.Embedded;
 
        #region Metadata References
 
        /// <summary>
        /// Resolves metadata references stored in <see cref="MetadataReferences"/> using given file resolver and metadata provider.
        /// </summary>
        /// <param name="metadataResolver"><see cref="MetadataReferenceResolver"/> to use for assembly name and relative path resolution.</param>
        /// <returns>Yields resolved metadata references or <see cref="UnresolvedMetadataReference"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="metadataResolver"/> is null.</exception>
        public IEnumerable<MetadataReference> ResolveMetadataReferences(MetadataReferenceResolver metadataResolver)
        {
            if (metadataResolver == null)
            {
                throw new ArgumentNullException(nameof(metadataResolver));
            }
 
            return ResolveMetadataReferences(metadataResolver, diagnosticsOpt: null, messageProviderOpt: null);
        }
 
        /// <summary>
        /// Resolves metadata references stored in <see cref="MetadataReferences"/> using given file resolver and metadata provider.
        /// If a non-null diagnostic bag <paramref name="diagnosticsOpt"/> is provided, it catches exceptions that may be generated while reading the metadata file and
        /// reports appropriate diagnostics.
        /// Otherwise, if <paramref name="diagnosticsOpt"/> is null, the exceptions are unhandled.
        /// </summary>
        /// <remarks>
        /// called by CommonCompiler with diagnostics and message provider
        /// </remarks>
        internal IEnumerable<MetadataReference> ResolveMetadataReferences(MetadataReferenceResolver metadataResolver, List<DiagnosticInfo>? diagnosticsOpt, CommonMessageProvider? messageProviderOpt)
        {
            RoslynDebug.Assert(metadataResolver != null);
 
            var resolved = new List<MetadataReference>();
            this.ResolveMetadataReferences(metadataResolver, diagnosticsOpt, messageProviderOpt, resolved);
 
            return resolved;
        }
 
        internal virtual bool ResolveMetadataReferences(MetadataReferenceResolver metadataResolver, List<DiagnosticInfo>? diagnosticsOpt, CommonMessageProvider? messageProviderOpt, List<MetadataReference> resolved)
        {
            bool result = true;
 
            foreach (CommandLineReference cmdReference in MetadataReferences)
            {
                var references = ResolveMetadataReference(cmdReference, metadataResolver, diagnosticsOpt, messageProviderOpt);
                if (!references.IsDefaultOrEmpty)
                {
                    resolved.AddRange(references);
                }
                else
                {
                    result = false;
                    if (diagnosticsOpt == null)
                    {
                        // no diagnostic, so leaved unresolved reference in list
                        resolved.Add(new UnresolvedMetadataReference(cmdReference.Reference, cmdReference.Properties));
                    }
                }
            }
 
            return result;
        }
 
        internal static ImmutableArray<PortableExecutableReference> ResolveMetadataReference(CommandLineReference cmdReference, MetadataReferenceResolver metadataResolver, List<DiagnosticInfo>? diagnosticsOpt, CommonMessageProvider? messageProviderOpt)
        {
            RoslynDebug.Assert(metadataResolver != null);
            Debug.Assert((diagnosticsOpt == null) == (messageProviderOpt == null));
 
            ImmutableArray<PortableExecutableReference> references;
            try
            {
                references = metadataResolver.ResolveReference(cmdReference.Reference, baseFilePath: null, properties: cmdReference.Properties);
            }
            catch (Exception e) when (diagnosticsOpt != null && (e is BadImageFormatException || e is IOException))
            {
                var diagnostic = PortableExecutableReference.ExceptionToDiagnostic(e, messageProviderOpt!, Location.None, cmdReference.Reference, cmdReference.Properties.Kind);
                diagnosticsOpt.Add(((DiagnosticWithInfo)diagnostic).Info);
                return ImmutableArray<PortableExecutableReference>.Empty;
            }
 
            if (references.IsDefaultOrEmpty && diagnosticsOpt != null)
            {
                RoslynDebug.AssertNotNull(messageProviderOpt);
                diagnosticsOpt.Add(new DiagnosticInfo(messageProviderOpt, messageProviderOpt.ERR_MetadataFileNotFound, cmdReference.Reference));
                return ImmutableArray<PortableExecutableReference>.Empty;
            }
 
            return references;
        }
 
        #endregion
 
        #region Analyzer References
 
        /// <summary>
        /// Resolves analyzer references stored in <see cref="AnalyzerReferences"/> using given file resolver.
        /// </summary>
        /// <param name="analyzerLoader">Load an assembly from a file path</param>
        /// <returns>Yields resolved <see cref="AnalyzerFileReference"/> or <see cref="UnresolvedAnalyzerReference"/>.</returns>
        public IEnumerable<AnalyzerReference> ResolveAnalyzerReferences(IAnalyzerAssemblyLoader analyzerLoader)
        {
            foreach (CommandLineAnalyzerReference cmdLineReference in AnalyzerReferences)
            {
                yield return ResolveAnalyzerReference(cmdLineReference, analyzerLoader)
                    ?? (AnalyzerReference)new UnresolvedAnalyzerReference(cmdLineReference.FilePath);
            }
        }
 
        internal void ResolveAnalyzersFromArguments(
            string language,
            List<DiagnosticInfo> diagnostics,
            CommonMessageProvider messageProvider,
            IAnalyzerAssemblyLoader analyzerLoader,
            CompilationOptions compilationOptions,
            bool skipAnalyzers,
            out ImmutableArray<DiagnosticAnalyzer> analyzers,
            out ImmutableArray<ISourceGenerator> generators)
        {
            var analyzerBuilder = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();
            var generatorBuilder = ImmutableArray.CreateBuilder<ISourceGenerator>();
 
            EventHandler<AnalyzerLoadFailureEventArgs> errorHandler = (o, e) =>
            {
                var analyzerReference = o as AnalyzerFileReference;
                RoslynDebug.Assert(analyzerReference is object);
                DiagnosticInfo? diagnostic;
                switch (e.ErrorCode)
                {
                    case AnalyzerLoadFailureEventArgs.FailureErrorCode.UnableToLoadAnalyzer:
                        diagnostic = new DiagnosticInfo(messageProvider, messageProvider.WRN_UnableToLoadAnalyzer, analyzerReference.FullPath, e.Message);
                        break;
                    case AnalyzerLoadFailureEventArgs.FailureErrorCode.UnableToCreateAnalyzer:
                        diagnostic = new DiagnosticInfo(messageProvider, messageProvider.WRN_AnalyzerCannotBeCreated, e.TypeName ?? "", analyzerReference.FullPath, e.Message);
                        break;
                    case AnalyzerLoadFailureEventArgs.FailureErrorCode.NoAnalyzers:
                        diagnostic = new DiagnosticInfo(messageProvider, messageProvider.WRN_NoAnalyzerInAssembly, analyzerReference.FullPath);
                        break;
                    case AnalyzerLoadFailureEventArgs.FailureErrorCode.ReferencesFramework:
                        diagnostic = new DiagnosticInfo(messageProvider, messageProvider.WRN_AnalyzerReferencesFramework, analyzerReference.FullPath, e.TypeName!);
                        break;
                    case AnalyzerLoadFailureEventArgs.FailureErrorCode.ReferencesNewerCompiler:
                        diagnostic = new DiagnosticInfo(messageProvider, messageProvider.WRN_AnalyzerReferencesNewerCompiler, analyzerReference.FullPath, e.ReferencedCompilerVersion!.ToString(), typeof(AnalyzerFileReference).Assembly.GetName().Version!.ToString());
                        break;
                    case AnalyzerLoadFailureEventArgs.FailureErrorCode.None:
                    default:
                        return;
                }
 
                // Filter this diagnostic based on the compilation options so that /nowarn and /warnaserror etc. take effect.
                diagnostic = messageProvider.FilterDiagnosticInfo(diagnostic, compilationOptions);
 
                if (diagnostic != null)
                {
                    diagnostics.Add(diagnostic);
                }
            };
 
            var resolvedReferencesSet = PooledHashSet<AnalyzerFileReference>.GetInstance();
            var resolvedReferencesList = ArrayBuilder<AnalyzerFileReference>.GetInstance();
            foreach (var reference in AnalyzerReferences)
            {
                var resolvedReference = ResolveAnalyzerReference(reference, analyzerLoader);
                if (resolvedReference != null)
                {
                    var isAdded = resolvedReferencesSet.Add(resolvedReference);
                    if (isAdded)
                    {
                        // register the reference to the analyzer loader:
                        analyzerLoader.AddDependencyLocation(resolvedReference.FullPath);
 
                        resolvedReferencesList.Add(resolvedReference);
                    }
                    else
                    {
                        // https://github.com/dotnet/roslyn/issues/63856
                        //diagnostics.Add(new DiagnosticInfo(messageProvider, messageProvider.WRN_DuplicateAnalyzerReference, reference.FilePath));
                    }
                }
                else
                {
                    diagnostics.Add(new DiagnosticInfo(messageProvider, messageProvider.ERR_MetadataFileNotFound, reference.FilePath));
                }
            }
 
            // All analyzer references are registered now, we can start loading them.
            foreach (var resolvedReference in resolvedReferencesList)
            {
                resolvedReference.AnalyzerLoadFailed += errorHandler;
                resolvedReference.AddAnalyzers(analyzerBuilder, language, shouldIncludeAnalyzer);
                resolvedReference.AddGenerators(generatorBuilder, language);
                resolvedReference.AnalyzerLoadFailed -= errorHandler;
            }
 
            resolvedReferencesList.Free();
            resolvedReferencesSet.Free();
 
            generators = generatorBuilder.ToImmutable();
            analyzers = analyzerBuilder.ToImmutable();
 
            // If we are skipping analyzers, ensure that we only add suppressors.
            bool shouldIncludeAnalyzer(DiagnosticAnalyzer analyzer) => !skipAnalyzers || analyzer is DiagnosticSuppressor;
        }
 
        private AnalyzerFileReference? ResolveAnalyzerReference(CommandLineAnalyzerReference reference, IAnalyzerAssemblyLoader analyzerLoader)
        {
            string? resolvedPath = FileUtilities.ResolveRelativePath(reference.FilePath, basePath: null, baseDirectory: BaseDirectory, searchPaths: ReferencePaths, fileExists: File.Exists);
            if (resolvedPath != null)
            {
                resolvedPath = FileUtilities.TryNormalizeAbsolutePath(resolvedPath);
            }
 
            if (resolvedPath != null)
            {
                return new AnalyzerFileReference(resolvedPath, analyzerLoader);
            }
 
            return null;
        }
        #endregion
    }
}