|
// 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.Text;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis
{
public abstract class CommandLineParser
{
private readonly CommonMessageProvider _messageProvider;
internal readonly bool IsScriptCommandLineParser;
private static readonly char[] s_searchPatternTrimChars = new char[] { '\t', '\n', '\v', '\f', '\r', ' ', '\x0085', '\x00a0' };
internal const string ErrorLogOptionFormat = "<file>[,version={1|1.0|2|2.1}]";
internal CommandLineParser(CommonMessageProvider messageProvider, bool isScriptCommandLineParser)
{
RoslynDebug.Assert(messageProvider != null);
_messageProvider = messageProvider;
IsScriptCommandLineParser = isScriptCommandLineParser;
}
internal CommonMessageProvider MessageProvider
{
get { return _messageProvider; }
}
protected abstract string RegularFileExtension { get; }
protected abstract string ScriptFileExtension { get; }
// internal for testing
internal virtual TextReader CreateTextFileReader(string fullPath)
{
return new StreamReader(
new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read),
detectEncodingFromByteOrderMarks: true);
}
/// <summary>
/// Enumerates files in the specified directory and subdirectories whose name matches the given pattern.
/// </summary>
/// <param name="directory">Full path of the directory to enumerate.</param>
/// <param name="fileNamePattern">File name pattern. May contain wildcards '*' (matches zero or more characters) and '?' (matches any character).</param>
/// <param name="searchOption">Specifies whether to search the specified <paramref name="directory"/> only, or all its subdirectories as well.</param>
/// <returns>Sequence of file paths.</returns>
internal virtual IEnumerable<string> EnumerateFiles(string? directory, string fileNamePattern, SearchOption searchOption)
{
if (directory is null)
{
return SpecializedCollections.EmptyEnumerable<string>();
}
Debug.Assert(PathUtilities.IsAbsolute(directory));
return Directory.EnumerateFiles(directory, fileNamePattern, searchOption);
}
internal abstract CommandLineArguments CommonParse(IEnumerable<string> args, string baseDirectory, string? sdkDirectory, string? additionalReferenceDirectories);
/// <summary>
/// Parses a command line.
/// </summary>
/// <param name="args">A collection of strings representing the command line arguments.</param>
/// <param name="baseDirectory">The base directory used for qualifying file locations.</param>
/// <param name="sdkDirectory">The directory to search for mscorlib, or null if not available.</param>
/// <param name="additionalReferenceDirectories">A string representing additional reference paths.</param>
/// <returns>a <see cref="CommandLineArguments"/> object representing the parsed command line.</returns>
public CommandLineArguments Parse(IEnumerable<string> args, string baseDirectory, string? sdkDirectory, string? additionalReferenceDirectories)
{
return CommonParse(args, baseDirectory, sdkDirectory, additionalReferenceDirectories);
}
internal static bool IsOptionName(string optionName, ReadOnlyMemory<char> value) =>
IsOptionName(optionName, value.Span);
internal static bool IsOptionName(string shortOptionName, string longOptionName, ReadOnlyMemory<char> value) =>
IsOptionName(shortOptionName, value) || IsOptionName(longOptionName, value);
/// <summary>
/// Determines if a <see cref="ReadOnlySpan{Char}"/> is equal to the provided option name
/// </summary>
/// <remarks>
/// Prefer this over the Equals methods on <see cref="ReadOnlySpan{Char}"/>. The
/// <see cref="StringComparison.InvariantCultureIgnoreCase"/> implementation allocates a <see cref="String"/>.
/// The 99% case here is that we are dealing with an ASCII string that matches the input hence
/// it's worth special casing that here and falling back to the more complicated comparison
/// when dealing with non-ASCII input
/// </remarks>
internal static bool IsOptionName(string optionName, ReadOnlySpan<char> value)
{
Debug.Assert(isAllAscii(optionName.AsSpan()));
if (isAllAscii(value))
{
if (optionName.Length != value.Length)
return false;
for (int i = 0; i < optionName.Length; i++)
{
if (optionName[i] != char.ToLowerInvariant(value[i]))
{
return false;
}
}
return true;
}
return optionName.AsSpan().Equals(value, StringComparison.InvariantCultureIgnoreCase);
static bool isAllAscii(ReadOnlySpan<char> span)
{
foreach (char ch in span)
{
if (ch > 127)
return false;
}
return true;
}
}
internal static bool IsOption(string arg) => IsOption(arg.AsSpan());
internal static bool IsOption(ReadOnlySpan<char> arg) =>
arg.Length > 0 && (arg[0] == '/' || arg[0] == '-');
internal static bool IsOption(string optionName, string arg, out ReadOnlyMemory<char> name, out ReadOnlyMemory<char>? value) =>
TryParseOption(arg, out name, out value) &&
IsOptionName(optionName, name);
internal static bool TryParseOption(string arg, [NotNullWhen(true)] out string? name, out string? value)
{
if (TryParseOption(arg, out ReadOnlyMemory<char> nameMemory, out ReadOnlyMemory<char>? valueMemory))
{
name = nameMemory.ToString().ToLowerInvariant();
value = valueMemory?.ToString();
return true;
}
name = null;
value = null;
return false;
}
internal static bool TryParseOption(string arg, out ReadOnlyMemory<char> name, out ReadOnlyMemory<char>? value)
{
if (!IsOption(arg))
{
name = default;
value = null;
return false;
}
// handle stdin operator
if (arg == "-")
{
name = arg.AsMemory();
value = null;
return true;
}
int colon = arg.IndexOf(':');
// temporary heuristic to detect Unix-style rooted paths
// pattern /goo/* or //* will not be treated as a compiler option
//
// TODO: consider introducing "/s:path" to disambiguate paths starting with /
if (arg.Length > 1 && arg[0] != '-')
{
int separator = arg.IndexOf('/', 1);
if (separator > 0 && (colon < 0 || separator < colon))
{
// "/goo/
// "//
name = default;
value = null;
return false;
}
}
var argMemory = arg.AsMemory();
if (colon >= 0)
{
name = argMemory.Slice(1, colon - 1);
value = argMemory.Slice(colon + 1);
}
else
{
name = argMemory.Slice(1);
value = null;
}
return true;
}
internal ErrorLogOptions? ParseErrorLogOptions(
ReadOnlyMemory<char> arg,
IList<Diagnostic> diagnostics,
string? baseDirectory,
out bool diagnosticAlreadyReported)
{
diagnosticAlreadyReported = false;
var parts = ArrayBuilder<ReadOnlyMemory<char>>.GetInstance();
try
{
ParseSeparatedStrings(arg, s_pathSeparators, removeEmptyEntries: true, parts);
if (parts.Count == 0 || parts[0].Length == 0)
{
return null;
}
string? path = ParseGenericPathToFile(parts[0].ToString(), diagnostics, baseDirectory);
if (path is null)
{
// ParseGenericPathToFile already reported the failure, so the caller should not
// report its own failure.
diagnosticAlreadyReported = true;
return null;
}
const char ParameterNameValueSeparator = '=';
SarifVersion sarifVersion = SarifVersion.Default;
if (parts.Count > 1 && parts[1].Length > 0)
{
string part = parts[1].ToString();
string versionParameterDesignator = "version" + ParameterNameValueSeparator;
int versionParameterDesignatorLength = versionParameterDesignator.Length;
if (!(
part.Length > versionParameterDesignatorLength &&
part.Substring(0, versionParameterDesignatorLength).Equals(versionParameterDesignator, StringComparison.OrdinalIgnoreCase) &&
SarifVersionFacts.TryParse(part.Substring(versionParameterDesignatorLength), out sarifVersion)
))
{
return null;
}
}
if (parts.Count > 2)
{
return null;
}
return new ErrorLogOptions(path, sarifVersion);
}
finally
{
parts.Free();
}
}
internal static void ParseAndNormalizeFile(
string unquoted,
string? baseDirectory,
out string? outputFileName,
out string? outputDirectory,
out string invalidPath)
{
outputFileName = null;
outputDirectory = null;
invalidPath = unquoted;
string? resolvedPath = FileUtilities.ResolveRelativePath(unquoted, baseDirectory);
if (resolvedPath != null)
{
try
{
// Windows 10 and earlier placed restrictions on file names that originally appeared as device
// names. For example COM1, PRN, CON, AUX, etc ... Files could not be created with those names even
// with extensions like .txt. When those restricted names are passed to GetFullPath the
// runtime will escape them with \\.\. For example GetFullPath("aux.txt") will return "\\.\aux.txt".
// The compiler detects these illegal names and bails out early
//
// Windows 11 removed this restriction though and hence the names are now legal. Cannot find documentation
// to support this but experimentally it can be validated.
resolvedPath = Path.GetFullPath(resolvedPath);
// preserve possible invalid path info for diagnostic purpose
invalidPath = resolvedPath;
outputFileName = Path.GetFileName(resolvedPath);
outputDirectory = Path.GetDirectoryName(resolvedPath);
}
catch (Exception)
{
resolvedPath = null;
}
if (outputFileName != null)
{
// normalize file
outputFileName = RemoveTrailingSpacesAndDots(outputFileName);
}
}
if (resolvedPath == null ||
// NUL-terminated, non-empty, valid Unicode strings
!MetadataHelpers.IsValidMetadataIdentifier(outputDirectory) ||
!MetadataHelpers.IsValidMetadataIdentifier(outputFileName))
{
outputFileName = null;
}
}
/// <summary>
/// Trims all '.' and whitespace from the end of the path
/// </summary>
[return: NotNullIfNotNull(nameof(path))]
internal static string? RemoveTrailingSpacesAndDots(string? path)
{
if (path == null)
{
return path;
}
int length = path.Length;
for (int i = length - 1; i >= 0; i--)
{
char c = path[i];
if (!char.IsWhiteSpace(c) && c != '.')
{
return i == (length - 1) ? path : path.Substring(0, i + 1);
}
}
return string.Empty;
}
protected ImmutableArray<KeyValuePair<string, string>> ParsePathMap(string pathMap, IList<Diagnostic> errors)
{
if (pathMap.IsEmpty())
{
return ImmutableArray<KeyValuePair<string, string>>.Empty;
}
var pathMapBuilder = ArrayBuilder<KeyValuePair<string, string>>.GetInstance();
foreach (var kEqualsV in SplitWithDoubledSeparatorEscaping(pathMap, ','))
{
if (kEqualsV.IsEmpty())
{
continue;
}
var kv = SplitWithDoubledSeparatorEscaping(kEqualsV, '=');
if (kv.Length != 2)
{
errors.Add(Diagnostic.Create(_messageProvider, _messageProvider.ERR_InvalidPathMap));
continue;
}
var from = kv[0];
var to = kv[1];
if (from.Length == 0 || to.Length == 0)
{
errors.Add(Diagnostic.Create(_messageProvider, _messageProvider.ERR_InvalidPathMap));
}
else
{
from = PathUtilities.EnsureTrailingSeparator(from);
to = PathUtilities.EnsureTrailingSeparator(to);
pathMapBuilder.Add(new KeyValuePair<string, string>(from, to));
}
}
return pathMapBuilder.ToImmutableAndFree();
}
/// <summary>
/// Splits specified <paramref name="str"/> on <paramref name="separator"/>
/// treating two consecutive separators as if they were a single non-separating character.
/// E.g. "a,,b,c" split on ',' yields ["a,b", "c"].
/// </summary>
internal static string[] SplitWithDoubledSeparatorEscaping(string str, char separator)
{
if (str.Length == 0)
{
return Array.Empty<string>();
}
var result = ArrayBuilder<string>.GetInstance();
var pooledPart = PooledStringBuilder.GetInstance();
var part = pooledPart.Builder;
int i = 0;
while (i < str.Length)
{
char c = str[i++];
if (c == separator)
{
if (i < str.Length && str[i] == separator)
{
i++;
}
else
{
result.Add(part.ToString());
part.Clear();
continue;
}
}
part.Append(c);
}
result.Add(part.ToString());
pooledPart.Free();
return result.ToArrayAndFree();
}
internal void ParseOutputFile(
string value,
IList<Diagnostic> errors,
string? baseDirectory,
out string? outputFileName,
out string? outputDirectory)
{
string unquoted = RemoveQuotesAndSlashes(value);
ParseAndNormalizeFile(unquoted, baseDirectory, out outputFileName, out outputDirectory, out string? invalidPath);
if (outputFileName == null ||
!MetadataHelpers.IsValidAssemblyOrModuleName(outputFileName))
{
errors.Add(Diagnostic.Create(_messageProvider, _messageProvider.FTL_InvalidInputFileName, invalidPath));
outputFileName = null;
outputDirectory = baseDirectory;
}
}
internal string? ParsePdbPath(
string value,
IList<Diagnostic> errors,
string? baseDirectory)
{
string? pdbPath = null;
string unquoted = RemoveQuotesAndSlashes(value);
ParseAndNormalizeFile(unquoted, baseDirectory, out string? outputFileName, out string? outputDirectory, out string? invalidPath);
if (outputFileName == null ||
PathUtilities.ChangeExtension(outputFileName, extension: null).Length == 0)
{
errors.Add(Diagnostic.Create(_messageProvider, _messageProvider.FTL_InvalidInputFileName, invalidPath));
}
else
{
// If outputDirectory were null, then outputFileName would be null (see ParseAndNormalizeFile)
Debug.Assert(outputDirectory is object);
pdbPath = Path.ChangeExtension(Path.Combine(outputDirectory, outputFileName), ".pdb");
}
return pdbPath;
}
internal string? ParseGenericPathToFile(
string unquoted,
IList<Diagnostic> errors,
string? baseDirectory,
bool generateDiagnostic = true)
{
string? genericPath = null;
ParseAndNormalizeFile(unquoted, baseDirectory, out string? outputFileName, out string? outputDirectory, out string? invalidPath);
if (string.IsNullOrWhiteSpace(outputFileName))
{
if (generateDiagnostic)
{
errors.Add(Diagnostic.Create(_messageProvider, _messageProvider.FTL_InvalidInputFileName, invalidPath));
}
}
else
{
// If outputDirectory were null, then outputFileName would be null (see ParseAndNormalizeFile)
genericPath = Path.Combine(outputDirectory!, outputFileName);
}
return genericPath;
}
internal void FlattenArgs(
IEnumerable<string> rawArguments,
IList<Diagnostic> diagnostics,
ArrayBuilder<string> processedArgs,
List<string>? scriptArgsOpt,
string? baseDirectory,
List<string>? responsePaths = null)
{
bool parsingScriptArgs = false;
bool sourceFileSeen = false;
bool optionsEnded = false;
var args = ArrayBuilder<string>.GetInstance();
args.AddRange(rawArguments);
args.ReverseContents();
var argsIndex = args.Count - 1;
while (argsIndex >= 0)
{
// EDMAURER trim off whitespace. Otherwise behavioral differences arise
// when the strings which represent args are constructed by cmd or users.
// cmd won't produce args with whitespace at the end.
string arg = args[argsIndex].TrimEnd();
argsIndex--;
if (parsingScriptArgs)
{
scriptArgsOpt!.Add(arg);
continue;
}
if (scriptArgsOpt != null)
{
// The order of the following two checks matters.
//
// Command line: Script: Script args:
// csi -- script.csx a b c script.csx ["a", "b", "c"]
// csi script.csx -- a b c script.csx ["--", "a", "b", "c"]
// csi -- @script.csx a b c @script.csx ["a", "b", "c"]
//
if (sourceFileSeen)
{
// csi/vbi: at most one script can be specified on command line, anything else is a script arg:
parsingScriptArgs = true;
scriptArgsOpt.Add(arg);
continue;
}
if (!optionsEnded && arg == "--")
{
// csi/vbi: no argument past "--" should be treated as an option/response file
optionsEnded = true;
processedArgs.Add(arg);
continue;
}
}
if (!optionsEnded && arg.StartsWith("@", StringComparison.Ordinal))
{
// response file:
string path = RemoveQuotesAndSlashes(arg.Substring(1)).TrimEnd(null);
string? resolvedPath = FileUtilities.ResolveRelativePath(path, baseDirectory);
if (resolvedPath != null)
{
parseResponseFile(resolvedPath);
if (responsePaths != null)
{
string? directory = PathUtilities.GetDirectoryName(resolvedPath);
if (directory is null)
{
diagnostics.Add(Diagnostic.Create(_messageProvider, _messageProvider.FTL_InvalidInputFileName, path));
}
else
{
responsePaths.Add(FileUtilities.NormalizeAbsolutePath(directory));
}
}
}
else
{
diagnostics.Add(Diagnostic.Create(_messageProvider, _messageProvider.FTL_InvalidInputFileName, path));
}
}
else
{
processedArgs.Add(arg);
sourceFileSeen |= optionsEnded || !IsOption(arg);
}
}
args.Free();
void parseResponseFile(string fullPath)
{
var stringBuilder = PooledStringBuilder.GetInstance();
var splitList = new List<string>();
try
{
Debug.Assert(PathUtilities.IsAbsolute(fullPath));
using TextReader reader = CreateTextFileReader(fullPath);
Span<char> lineBuffer = stackalloc char[256];
var lineBufferLength = 0;
while (true)
{
var ch = reader.Read();
if (ch == -1)
{
if (lineBufferLength > 0)
{
stringBuilder.Builder.Length = 0;
CommandLineUtilities.SplitCommandLineIntoArguments(
lineBuffer.Slice(0, lineBufferLength),
removeHashComments: true,
stringBuilder.Builder,
splitList,
out _);
}
break;
}
if (ch is '\r' or '\n')
{
if (ch is '\r' && reader.Peek() == '\n')
{
reader.Read();
}
stringBuilder.Builder.Length = 0;
CommandLineUtilities.SplitCommandLineIntoArguments(
lineBuffer.Slice(0, lineBufferLength),
removeHashComments: true,
stringBuilder.Builder,
splitList,
out _);
lineBufferLength = 0;
}
else
{
if (lineBufferLength >= lineBuffer.Length)
{
var temp = new char[lineBuffer.Length * 2];
lineBuffer.CopyTo(temp.AsSpan());
lineBuffer = temp;
}
lineBuffer[lineBufferLength] = (char)ch;
lineBufferLength++;
}
}
}
catch (Exception)
{
diagnostics.Add(Diagnostic.Create(_messageProvider, _messageProvider.ERR_OpenResponseFile, fullPath));
return;
}
for (var i = splitList.Count - 1; i >= 0; i--)
{
var newArg = splitList[i];
// Ignores /noconfig option specified in a response file
if (!string.Equals(newArg, "/noconfig", StringComparison.OrdinalIgnoreCase) && !string.Equals(newArg, "-noconfig", StringComparison.OrdinalIgnoreCase))
{
argsIndex++;
if (argsIndex < args.Count)
{
args[argsIndex] = newArg;
}
else
{
args.Add(newArg);
}
}
else
{
diagnostics.Add(Diagnostic.Create(_messageProvider, _messageProvider.WRN_NoConfigNotOnCommandLine));
}
}
stringBuilder.Free();
}
}
internal static IEnumerable<string> ParseResponseLines(IEnumerable<string> lines)
{
var arguments = new List<string>();
foreach (string line in lines)
{
arguments.AddRange(CommandLineUtilities.SplitCommandLineIntoArguments(line, removeHashComments: true));
}
return arguments;
}
/// <summary>
/// Returns false if any of the client arguments are invalid and true otherwise.
/// </summary>
/// <param name="args">
/// The original args to the client.
/// </param>
/// <param name="parsedArgs">
/// The original args minus the client args, if no errors were encountered.
/// </param>
/// <param name="containsShared">
/// Only defined if no errors were encountered.
/// True if '/shared' was an argument, false otherwise.
/// </param>
/// <param name="keepAliveValue">
/// Only defined if no errors were encountered.
/// The value to the '/keepalive' argument if one was specified, null otherwise.
/// </param>
/// <param name="errorMessage">
/// Only defined if errors were encountered.
/// The error message for the encountered error.
/// </param>
/// <param name="pipeName">
/// Only specified if <paramref name="containsShared"/> is true and the session key
/// was provided. Can be null
/// </param>
internal static bool TryParseClientArgs(
IEnumerable<string> args,
[NotNullWhen(true)] out List<string>? parsedArgs,
out bool containsShared,
out string? keepAliveValue,
out string? pipeName,
[NotNullWhen(false)] out string? errorMessage)
{
containsShared = false;
keepAliveValue = null;
errorMessage = null;
parsedArgs = null;
pipeName = null;
var newArgs = new List<string>();
foreach (var arg in args)
{
if (isClientArgsOption(arg, "keepalive", out bool hasValue, out string? value))
{
if (string.IsNullOrEmpty(value))
{
errorMessage = CodeAnalysisResources.MissingKeepAlive;
return false;
}
if (int.TryParse(value, out int intValue))
{
if (intValue < -1)
{
errorMessage = CodeAnalysisResources.KeepAliveIsTooSmall;
return false;
}
keepAliveValue = value;
}
else
{
errorMessage = CodeAnalysisResources.KeepAliveIsNotAnInteger;
return false;
}
continue;
}
if (isClientArgsOption(arg, "shared", out hasValue, out value))
{
if (hasValue)
{
if (string.IsNullOrEmpty(value))
{
errorMessage = CodeAnalysisResources.SharedArgumentMissing;
return false;
}
pipeName = value;
}
containsShared = true;
continue;
}
newArgs.Add(arg);
}
if (keepAliveValue != null && !containsShared)
{
errorMessage = CodeAnalysisResources.KeepAliveWithoutShared;
return false;
}
else
{
parsedArgs = newArgs;
return true;
}
static bool isClientArgsOption(string arg, string optionName, out bool hasValue, out string? optionValue)
{
hasValue = false;
optionValue = null;
if (arg.Length == 0 || !(arg[0] == '/' || arg[0] == '-'))
{
return false;
}
arg = arg.Substring(1);
if (!arg.StartsWith(optionName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (arg.Length > optionName.Length)
{
if (!(arg[optionName.Length] == ':' || arg[optionName.Length] == '='))
{
return false;
}
hasValue = true;
optionValue = arg.Substring(optionName.Length + 1).Trim('"');
}
return true;
}
}
internal static string MismatchedVersionErrorText => CodeAnalysisResources.MismatchedVersion;
private static readonly char[] s_resourceSeparators = { ',' };
internal static void ParseResourceDescription(
ReadOnlyMemory<char> resourceDescriptor,
string? baseDirectory,
bool skipLeadingSeparators, //VB does this
out string? filePath,
out string? fullPath,
out string? fileName,
out string resourceName,
out string? accessibility)
{
filePath = null;
fullPath = null;
fileName = null;
resourceName = "";
accessibility = null;
// resource descriptor is: "<filePath>[,<string name>[,public|private]]"
var parts = ArrayBuilder<ReadOnlyMemory<char>>.GetInstance();
ParseSeparatedStrings(resourceDescriptor, s_resourceSeparators, removeEmptyEntries: false, parts);
int offset = 0;
int length = parts.Count;
if (skipLeadingSeparators)
{
for (; offset < length && parts[offset].Length == 0; offset++)
{
}
length -= offset;
}
if (length >= 1)
{
filePath = RemoveQuotesAndSlashes(parts[offset + 0]);
}
if (length >= 2)
{
resourceName = RemoveQuotesAndSlashes(parts[offset + 1]);
}
if (length >= 3)
{
accessibility = RemoveQuotesAndSlashes(parts[offset + 2]);
}
parts.Free();
if (RoslynString.IsNullOrWhiteSpace(filePath))
{
return;
}
fileName = PathUtilities.GetFileName(filePath);
fullPath = FileUtilities.ResolveRelativePath(filePath, baseDirectory);
// The default resource name is the file name.
// Also use the file name for the name when user specifies string like "filePath,,private"
if (RoslynString.IsNullOrWhiteSpace(resourceName))
{
resourceName = fileName;
}
}
/// <summary>
/// See <see cref="CommandLineUtilities.SplitCommandLineIntoArguments(string, bool)"/>
/// </summary>
public static IEnumerable<string> SplitCommandLineIntoArguments(string commandLine, bool removeHashComments)
{
return CommandLineUtilities.SplitCommandLineIntoArguments(commandLine, removeHashComments);
}
/// <summary>
/// Remove the extraneous quotes and slashes from the argument. This function is designed to have
/// compat behavior with the native compiler.
/// </summary>
/// <remarks>
/// Mimics the function RemoveQuotes from the native C# compiler. The native VB equivalent of this
/// function is called RemoveQuotesAndSlashes. It has virtually the same behavior except for a few
/// quirks in error cases.
/// </remarks>
[return: NotNullIfNotNull(parameterName: nameof(arg))]
internal static string? RemoveQuotesAndSlashes(string? arg) =>
arg is not null
? RemoveQuotesAndSlashes(arg.AsMemory())
: null;
internal static string RemoveQuotesAndSlashes(ReadOnlyMemory<char> argMemory) =>
RemoveQuotesAndSlashesEx(argMemory).ToString();
internal static string? RemoveQuotesAndSlashes(ReadOnlyMemory<char>? argMemory) =>
argMemory is { } m
? RemoveQuotesAndSlashesEx(m).ToString()
: null;
internal static ReadOnlyMemory<char>? RemoveQuotesAndSlashesEx(ReadOnlyMemory<char>? argMemory) =>
argMemory is { } m
? RemoveQuotesAndSlashesEx(m)
: null;
internal static ReadOnlyMemory<char> RemoveQuotesAndSlashesEx(ReadOnlyMemory<char> argMemory)
{
if (removeFastPath(argMemory) is { } m)
{
return m;
}
var pool = PooledStringBuilder.GetInstance();
var builder = pool.Builder;
var arg = argMemory.Span;
var i = 0;
while (i < arg.Length)
{
var cur = arg[i];
switch (cur)
{
case '\\':
processSlashes(builder, arg, ref i);
break;
case '"':
// Intentionally dropping quotes that don't have explicit escaping.
i++;
break;
default:
builder.Append(cur);
i++;
break;
}
}
return pool.ToStringAndFree().AsMemory();
// Mimic behavior of the native function by the same name.
static void processSlashes(StringBuilder builder, ReadOnlySpan<char> arg, ref int i)
{
RoslynDebug.Assert(arg != null);
Debug.Assert(i < arg.Length);
var slashCount = 0;
while (i < arg.Length && arg[i] == '\\')
{
slashCount++;
i++;
}
if (i < arg.Length && arg[i] == '"')
{
// Before a quote slashes are interpretted as escape sequences for other slashes so
// output one for every two.
while (slashCount >= 2)
{
builder.Append('\\');
slashCount -= 2;
}
Debug.Assert(slashCount >= 0);
// If there is an odd number of slashes then the quote is escaped and hence a part
// of the output. Otherwise it is a normal quote and can be ignored.
if (slashCount == 1)
{
// The quote is escaped so eat it.
builder.Append('"');
}
i++;
}
else
{
// Slashes that aren't followed by quotes are simply slashes.
while (slashCount > 0)
{
builder.Append('\\');
slashCount--;
}
}
}
// The 99% case when using MSBuild is that at worst a path has quotes at the start and
// end of the string but no where else. When that happens there is no need to allocate
// a new string here and instead we can just do a simple Slice on the existing
// ReadOnlyMemory object.
//
// This removes one of the largest allocation paths during command line parsing
static ReadOnlyMemory<char>? removeFastPath(ReadOnlyMemory<char> arg)
{
int start = 0;
int end = arg.Length;
var span = arg.Span;
while (end > 0 && span[end - 1] == '"')
{
end--;
}
while (start < end && span[start] == '"')
{
start++;
}
for (int i = start; i < end; i++)
{
if (span[i] == '"')
{
return null;
}
}
return arg.Slice(start, end - start);
}
}
private static readonly char[] s_pathSeparators = { ';', ',' };
private static readonly char[] s_wildcards = new[] { '*', '?' };
internal static IEnumerable<string> ParseSeparatedPaths(string arg)
{
var builder = ArrayBuilder<ReadOnlyMemory<char>>.GetInstance();
ParseSeparatedPathsEx(arg.AsMemory(), builder);
return builder.ToArrayAndFree().Select(static x => x.ToString());
}
internal static void ParseSeparatedPathsEx(ReadOnlyMemory<char>? str, ArrayBuilder<ReadOnlyMemory<char>> builder)
{
ParseSeparatedStrings(str, s_pathSeparators, removeEmptyEntries: true, builder);
for (var i = 0; i < builder.Count; i++)
{
builder[i] = RemoveQuotesAndSlashesEx(builder[i]);
}
}
/// <summary>
/// Split a string by a set of separators, taking quotes into account.
/// </summary>
internal static void ParseSeparatedStrings(ReadOnlyMemory<char>? strMemory, char[] separators, bool removeEmptyEntries, ArrayBuilder<ReadOnlyMemory<char>> builder)
{
if (strMemory is null)
{
return;
}
int nextPiece = 0;
var inQuotes = false;
var memory = strMemory.Value;
var span = memory.Span;
for (int i = 0; i < span.Length; i++)
{
var c = span[i];
if (c == '\"')
{
inQuotes = !inQuotes;
}
if (!inQuotes && separators.IndexOf(c) >= 0)
{
var current = memory.Slice(nextPiece, i - nextPiece);
if (!removeEmptyEntries || current.Length > 0)
{
builder.Add(current);
}
nextPiece = i + 1;
}
}
var last = memory.Slice(nextPiece);
if (!removeEmptyEntries || last.Length > 0)
{
builder.Add(last);
}
}
internal IEnumerable<string> ResolveRelativePaths(IEnumerable<string> paths, string baseDirectory, IList<Diagnostic> errors)
{
foreach (var path in paths)
{
string? resolvedPath = FileUtilities.ResolveRelativePath(path, baseDirectory);
if (resolvedPath == null)
{
errors.Add(Diagnostic.Create(_messageProvider, _messageProvider.FTL_InvalidInputFileName, path));
}
else
{
yield return resolvedPath;
}
}
}
private protected CommandLineSourceFile ToCommandLineSourceFile(string resolvedPath, bool isInputRedirected = false)
{
bool isScriptFile;
if (IsScriptCommandLineParser)
{
ReadOnlyMemory<char> extension = PathUtilities.GetExtension(resolvedPath.AsMemory());
isScriptFile = !extension.Span.Equals(RegularFileExtension.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
else
{
// TODO: uncomment when fixing https://github.com/dotnet/roslyn/issues/5325
//isScriptFile = string.Equals(extension, ScriptFileExtension, StringComparison.OrdinalIgnoreCase);
isScriptFile = false;
}
return new CommandLineSourceFile(resolvedPath, isScriptFile, isInputRedirected);
}
internal void ParseFileArgument(ReadOnlyMemory<char> arg, string? baseDirectory, ArrayBuilder<string> filePathBuilder, IList<Diagnostic> errors)
{
Debug.Assert(IsScriptCommandLineParser || !arg.StartsWith('-') && !arg.StartsWith('@'));
// We remove all doubles quotes from a file name. So that, for example:
// "Path With Spaces"\goo.cs
// becomes
// Path With Spaces\goo.cs
string path = RemoveQuotesAndSlashes(arg);
int wildcard = path.IndexOfAny(s_wildcards);
if (wildcard != -1)
{
foreach (var file in ExpandFileNamePattern(path, baseDirectory, SearchOption.TopDirectoryOnly, errors))
{
filePathBuilder.Add(file);
}
}
else
{
string? resolvedPath = FileUtilities.ResolveRelativePath(path, baseDirectory);
if (resolvedPath == null)
{
errors.Add(Diagnostic.Create(MessageProvider, (int)MessageProvider.FTL_InvalidInputFileName, path));
}
else
{
filePathBuilder.Add(resolvedPath);
}
}
}
private protected void ParseSeparatedFileArgument(ReadOnlyMemory<char> value, string? baseDirectory, ArrayBuilder<string> filePathBuilder, IList<Diagnostic> errors)
{
var pathBuilder = ArrayBuilder<ReadOnlyMemory<char>>.GetInstance();
ParseSeparatedPathsEx(value, pathBuilder);
foreach (ReadOnlyMemory<char> path in pathBuilder)
{
if (path.IsWhiteSpace())
{
continue;
}
ParseFileArgument(path, baseDirectory, filePathBuilder, errors);
}
pathBuilder.Free();
}
private protected IEnumerable<string> ParseSeparatedFileArgument(string value, string? baseDirectory, IList<Diagnostic> errors)
{
var builder = ArrayBuilder<string>.GetInstance();
ParseSeparatedFileArgument(value.AsMemory(), baseDirectory, builder, errors);
foreach (var filePath in builder)
{
yield return filePath;
}
builder.Free();
}
internal IEnumerable<CommandLineSourceFile> ParseRecurseArgument(string arg, string? baseDirectory, IList<Diagnostic> errors)
{
foreach (var path in ExpandFileNamePattern(arg, baseDirectory, SearchOption.AllDirectories, errors))
{
yield return ToCommandLineSourceFile(path);
}
}
internal static Encoding? TryParseEncodingName(string arg)
{
if (!string.IsNullOrWhiteSpace(arg)
&& long.TryParse(arg, NumberStyles.None, CultureInfo.InvariantCulture, out long codepage)
&& (codepage > 0))
{
try
{
return Encoding.GetEncoding((int)codepage);
}
catch (Exception)
{
return null;
}
}
return null;
}
internal static SourceHashAlgorithm TryParseHashAlgorithmName(string arg)
{
if (string.Equals("sha1", arg, StringComparison.OrdinalIgnoreCase))
{
return SourceHashAlgorithm.Sha1;
}
if (string.Equals("sha256", arg, StringComparison.OrdinalIgnoreCase))
{
return SourceHashAlgorithm.Sha256;
}
// MD5 is legacy, not supported
return SourceHashAlgorithm.None;
}
private IEnumerable<string> ExpandFileNamePattern(
string path,
string? baseDirectory,
SearchOption searchOption,
IList<Diagnostic> errors)
{
string? directory = PathUtilities.GetDirectoryName(path);
string pattern = PathUtilities.GetFileName(path);
var resolvedDirectoryPath = string.IsNullOrEmpty(directory) ?
baseDirectory :
FileUtilities.ResolveRelativePath(directory, baseDirectory);
IEnumerator<string>? enumerator = null;
try
{
bool yielded = false;
// NOTE: Directory.EnumerateFiles(...) surprisingly treats pattern "." the
// same way as "*"; as we don't expect anything to be found by this
// pattern, let's just not search in this case
pattern = pattern.Trim(s_searchPatternTrimChars);
bool singleDotPattern = string.Equals(pattern, ".", StringComparison.Ordinal);
if (!singleDotPattern)
{
while (true)
{
string? resolvedPath = null;
try
{
if (enumerator == null)
{
enumerator = EnumerateFiles(resolvedDirectoryPath, pattern, searchOption).GetEnumerator();
}
if (!enumerator.MoveNext())
{
break;
}
resolvedPath = enumerator.Current;
}
catch
{
resolvedPath = null;
}
if (resolvedPath != null)
{
// just in case EnumerateFiles returned a relative path
resolvedPath = FileUtilities.ResolveRelativePath(resolvedPath, baseDirectory);
}
if (resolvedPath == null)
{
errors.Add(Diagnostic.Create(MessageProvider, (int)MessageProvider.FTL_InvalidInputFileName, path));
break;
}
yielded = true;
yield return resolvedPath;
}
}
// the pattern didn't match any files:
if (!yielded)
{
if (searchOption == SearchOption.AllDirectories)
{
// handling /recurse
GenerateErrorForNoFilesFoundInRecurse(path, errors);
}
else
{
// handling wildcard in file spec
errors.Add(Diagnostic.Create(MessageProvider, (int)MessageProvider.ERR_FileNotFound, path));
}
}
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
}
internal abstract void GenerateErrorForNoFilesFoundInRecurse(string path, IList<Diagnostic> errors);
internal ReportDiagnostic GetDiagnosticOptionsFromRulesetFile(string? fullPath, out Dictionary<string, ReportDiagnostic> diagnosticOptions, IList<Diagnostic> diagnostics)
{
return RuleSet.GetDiagnosticOptionsFromRulesetFile(fullPath, out diagnosticOptions, diagnostics, _messageProvider);
}
/// <summary>
/// Tries to parse a UInt64 from string in either decimal, octal or hex format.
/// </summary>
/// <param name="value">The string value.</param>
/// <param name="result">The result if parsing was successful.</param>
/// <returns>true if parsing was successful, otherwise false.</returns>
internal static bool TryParseUInt64(string? value, out ulong result)
{
result = 0;
if (RoslynString.IsNullOrEmpty(value))
{
return false;
}
int numBase = 10;
if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
numBase = 16;
}
else if (value.StartsWith("0", StringComparison.OrdinalIgnoreCase))
{
numBase = 8;
}
try
{
result = Convert.ToUInt64(value, numBase);
}
catch
{
return false;
}
return true;
}
/// <summary>
/// Tries to parse a UInt16 from string in either decimal, octal or hex format.
/// </summary>
/// <param name="value">The string value.</param>
/// <param name="result">The result if parsing was successful.</param>
/// <returns>true if parsing was successful, otherwise false.</returns>
internal static bool TryParseUInt16(string? value, out ushort result)
{
result = 0;
if (RoslynString.IsNullOrEmpty(value))
{
return false;
}
int numBase = 10;
if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
numBase = 16;
}
else if (value.StartsWith("0", StringComparison.OrdinalIgnoreCase))
{
numBase = 8;
}
try
{
result = Convert.ToUInt16(value, numBase);
}
catch
{
return false;
}
return true;
}
internal static ImmutableDictionary<string, string> ParseFeatures(List<string> features)
{
var builder = ImmutableDictionary.CreateBuilder<string, string>();
CompilerOptionParseUtilities.ParseFeatures(builder, features);
return builder.ToImmutable();
}
/// <summary>
/// Sort so that more specific keys precede less specific.
/// When mapping a path we find the first key in the array that is a prefix of the path.
/// If multiple keys are prefixes of the path we want to use the longest (more specific) one for the mapping.
/// </summary>
internal static ImmutableArray<KeyValuePair<string, string>> SortPathMap(ImmutableArray<KeyValuePair<string, string>> pathMap)
=> pathMap.Sort((x, y) => -x.Key.Length.CompareTo(y.Key.Length));
}
}
|