| File: Microsoft.NetCore.Analyzers\Usage\FileBasedProgramDirectiveQuoting.cs | Web Access |
| Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.CSharp.NetAnalyzers\Microsoft.CodeAnalysis.CSharp.NetAnalyzers.csproj (Microsoft.CodeAnalysis.CSharp.NetAnalyzers) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.DotNet.FileBasedPrograms; using static Microsoft.DotNet.FileBasedPrograms.FileBasedProgramDirectiveValueHelpers; namespace Microsoft.NetCore.CSharp.Analyzers.Usage; /// <summary> /// Shared logic for detecting the deprecated unquoted-whitespace form of a file-based program /// <c>#:</c> directive and for computing its quoted replacement. This mirrors (a conservative /// subset of) the directive parser in <c>Microsoft.DotNet.FileBasedPrograms</c> and reuses that /// parser's value-level primitives (see <see cref="FileBasedProgramDirectiveValueHelpers"/>) for /// quoting, name validity, and metadata detection so the two cannot drift. It flags only directives /// that the parser accepts as the legacy form and that have an unambiguous, semantics-preserving /// quoted equivalent. /// </summary> internal static class FileBasedProgramDirectiveQuoting { /// <summary> /// Extracts the directive kind (e.g. <c>property</c>) and its value text from a file-based program /// <c>#:</c> directive trivia. Returns <see langword="false"/> for any other trivia. /// </summary> public static bool TryParse(SyntaxTrivia trivia, out string kind, out string value, out string valueLeadingWhitespace) { kind = string.Empty; value = string.Empty; valueLeadingWhitespace = string.Empty; // '#:' directives are represented as directive trivia whose structure carries a single // string literal token holding the text after '#:'. Exclude the '#!' shebang explicitly. if (trivia.IsKind(SyntaxKind.ShebangDirectiveTrivia)) { return false; } var structure = trivia.GetStructure(); if (structure is null) { return false; } var content = structure.ChildTokens().FirstOrDefault(static token => token.IsKind(SyntaxKind.StringLiteralToken)); if (!content.IsKind(SyntaxKind.StringLiteralToken)) { return false; } var text = content.Text.Trim(); if (text.Length == 0) { return false; } var whitespaceIndex = IndexOfWhitespace(text); if (whitespaceIndex < 0) { kind = text; value = string.Empty; } else { kind = text.Substring(0, whitespaceIndex); var valueWithLeadingWhitespace = text.Substring(whitespaceIndex); value = valueWithLeadingWhitespace.TrimStart(); valueLeadingWhitespace = valueWithLeadingWhitespace.Substring(0, valueWithLeadingWhitespace.Length - value.Length); } return true; } /// <summary> /// Returns whether the directive uses the deprecated unquoted-whitespace form. /// </summary> public static bool IsLegacyForm(string kind, string value) { // No value, or already quoted (quotes unambiguously mean the new form): nothing to flag. if (value.Length == 0 || value.IndexOf('"') >= 0) { return false; } var tokens = SplitWhitespace(value); // A single whitespace-separated token is unambiguous and never the legacy form. if (tokens.Length <= 1) { return false; } return kind switch { "property" or "sdk" or "include" or "exclude" => true, "package" or "project" or "ref" => !AllValidMetadata(tokens, start: 1), _ => false, }; } /// <summary> /// Computes an equivalent quoted <paramref name="newValue"/> for a legacy directive. /// Returns <see langword="false"/> when the legacy form cannot be rewritten without changing /// its value. /// </summary> public static bool TryGetQuotedForm(string kind, string value, out string newValue) { newValue = value; if (!IsLegacyForm(kind, value)) { return false; } switch (kind) { case "property": return TryQuoteAfterSeparator(value, out newValue); case "sdk": case "package": return TryQuoteVersion(value, out newValue); case "project": case "ref": case "include": case "exclude": newValue = QuoteIfNeeded(value); return true; default: return false; } } private static bool TryQuoteAfterSeparator(string value, out string newValue) { newValue = value; var separatorIndex = value.IndexOf('='); if (separatorIndex < 0) { return false; } var name = value.Substring(0, separatorIndex).TrimEnd(); if (name.Length == 0 || ContainsDisallowedNameCharacter(name)) { return false; } var valueAfterSeparator = value.Substring(separatorIndex + 1); var innerValue = valueAfterSeparator.TrimStart(); var leadingWhitespace = valueAfterSeparator.Substring(0, valueAfterSeparator.Length - innerValue.Length); newValue = value.Substring(0, separatorIndex + 1) + leadingWhitespace + SymbolDisplay.FormatLiteral(innerValue, quote: true); return true; } private static bool TryQuoteVersion(string value, out string newValue) { newValue = value; var separatorIndex = value.IndexOf('@'); if (separatorIndex < 0) { return false; } var name = value.Substring(0, separatorIndex).TrimEnd(); if (name.Length == 0 || ContainsDisallowedNameCharacter(name)) { return false; } // A version with internal whitespace is already invalid and is left alone. var valueAfterSeparator = value.Substring(separatorIndex + 1); var version = valueAfterSeparator.TrimStart(); if (version.Length == 0 || IndexOfWhitespace(version) >= 0) { return false; } var leadingWhitespace = valueAfterSeparator.Substring(0, valueAfterSeparator.Length - version.Length); newValue = value.Substring(0, separatorIndex + 1) + leadingWhitespace + SymbolDisplay.FormatLiteral(version, quote: true); return true; } private static int IndexOfWhitespace(string text) { for (var i = 0; i < text.Length; i++) { if (char.IsWhiteSpace(text[i])) { return i; } } return -1; } private static ImmutableArray<string> SplitWhitespace(string text) { var tokens = ImmutableArray.CreateBuilder<string>(); var start = -1; for (var i = 0; i < text.Length; i++) { if (char.IsWhiteSpace(text[i])) { if (start >= 0) { tokens.Add(text.Substring(start, i - start)); start = -1; } } else if (start < 0) { start = i; } } if (start >= 0) { tokens.Add(text.Substring(start)); } return tokens.ToImmutable(); } }