diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 3e0db6afaadf..56bed21ab1ca 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -179,6 +179,7 @@ which are [ignored][ignored-directives] by the C# language but recognized by the #:property TargetFramework=net11.0 #:property LangVersion=preview #:package System.CommandLine@2.0.0-* +#:package Microsoft.Build@17.0.0 ExcludeAssets=runtime PrivateAssets=all #:project ../MyLibrary #:ref ../lib/lib.cs #:include ./**/*.cs @@ -190,6 +191,32 @@ The value is required for `#:property`, optional for `#:package`/`#:sdk`, and di The name must be separated from the kind of the directive by whitespace and any leading and trailing white space is not considered part of the name and value. +The remainder of a directive (after the kind) is split into whitespace-separated tokens. +Whitespace inside a value is not allowed unless the value is enclosed in double quotes (`"`). +A value is written either bare or wrapped entirely in double quotes. A quoted value is lexed as a +regular C# string literal (the same way `#r`/`#load` directives lex their argument), so its escape +sequences are decoded, e.g., `#:property Description="Hello World"` sets the value to `Hello World`, +`#:property Path="a\\b"` sets it to `a\b`, and `#:property Text="a\"b"` sets it to `a"b`. Verbatim +(`@"..."`) and raw (`"""..."""`) string literals are not supported. Quotes can only enclose a whole +value, so `#:property A=B` and `#:property A="B"` are allowed, but `#:property A=B"C"` is an error. +It is an error if a quote is left unterminated or if a quoted value contains an invalid escape +sequence (e.g., `"a\q"`). + +Because a bare value keeps a backslash literal while a quoted value follows C# escape rules, a Windows +path is simplest written bare (`#:project C:\src\lib`) or with forward slashes if quoting is needed +(`#:project "C:/src/my lib"`); quoting a backslash path requires escaping it (`"C:\\src\\my lib"`). + +For backward compatibility, a directive whose value contains no double quotes is still accepted in a +*legacy mode*: the entire remainder after the name and separator is taken verbatim as a single value +(including any internal whitespace), matching how these directives behaved before quoting and metadata +were supported. Analyzer [CA2267](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2267) +flags such legacy directives and offers a code fix to rewrite them into the quoted form. + +`#:package`, `#:project`, and `#:ref` directives can specify additional MSBuild item metadata as trailing `Name=Value` tokens, +e.g., `#:package Microsoft.Build@17.0.0 ExcludeAssets=runtime PrivateAssets=all`. +Each metadata name must be a valid XML element name; each metadata value can be quoted to contain whitespace. +The other directive kinds do not support trailing metadata and it is an error to specify extra tokens for them. + The directives are processed as follows: - The name and value of the first `#:sdk` is injected into `` (or just `` if it has no value), @@ -201,6 +228,8 @@ The directives are processed as follows: - Each `#:package` is injected as `` (or without the `Version` attribute if it has no value) in an ``. It is an error if its name is empty (the value, i.e., package version, is allowed to be empty, but that results in empty `Version=""`). + Any trailing `Name=Value` metadata is injected as child elements, e.g., + `runtime`. It is valid to have a `#:package` directive without a version. That's useful when central package management (CPM) is used. @@ -208,6 +237,7 @@ The directives are processed as follows: - Each `#:project` is injected as `` in an ``. It is an error if the value is empty. + Any trailing `Name=Value` metadata is injected as child elements of the ``. If the path points to an existing directory, a project file is found inside that directory and its path is used instead (because `ProjectReference` items don't support directory paths). An error is reported if zero or more than one projects are found in the directory, just like `dotnet reference add` would do. @@ -216,6 +246,7 @@ The directives are processed as follows: A virtual project is created for the referenced file (e.g., `lib.cs` produces a virtual `lib.cs.csproj`), and a `` is injected in an ``. It is an error if the name is empty or if the referenced file does not exist. + Any trailing `Name=Value` metadata is injected as child elements of the ``. Unlike `#:project`, `#:ref` points to a `.cs` file (not a `.csproj` file or directory). The referenced file is itself a file-based program with its own virtual project (defaulting to `OutputType=Exe`). diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramDirectiveValueHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramDirectiveValueHelpers.cs new file mode 100644 index 000000000000..c34732101899 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramDirectiveValueHelpers.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Xml; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.DotNet.FileBasedPrograms; + +/// +/// Low-level primitives for parsing and formatting the values of file-based program #: +/// directives. These are source-shared between the CLI directive parser +/// (FileLevelDirectiveHelpers) and the analyzer that flags the deprecated unquoted form +/// (FileBasedProgramDirectiveQuoting), so both agree on quoting, name validity, and metadata +/// detection instead of each duplicating the logic. +/// +internal static class FileBasedProgramDirectiveValueHelpers +{ + // Characters that are not allowed in a directive or metadata name because they would be confused + // with a separator: whitespace, '@', '=', '/'. + private static readonly Regex s_disallowedNameCharacters = new("""[\s@=/]""", RegexOptions.Compiled); + + /// + /// Returns whether contains a character that is not allowed in a directive + /// or metadata name (whitespace or one of the separator characters @, =, /). + /// + public static bool ContainsDisallowedNameCharacter(string name) => s_disallowedNameCharacters.IsMatch(name); + + /// + /// Validates that is a valid XML NCName, the constraint MSBuild applies to + /// property and item-metadata names (an NCName additionally disallows the ':' that a plain XML name + /// permits). Returns when valid; otherwise returns and + /// sets to the underlying validation-failure message. + /// + public static bool IsValidMSBuildName(string name, out string? errorMessage) + { + try + { + XmlConvert.VerifyNCName(name); + errorMessage = null; + return true; + } + catch (XmlException ex) + { + errorMessage = ex.Message; + return false; + } + } + + /// + /// Returns whether every token from onwards is a valid Name=Value + /// item-metadata pair (a valid MSBuild name, then '=', then any value). + /// + public static bool AllValidMetadata(IReadOnlyList tokens, int start) + { + for (var i = start; i < tokens.Count; i++) + { + var token = tokens[i]; + var separatorIndex = token.IndexOf('='); + if (separatorIndex <= 0) + { + return false; + } + + if (!IsValidMSBuildName(token.Substring(0, separatorIndex), out _)) + { + return false; + } + } + + return true; + } + + /// + /// Wraps in a C# string literal when it contains a character (whitespace or + /// a double quote) that cannot appear in a bare directive token, so it round-trips through the parser + /// (which lexes a quoted value as a regular C# string literal). Otherwise returns it unchanged. + /// + public static string QuoteIfNeeded(string value) + { + foreach (var c in value) + { + if (char.IsWhiteSpace(c) || c == '"') + { + // FormatLiteral produces a properly escaped C# string literal (e.g. "a\"b", "a\tb") that + // the parser decodes back to the original value. + return SymbolDisplay.FormatLiteral(value, quote: true); + } + } + + return value; + } +} diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx index ceae23b8f367..dc031cbb9e9a 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx @@ -158,8 +158,32 @@ Duplicate directives are not supported: {0} {0} is the directive type and name. - - Directives currently cannot contain double quotes ("). + + Unterminated double quote (") in directive. + + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + + + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + + + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. The '#:project' directive is invalid: {0} diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs index d5beaac92f67..91cd9a0f53f8 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs @@ -11,12 +11,12 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; -using System.Xml; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using Microsoft.DotNet.ProjectTools; +using static Microsoft.DotNet.FileBasedPrograms.FileBasedProgramDirectiveValueHelpers; namespace Microsoft.DotNet.FileBasedPrograms; @@ -148,12 +148,6 @@ public static void FindLeadingDirectives( DirectiveText = value, }; - // Block quotes now so we can later support quoted values without a breaking change. https://github.com/dotnet/sdk/issues/49367 - if (value.Contains('"')) - { - context.ReportError(FileBasedProgramsResources.QuoteInDirective); - } - if (CSharpDirective.Parse(context) is { } directive) { if (checkDuplicates) @@ -249,8 +243,6 @@ internal static partial class Patterns { public static Regex Whitespace { get; } = new Regex("""\s+""", RegexOptions.Compiled); - public static Regex DisallowedNameCharacters { get; } = new Regex("""[\s@=/]""", RegexOptions.Compiled); - public static Regex EscapedCompilerOption { get; } = new Regex("""^/\w+:".*"$""", RegexOptions.Compiled | RegexOptions.Singleline); } @@ -325,40 +317,306 @@ public void ReportError(TextSpan span, string message) } } - private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, char separator) + /// + /// Splits into whitespace-separated tokens. + /// A value is written either bare or wrapped entirely in double quotes ("), which lets it + /// contain whitespace. A quoted value is lexed as a regular C# string literal (the same way + /// #r/#load lex their argument), so escape sequences like \", \\ and + /// \t are decoded; verbatim (@"...") and raw ("""...""") literals are not + /// supported. A quote may open only at the start of a token (e.g., "a b") or immediately + /// after a single Name= separator (e.g., A="b c"), and nothing may follow the + /// closing quote within the token. So A=B and A="B" are allowed, but A=B"C" + /// and A="B"C are errors. Returns and reports an error if a quote is + /// misplaced or left unterminated. + /// + private static ImmutableArray? Tokenize(in ParseContext context) { - var separatorIndex = context.DirectiveText.IndexOf(separator); - var firstPart = (separatorIndex < 0 ? context.DirectiveText : context.DirectiveText.AsSpan(0, separatorIndex)).TrimEnd(); + var text = context.DirectiveText; + var tokens = ImmutableArray.CreateBuilder(); + var current = new StringBuilder(); + var tokenStarted = false; + var quoteClosed = false; + var equalsCount = 0; + + for (var i = 0; i < text.Length; i++) + { + var c = text[i]; + + if (c == '"') + { + // A quoted value must be the whole token or the value right after a single 'Name=' separator. + var atTokenStart = current.Length == 0; + var afterNameSeparator = current.Length > 0 && current[current.Length - 1] == '=' && equalsCount == 1; + if (quoteClosed || !(atTokenStart || afterNameSeparator)) + { + context.ReportError(FileBasedProgramsResources.InvalidQuoteInDirective); + return null; + } - string directiveKind = context.DirectiveKind; - if (firstPart.IsWhiteSpace()) + // Lex a regular C# string literal (like '#r') so the value can contain whitespace and use + // escape sequences. Verbatim (@"...") literals can't start here (the '@' would precede the + // quote and fail the check above), and raw ("""...""") literals lex to a different token kind + // and are rejected below. + var token = SyntaxFactory.ParseToken(text, offset: i); + var errors = token.GetDiagnostics().Where(static d => d.Severity == DiagnosticSeverity.Error).ToList(); + if (errors.Count > 0) + { + // CS1010 ("Newline in constant") means the literal was left unterminated; give it our + // clearer directive-specific message. Any other lexer error (e.g. CS1009 for an invalid + // escape sequence) forwards Roslyn's already-localized message so it stays accurate. + if (errors.Any(static d => d.Id == "CS1010")) + { + context.ReportError(FileBasedProgramsResources.UnterminatedQuoteInDirective); + } + else + { + context.ReportError(string.Format(FileBasedProgramsResources.InvalidStringLiteralInDirective, errors[0].GetMessage())); + } + + return null; + } + + if (!token.IsKind(SyntaxKind.StringLiteralToken)) + { + // Any token carrying a lexer error was already reported (and Roslyn's diagnostic + // forwarded) above, so the only thing that reaches here is a *well-formed* literal + // that starts with '"' yet isn't a simple string literal. Today that can only be a + // raw string literal ('"""..."""'); verbatim ('@"..."') can't start here because the + // '@' would precede the quote and fail the position check. Raw/verbatim literals are + // intentionally unsupported (we match '#r'/'#load', which accept only a simple string + // literal). Report the actual token text so the message shows the user exactly what was + // wrong, and stays accurate even if a future Roslyn lexer change routes some other kind + // here. + context.ReportError(string.Format(FileBasedProgramsResources.ExpectedSimpleStringLiteralInDirective, token.Text)); + return null; + } + + // The decoded value is appended to the current token (which may already hold a 'Name=' + // prefix); a quote starts a token even if it is empty (e.g., '""' is an empty token). + current.Append(token.ValueText); + tokenStarted = true; + quoteClosed = true; + i += token.Text.Length - 1; + continue; + } + + if (char.IsWhiteSpace(c)) + { + if (tokenStarted) + { + tokens.Add(current.ToString()); + current.Clear(); + tokenStarted = false; + quoteClosed = false; + equalsCount = 0; + } + + continue; + } + + if (quoteClosed) + { + context.ReportError(FileBasedProgramsResources.InvalidQuoteInDirective); + return null; + } + + if (c == '=') + { + equalsCount++; + } + + current.Append(c); + tokenStarted = true; + } + + if (tokenStarted) { - context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + tokens.Add(current.ToString()); + } + + return tokens.ToImmutable(); + } + + /// + /// Tokenizes like for the "new" form + /// (which may use double quotes and/or trailing Name=Value metadata), but falls back to the + /// pre-quoting "legacy" behavior to avoid a breaking change: before quoting and metadata were + /// supported, a directive value could contain unquoted whitespace and was taken verbatim. + /// + /// Rules (double quotes were previously disallowed, so their presence unambiguously means the new form): + /// + /// If the text contains a double quote, it is parsed strictly via . + /// Otherwise, if there is at most one whitespace-separated token, it is returned as-is. + /// Otherwise, the trailing tokens are treated as metadata only when + /// is set and every trailing token is a valid Name=Value pair; then the split tokens are returned. + /// This is unlikely to be a breaking change as it requires a construct like + /// #:package X@1 A=B (which would previously fail because space is disallowed in version) + /// or #:ref ./f.cs A=B (which is unlikely to be a real path). + /// Otherwise the whole (already trimmed) remainder is returned as a single legacy value with its + /// internal whitespace preserved, and is set. The deprecated legacy form is + /// flagged by an analyzer rather than erroring here. + /// + /// + /// + private static ImmutableArray? TokenizeWithLegacyFallback(in ParseContext context, bool allowMetadata, out bool isLegacy) + { + isLegacy = false; + var text = context.DirectiveText; + + // Quoting is the "new" form; parse strictly with full validation once a quote is present. + if (text.IndexOf('"') >= 0) + { + return Tokenize(context); + } + + if (text.Length == 0) + { + return ImmutableArray.Empty; + } + + var rawTokens = Patterns.Whitespace.Split(text); + + // A single token (no internal whitespace) is unambiguous. + if (rawTokens.Length <= 1) + { + return ImmutableArray.Create(rawTokens); + } + + // Multiple unquoted whitespace-separated tokens. Interpret the trailing ones as item metadata + // only when metadata is supported and every trailing token is a valid 'Name=Value' pair. + if (allowMetadata && AllValidMetadata(rawTokens, start: 1)) + { + return ImmutableArray.Create(rawTokens); + } + + // Legacy: the whole remainder is a single value (preserves pre-quoting behavior). + isLegacy = true; + return ImmutableArray.Create(text); + } + + /// + /// Splits the first of into a required name and optional value + /// on the first occurrence of (e.g., Name@Version), + /// validating the name. Used by #:sdk, #:property, and #:package. + /// When is set (legacy form, where the token may contain unquoted whitespace), + /// whitespace adjacent to the separator is trimmed to match the pre-quoting behavior. + /// + private static (string Name, string? Value)? ParseNameAndValue(in ParseContext context, ImmutableArray tokens, char separator, bool trimAroundSeparator = false) + { + if (tokens.Length == 0) + { + context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind)); + return null; + } + + var token = tokens[0]; + var separatorIndex = token.IndexOf(separator); + var name = separatorIndex < 0 ? token : token.Substring(0, separatorIndex); + if (trimAroundSeparator) + { + name = name.TrimEnd(); + } + + if (name.Length == 0) + { + context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind)); return null; } // If the name contains characters that resemble separators, report an error to avoid any confusion. - if (Patterns.DisallowedNameCharacters.Match(context.DirectiveText, beginning: 0, length: firstPart.Length).Success) + if (ContainsDisallowedNameCharacter(name)) { - context.ReportError(string.Format(FileBasedProgramsResources.InvalidDirectiveName, directiveKind, separator)); + context.ReportError(string.Format(FileBasedProgramsResources.InvalidDirectiveName, context.DirectiveKind, separator)); return null; } - if (separatorIndex < 0) + var value = separatorIndex < 0 ? null : token.Substring(separatorIndex + 1); + if (trimAroundSeparator && value is not null) + { + value = value.TrimStart(); + } + + return (name, value); + } + + /// + /// Parses the trailing (starting at ) as + /// Name=Value item metadata pairs. Returns and reports an error + /// if a token is not a valid metadata pair. + /// + private static ImmutableArray<(string Name, string Value)>? ParseMetadata(in ParseContext context, ImmutableArray tokens, int start) + { + if (start >= tokens.Length) { - return (firstPart.ToString(), null); + return []; } - var secondPart = context.DirectiveText.AsSpan(separatorIndex + 1).TrimStart(); - if (secondPart.IsWhiteSpace()) + var builder = ImmutableArray.CreateBuilder<(string Name, string Value)>(tokens.Length - start); + + for (var i = start; i < tokens.Length; i++) { - Debug.Assert(secondPart.Length == 0, - "We have trimmed the second part, so if it's white space, it should be actually empty."); + var token = tokens[i]; + var separatorIndex = token.IndexOf('='); + if (separatorIndex <= 0) + { + context.ReportError(string.Format(FileBasedProgramsResources.InvalidDirectiveMetadata, token)); + return null; + } - return (firstPart.ToString(), string.Empty); + var name = token.Substring(0, separatorIndex); + var value = token.Substring(separatorIndex + 1); + + if (!IsValidMSBuildName(name, out var nameError)) + { + context.ReportError(string.Format(FileBasedProgramsResources.DirectiveMetadataInvalidName, name, nameError)); + return null; + } + + builder.Add((name, value)); + } + + return builder.ToImmutable(); + } + + /// + /// Parses a directive that expects exactly one token (its value) and no metadata. + /// Reports an error and returns on empty or extra tokens. + /// Unquoted whitespace is accepted as part of the value for backward compatibility + /// (see ). + /// + private static string? ParseSingleValue(in ParseContext context) + { + if (TokenizeWithLegacyFallback(context, allowMetadata: false, out _) is not { } tokens) + { + return null; + } + + if (tokens.Length == 0 || tokens[0].Length == 0) + { + context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind)); + return null; + } + + if (tokens.Length > 1) + { + context.ReportError(string.Format(FileBasedProgramsResources.UnexpectedDirectiveText, context.DirectiveKind)); + return null; + } + + return tokens[0]; + } + + private static void AppendMetadata(StringBuilder builder, ImmutableArray<(string Name, string Value)> metadata) + { + if (metadata.IsDefaultOrEmpty) + { + return; } - return (firstPart.ToString(), secondPart.ToString()); + foreach (var (name, value) in metadata) + { + builder.Append(' ').Append(name).Append('=').Append(QuoteIfNeeded(value)); + } } public abstract override string ToString(); @@ -387,7 +645,18 @@ public sealed class Sdk(in ParseInfo info) : Named(info) public static new Sdk? Parse(in ParseContext context) { - if (ParseOptionalTwoParts(context, separator: '@') is not var (sdkName, sdkVersion)) + if (TokenizeWithLegacyFallback(context, allowMetadata: false, out var isLegacy) is not { } tokens) + { + return null; + } + + if (tokens.Length > 1) + { + context.ReportError(string.Format(FileBasedProgramsResources.UnexpectedDirectiveText, context.DirectiveKind)); + return null; + } + + if (ParseNameAndValue(context, tokens, separator: '@', trimAroundSeparator: isLegacy) is not var (sdkName, sdkVersion)) { return null; } @@ -399,7 +668,7 @@ public sealed class Sdk(in ParseInfo info) : Named(info) }; } - public override string ToString() => Version is null ? $"#:sdk {Name}" : $"#:sdk {Name}@{Version}"; + public override string ToString() => Version is null ? $"#:sdk {QuoteIfNeeded(Name)}" : $"#:sdk {QuoteIfNeeded($"{Name}@{Version}")}"; } /// @@ -411,24 +680,31 @@ public sealed class Property(in ParseInfo info) : Named(info) public static new Property? Parse(in ParseContext context) { - if (ParseOptionalTwoParts(context, separator: '=') is not var (propertyName, propertyValue)) + if (TokenizeWithLegacyFallback(context, allowMetadata: false, out var isLegacy) is not { } tokens) { return null; } - if (propertyValue is null) + if (tokens.Length > 1) + { + context.ReportError(string.Format(FileBasedProgramsResources.UnexpectedDirectiveText, context.DirectiveKind)); + return null; + } + + if (ParseNameAndValue(context, tokens, separator: '=', trimAroundSeparator: isLegacy) is not var (propertyName, propertyValue)) { - context.ReportError(FileBasedProgramsResources.PropertyDirectiveMissingParts); return null; } - try + if (propertyValue is null) { - propertyName = XmlConvert.VerifyName(propertyName); + context.ReportError(FileBasedProgramsResources.PropertyDirectiveMissingParts); + return null; } - catch (XmlException ex) + + if (!IsValidMSBuildName(propertyName, out var nameError)) { - context.ReportError(string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, ex.Message)); + context.ReportError(string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, nameError)); return null; } @@ -445,7 +721,7 @@ public sealed class Property(in ParseInfo info) : Named(info) }; } - public override string ToString() => $"#:property {Name}={Value}"; + public override string ToString() => $"#:property {Name}={QuoteIfNeeded(Value)}"; } /// @@ -455,9 +731,25 @@ public sealed class Package(in ParseInfo info) : Named(info) { public string? Version { get; init; } + /// + /// Additional item metadata specified as trailing Name=Value pairs, + /// e.g. #:package Foo@1.0.0 ExcludeAssets=runtime PrivateAssets=all. + /// + public ImmutableArray<(string Name, string Value)> Metadata { get; init; } + public static new Package? Parse(in ParseContext context) { - if (ParseOptionalTwoParts(context, separator: '@') is not var (packageName, packageVersion)) + if (TokenizeWithLegacyFallback(context, allowMetadata: true, out var isLegacy) is not { } tokens) + { + return null; + } + + if (ParseNameAndValue(context, tokens, separator: '@', trimAroundSeparator: isLegacy) is not var (packageName, packageVersion)) + { + return null; + } + + if (ParseMetadata(context, tokens, start: 1) is not { } metadata) { return null; } @@ -466,10 +758,17 @@ public sealed class Package(in ParseInfo info) : Named(info) { Name = packageName, Version = packageVersion, + Metadata = metadata, }; } - public override string ToString() => Version is null ? $"#:package {Name}" : $"#:package {Name}@{Version}"; + public override string ToString() + { + var builder = new StringBuilder("#:package "); + builder.Append(QuoteIfNeeded(Version is null ? Name : $"{Name}@{Version}")); + AppendMetadata(builder, Metadata); + return builder.ToString(); + } } /// @@ -502,16 +801,31 @@ public Project(in ParseInfo info, string name) : base(info) /// public string? ProjectFilePath { get; init; } + /// + /// Additional item metadata specified as trailing Name=Value pairs, + /// e.g. #:project ../MyLibrary Private=false. + /// + public ImmutableArray<(string Name, string Value)> Metadata { get; init; } + public static new Project? Parse(in ParseContext context) { - var directiveText = context.DirectiveText; - if (directiveText.IsWhiteSpace()) + if (TokenizeWithLegacyFallback(context, allowMetadata: true, out _) is not { } tokens) + { + return null; + } + + if (tokens is not [{ Length: > 0 } firstToken, ..]) { context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind)); return null; } - return new Project(context.Info, directiveText); + if (ParseMetadata(context, tokens, start: 1) is not { } metadata) + { + return null; + } + + return new Project(context.Info, firstToken) { Metadata = metadata }; } public enum NameKind @@ -539,6 +853,7 @@ public Project WithName(string name, NameKind kind) OriginalName = OriginalName, ExpandedName = kind == NameKind.Expanded ? name : ExpandedName, ProjectFilePath = kind == NameKind.ProjectFilePath ? name : ProjectFilePath, + Metadata = Metadata, }; } @@ -581,7 +896,13 @@ void ReportError(string message) => errorReporter(Info.SourceFile.Text, sourcePath, Info.Span, message); } - public override string ToString() => $"#:project {Name}"; + public override string ToString() + { + var builder = new StringBuilder("#:project "); + builder.Append(QuoteIfNeeded(Name)); + AppendMetadata(builder, Metadata); + return builder.ToString(); + } } /// @@ -614,16 +935,31 @@ public Ref(in ParseInfo info, string name) : base(info) /// public string? ResolvedPath { get; init; } + /// + /// Additional item metadata specified as trailing Name=Value pairs, + /// e.g. #:ref ../lib/lib.cs Aliases=lib. + /// + public ImmutableArray<(string Name, string Value)> Metadata { get; init; } + public static new Ref? Parse(in ParseContext context) { - var directiveText = context.DirectiveText; - if (directiveText.IsWhiteSpace()) + if (TokenizeWithLegacyFallback(context, allowMetadata: true, out _) is not { } tokens) + { + return null; + } + + if (tokens is not [{ Length: > 0 } firstToken, ..]) { context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind)); return null; } - return new Ref(context.Info, directiveText); + if (ParseMetadata(context, tokens, start: 1) is not { } metadata) + { + return null; + } + + return new Ref(context.Info, firstToken) { Metadata = metadata }; } public enum NameKind @@ -651,6 +987,7 @@ public Ref WithName(string name, NameKind kind) OriginalName = OriginalName, ExpandedName = kind == NameKind.Expanded ? name : ExpandedName, ResolvedPath = kind == NameKind.Resolved ? name : ResolvedPath, + Metadata = Metadata, }; } @@ -675,7 +1012,13 @@ public Ref EnsureResolvedPath(ErrorReporter errorReporter) return WithName(resolvedFilePath, NameKind.Resolved); } - public override string ToString() => $"#:ref {Name}"; + public override string ToString() + { + var builder = new StringBuilder("#:ref "); + builder.Append(QuoteIfNeeded(Name)); + AppendMetadata(builder, Metadata); + return builder.ToString(); + } } public enum IncludeOrExcludeKind @@ -725,18 +1068,15 @@ public sealed class IncludeOrExclude(in ParseInfo info) : Named(info) public static new IncludeOrExclude? Parse(in ParseContext context) { - var directiveText = context.DirectiveText; - if (directiveText.IsWhiteSpace()) + if (ParseSingleValue(context) is not { } value) { - string directiveKind = context.DirectiveKind; - context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); return null; } return new IncludeOrExclude(context.Info) { - OriginalName = directiveText, - Name = directiveText, + OriginalName = value, + Name = value, Kind = KindFromString(context.DirectiveKind), }; } @@ -822,7 +1162,7 @@ public string KindToMSBuildString() }; } - public override string ToString() => $"#:{KindToString()} {Name}"; + public override string ToString() => $"#:{KindToString()} {QuoteIfNeeded(Name)}"; /// /// Parses a in the format .protobuf=Protobuf;.cshtml=Content. @@ -934,7 +1274,8 @@ private static bool HasSameValue(CSharpDirective.Named existingDirective, CSharp (CSharpDirective.Property existing, CSharpDirective.Property current) => string.Equals(existing.Value, current.Value, StringComparison.Ordinal), (CSharpDirective.Package existing, CSharpDirective.Package current) => - string.Equals(existing.Version, current.Version, StringComparison.Ordinal), + string.Equals(existing.Version, current.Version, StringComparison.Ordinal) && + existing.Metadata.SequenceEqual(current.Metadata), _ => false, }; } diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt index 24d1392569e7..1582d3b0ae43 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt @@ -82,6 +82,8 @@ Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Named.Name.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Named.Named(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Package(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Metadata.get -> System.Collections.Immutable.ImmutableArray<(string! Name, string! Value)> +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Metadata.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Version.get -> string? Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Version.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext @@ -112,6 +114,8 @@ Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.EnsureProjectFilePath(Microsoft.DotNet.FileBasedPrograms.ErrorReporter! errorReporter) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project! Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ExpandedName.get -> string? Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ExpandedName.init -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Metadata.get -> System.Collections.Immutable.ImmutableArray<(string! Name, string! Value)> +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Metadata.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind.Expanded = 1 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind.Final = 3 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind @@ -130,6 +134,8 @@ Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.EnsureResolvedPath(Microsoft.DotNet.FileBasedPrograms.ErrorReporter! errorReporter) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref! Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ExpandedName.get -> string? Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.ExpandedName.init -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.Metadata.get -> System.Collections.Immutable.ImmutableArray<(string! Name, string! Value)> +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.Metadata.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind.Expanded = 1 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind.Final = 3 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.NameKind @@ -153,6 +159,7 @@ Microsoft.DotNet.FileBasedPrograms.ErrorReporter Microsoft.DotNet.FileBasedPrograms.ErrorReporters Microsoft.DotNet.FileBasedPrograms.ExternalHelpers Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.ExternalHelpers() -> void +Microsoft.DotNet.FileBasedPrograms.FileBasedProgramDirectiveValueHelpers Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers Microsoft.DotNet.FileBasedPrograms.MSBuildUtilities Microsoft.DotNet.FileBasedPrograms.MSBuildUtilities.MSBuildUtilities() -> void @@ -210,6 +217,10 @@ static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Parse(in Micr static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Ref? static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk? static Microsoft.DotNet.FileBasedPrograms.ErrorReporters.CreateCollectingReporter(out System.Collections.Immutable.ImmutableArray.Builder! builder) -> Microsoft.DotNet.FileBasedPrograms.ErrorReporter! +static Microsoft.DotNet.FileBasedPrograms.FileBasedProgramDirectiveValueHelpers.AllValidMetadata(System.Collections.Generic.IReadOnlyList! tokens, int start) -> bool +static Microsoft.DotNet.FileBasedPrograms.FileBasedProgramDirectiveValueHelpers.ContainsDisallowedNameCharacter(string! name) -> bool +static Microsoft.DotNet.FileBasedPrograms.FileBasedProgramDirectiveValueHelpers.IsValidMSBuildName(string! name, out string? errorMessage) -> bool +static Microsoft.DotNet.FileBasedPrograms.FileBasedProgramDirectiveValueHelpers.QuoteIfNeeded(string! value) -> string! static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.CombineHashCodes(int value1, int value2) -> int static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.GetRelativePath(string! relativeTo, string! path) -> string! static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.IsPathFullyQualified(string! path) -> bool @@ -217,7 +228,6 @@ static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.CreateTokeni static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.FindDirectives(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, bool reportAllErrors, Microsoft.DotNet.FileBasedPrograms.ErrorReporter! errorReporter, bool checkDuplicates = true) -> System.Collections.Immutable.ImmutableArray static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.FindLeadingDirectives(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.CodeAnalysis.SyntaxTriviaList triviaList, Microsoft.DotNet.FileBasedPrograms.ErrorReporter! errorReporter, System.Collections.Immutable.ImmutableArray.Builder? builder, bool checkDuplicates = true) -> void static Microsoft.DotNet.FileBasedPrograms.MSBuildUtilities.ConvertStringToBool(string? parameterValue, bool defaultValue = false) -> bool -static Microsoft.DotNet.FileBasedPrograms.Patterns.DisallowedNameCharacters.get -> System.Text.RegularExpressions.Regex! static Microsoft.DotNet.FileBasedPrograms.Patterns.EscapedCompilerOption.get -> System.Text.RegularExpressions.Regex! static Microsoft.DotNet.FileBasedPrograms.Patterns.Whitespace.get -> System.Text.RegularExpressions.Regex! static Microsoft.DotNet.FileBasedPrograms.SourceFile.Load(string! filePath) -> Microsoft.DotNet.FileBasedPrograms.SourceFile diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/VirtualProjectBuilder.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/VirtualProjectBuilder.cs index e01aeaa06b80..a42de16ec783 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/VirtualProjectBuilder.cs +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/VirtualProjectBuilder.cs @@ -860,18 +860,11 @@ internal static void WriteProjectFile( foreach (var package in packageDirectives) { - if (package.Version is null) - { - writer.WriteLine($""" - - """); - } - else - { - writer.WriteLine($""" - - """); - } + string attributes = package.Version is null + ? $"Include=\"{EscapeValue(package.Name)}\"" + : $"Include=\"{EscapeValue(package.Name)}\" Version=\"{EscapeValue(package.Version)}\""; + + WriteItem(writer, "PackageReference", attributes, package.Metadata); processedDirectives++; } @@ -890,9 +883,7 @@ internal static void WriteProjectFile( foreach (var projectReference in projectDirectives) { - writer.WriteLine($""" - - """); + WriteItem(writer, "ProjectReference", $"Include=\"{EscapeValue(projectReference.Name)}\"", projectReference.Metadata); processedDirectives++; } @@ -902,9 +893,8 @@ internal static void WriteProjectFile( if (refDirective.ResolvedPath is not null) { var virtualProjectPath = GetVirtualProjectPath(refDirective.ResolvedPath); - writer.WriteLine($""" - - """); + var attributes = $"Include=\"{EscapeValue(virtualProjectPath)}\" {FromRefDirectiveMetadataName}=\"{EscapeValue(refDirective.ResolvedPath)}\""; + WriteItem(writer, "ProjectReference", attributes, refDirective.Metadata); } processedDirectives++; @@ -966,6 +956,23 @@ internal static void WriteProjectFile( static string EscapeValue(string value) => SecurityElement.Escape(value); + static void WriteItem(TextWriter writer, string itemType, string attributes, ImmutableArray<(string Name, string Value)> metadata) + { + if (metadata.IsDefaultOrEmpty) + { + writer.WriteLine($" <{itemType} {attributes} />"); + return; + } + + writer.WriteLine($" <{itemType} {attributes}>"); + foreach (var (name, value) in metadata) + { + writer.WriteLine($" <{name}>{EscapeValue(value)}"); + } + + writer.WriteLine($" "); + } + static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk sdk) { if (sdk.Version is null) diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf index c6343dbb1e37..cfee65ccf421 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf @@ -27,6 +27,11 @@ chyba Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} Duplicitní direktivy nejsou podporovány: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Direktiva by měla obsahovat název bez speciálních znaků a volitelnou hodnotu oddělenou znakem {1}, například #:{0} Název{1}Hodnota. @@ -77,11 +92,21 @@ Direktiva #:project je neplatná: {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} Direktiva #:ref je neplatná: {0}. {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. Chybí název pro: {0}. @@ -102,21 +127,26 @@ Direktiva property musí mít dvě části oddělené znakem =, například #:property PropertyName=PropertyValue. {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - Direktivy v současné době nemůžou obsahovat dvojité uvozovky ("). - - Static graph restore is not supported for file-based apps. Remove the '#:property'. Statické obnovení grafu se pro souborové aplikace nepodporuje. Odeberte #:property. {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. Nerozpoznaná direktiva {0}. {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf index efcf50901d8f..6a9787bf33a2 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf @@ -27,6 +27,11 @@ Fehler Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} Doppelte Anweisungen werden nicht unterstützt: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Die Anweisung sollte einen Namen ohne Sonderzeichen und einen optionalen Wert enthalten, die durch „{1}“ getrennt sind, wie „#:{0} Name{1}Wert“. @@ -77,11 +92,21 @@ Die Anweisung „#:p roject“ ist ungültig: {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} Die „#:ref“-Direktive ist ungültig: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. Fehlender Name der Anweisung „{0}“. @@ -102,21 +127,26 @@ Die Eigenschaftsanweisung muss zwei durch „=“ getrennte Teile aufweisen, z. B. „#:property PropertyName=PropertyValue“. {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - Direktiven dürfen derzeit keine doppelten Anführungszeichen (") enthalten. - - Static graph restore is not supported for file-based apps. Remove the '#:property'. Die Statische Graphwiederherstellung wird für dateibasierte Apps nicht unterstützt. Entfernen Sie '#:property'. {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. Unbekannte Anweisung „{0}“. {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf index cae5bfafdbb9..a4975598e8cc 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf @@ -27,6 +27,11 @@ error Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} No se admiten directivas duplicadas: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. La directiva debe contener un nombre sin caracteres especiales y un valor opcional separado por "{1}" como "#:{0} Nombre{1}Valor". @@ -77,11 +92,21 @@ La directiva "#:project" no es válida: {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} La directiva "#:ref" no es válida: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. Falta el nombre de "{0}". @@ -102,21 +127,26 @@ La directiva de propiedad debe tener dos partes separadas por "=", como "#:property PropertyName=PropertyValue". {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - Las directivas no pueden contener comillas dobles ("), por ahora. - - Static graph restore is not supported for file-based apps. Remove the '#:property'. No se admite la restauración de gráficos estáticos para aplicaciones basadas en archivos. Elimine "#:property". {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. Directiva no reconocida "{0}". {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf index 684b666ca439..4a2e6c42bb9b 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf @@ -27,6 +27,11 @@ erreur Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} Les directives dupliquées ne sont pas prises en charge : {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. La directive dans doit contenir un nom sans caractères spéciaux et une valeur facultative séparée par « {1} » comme « # :{0} Nom{1}Valeur ». @@ -77,11 +92,21 @@ La directive « #:project » n’est pas valide : {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} La directive « #:ref » est invalide : {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. Nom manquant pour « {0} ». @@ -102,21 +127,26 @@ La directive de propriété doit avoir deux parties séparées par '=' comme '#:property PropertyName=PropertyValue'. {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - Les directives ne peuvent actuellement pas contenir de guillemets doubles ("). - - Static graph restore is not supported for file-based apps. Remove the '#:property'. La restauration de graphique statique n’est pas prise en charge pour les applications basées sur des fichiers. Supprimer la « #:property ». {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. Directive « {0} » non reconnue. {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf index f9fb1f2c4bc5..3ce5ec370ab3 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf @@ -27,6 +27,11 @@ errore Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} Le direttive duplicate non supportate: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. La direttiva deve contenere un nome senza caratteri speciali e un valore facoltativo delimitato da '{1}' come '#:{0}Nome {1}Valore'. @@ -77,11 +92,21 @@ La direttiva '#:project' non è valida: {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} La direttiva "#:ref" non è valida: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. Manca il nome di '{0}'. @@ -102,21 +127,26 @@ La direttiva di proprietà deve avere due parti delimitate da '=', come '#:property PropertyName=PropertyValue'. {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - Le direttive attualmente non possono contenere virgolette doppie ("). - - Static graph restore is not supported for file-based apps. Remove the '#:property'. Il ripristino statico del grafo non è supportato per le app basate su file. Rimuovere '#:property'. {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. Direttiva non riconosciuta '{0}'. {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf index 391a0d8d7042..cbba694f1474 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf @@ -27,6 +27,11 @@ エラー Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} 重複するディレクティブはサポートされていません: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. ディレクティブには、特殊文字を含まない名前と、'#:{0} Name{1}Value' などの '{1}' で区切られた省略可能な値を含める必要があります。 @@ -77,11 +92,21 @@ '#:p roject' ディレクティブが無効です: {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} '#:ref' ディレクティブが無効です: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. '{0}' の名前がありません。 @@ -102,21 +127,26 @@ プロパティ ディレクティブには、'#:property PropertyName=PropertyValue' のように '=' で区切られた 2 つの部分が必要です。 {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - ディレクティブには二重引用符 (") を含めることはできません。 - - Static graph restore is not supported for file-based apps. Remove the '#:property'. 静的グラフの復元はファイルベースのアプリではサポートされていません。'#:property' を削除します。 {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. 認識されないディレクティブ '{0}' です。 {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf index 15567ec5d845..a6f1dcbeeae0 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf @@ -27,6 +27,11 @@ 오류 Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} 중복 지시문은 지원되지 않습니다. {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. 지시문에는 특수 문자가 없는 이름과 '#:{0} 이름{1}값'과 같이 '{1}'(으)로 구분된 선택적 값이 포함되어야 합니다. @@ -77,11 +92,21 @@ '#:p roject' 지시문이 잘못되었습니다. {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} ‘#:ref’ 지시문이 잘못되었습니다: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. '{0}' 이름이 없습니다. @@ -102,21 +127,26 @@ property 지시문에는 '#:property PropertyName=PropertyValue'와 같이 '='로 구분된 두 부분이 있어야 합니다. {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - 지시문은 현재 큰따옴표(")를 포함할 수 없습니다. - - Static graph restore is not supported for file-based apps. Remove the '#:property'. 정적 그래프 복원은 파일 기반 앱에서 지원되지 않습니다. '#:property'를 제거합니다. {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. 인식할 수 없는 지시문 '{0}'입니다. {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf index bcac23cdd47f..a3523df2b241 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf @@ -27,6 +27,11 @@ błąd Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} Zduplikowane dyrektywy nie są obsługiwane: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Dyrektywa powinna zawierać nazwę bez znaków specjalnych i opcjonalną wartość rozdzieloną znakiem "{1}#:{0} Name{1}Value". @@ -77,11 +92,21 @@ Dyrektywa „#:project” jest nieprawidłowa: {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} Dyrektywa „#:ref” jest nieprawidłowa: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. Brak nazwy „{0}”. @@ -102,21 +127,26 @@ Dyrektywa właściwości musi mieć dwie części oddzielone znakiem „=”, na przykład „#:property PropertyName=PropertyValue”. {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - Dyrektywy nie mogą obecnie zawierać podwójnych cudzysłowów ("). - - Static graph restore is not supported for file-based apps. Remove the '#:property'. Przywracanie statycznego grafu nie jest obsługiwane w przypadku aplikacji opartych na plikach. Usuń element „#:property”. {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. Nierozpoznana dyrektywa „{0}”. {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf index 1c790e20764a..a7a0568aab7c 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf @@ -27,6 +27,11 @@ erro Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} Diretivas duplicadas não são suportadas:{0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. A diretiva deve conter um nome sem caracteres especiais e um valor opcional separado por '{1}' como '#:{0} Nome{1}Valor'. @@ -77,11 +92,21 @@ A diretiva '#:project' é inválida:{0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} A diretiva ''#:ref'' é inválida: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. Nome de '{0}' ausente. @@ -102,21 +127,26 @@ A diretiva de propriedade precisa ter duas partes separadas por '=' como '#:property PropertyName=PropertyValue'. {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - No momento, as diretivas não podem conter aspas duplas ("). - - Static graph restore is not supported for file-based apps. Remove the '#:property'. A restauração de grafo estático não é suportada para aplicativos baseados em arquivos. Remova '#:property'. {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. Diretiva não reconhecida '{0}'. {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf index 01584cb58123..40dadbd09e5e 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf @@ -27,6 +27,11 @@ ошибка Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} Повторяющиеся директивы не поддерживаются: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Директива должна содержать имя без специальных символов и необязательное значение, разделенные символом-разделителем "{1}", например "#:{0} Имя{1}Значение". @@ -77,11 +92,21 @@ Недопустимая директива "#:project": {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} Недопустимая директива "#:ref": {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. Отсутствует имя "{0}". @@ -102,21 +127,26 @@ Директива свойства должна иметь две части, разделенные символом "=", например "#:property PropertyName=PropertyValue". {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - В директивах пока нельзя использовать двойные кавычки ("). - - Static graph restore is not supported for file-based apps. Remove the '#:property'. Восстановление статического графа не поддерживается для приложений на основе файлов. Удалите "#:property". {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. Нераспознанная директива "{0}". {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf index f9afec6646d3..2b626051e890 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf @@ -27,6 +27,11 @@ hata Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} Yinelenen yönergeler desteklenmez: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Yönerge, özel karakterler içermeyen bir ad ve ‘#:{0} Ad{1}Değer’ gibi '{1}' ile ayrılmış isteğe bağlı bir değer içermelidir. @@ -77,11 +92,21 @@ ‘#:project’ yönergesi geçersizdir: {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} '#:ref' yönergesi geçersiz: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. '{0}' adı eksik. @@ -102,21 +127,26 @@ Özellik yönergesi, ‘#:property PropertyName=PropertyValue’ gibi ‘=’ ile ayrılmış iki bölümden oluşmalıdır. {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - Yönergeler şu anda çift tırnak (") içeremez. - - Static graph restore is not supported for file-based apps. Remove the '#:property'. Dosya tabanlı uygulamalar için statik grafik geri yükleme desteklenmemektedir. ‘#:property’i kaldırın. {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. Tanınmayan yönerge '{0}'. {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf index 813e15cedb45..891d5355041e 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf @@ -27,6 +27,11 @@ 错误 Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} 不支持重复指令: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. 该指令应包含一个不带特殊字符的名称,以及一个以 '#:{0} Name{1}Value' 等 ‘{1}’ 分隔的可选值。 @@ -77,11 +92,21 @@ '#:project' 指令无效: {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} "#:ref" 指令无效: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. 缺少 '{0}' 的名称。 @@ -102,21 +127,26 @@ 属性指令需要包含两个由 ‘=’ 分隔的部件,例如 '#:property PropertyName=PropertyValue'。 {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - 指令当前不能包含双引号(")。 - - Static graph restore is not supported for file-based apps. Remove the '#:property'. 基于文件的应用不支持静态图形还原。移除 '#:property'。 {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. 无法识别的指令 ‘{0}’。 {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf index 784fd58c51f3..5db9efaa5c2a 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf @@ -27,6 +27,11 @@ 錯誤 Used when reporting directive errors like "file(location): error: message". + + Invalid directive metadata name '{0}': {1} + Invalid directive metadata name '{0}': {1} + {0} is the metadata name. {1} is the inner exception message. + Duplicate directives are not supported: {0} 不支援重複的指示詞: {0} @@ -37,6 +42,11 @@ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + Expected a simple string literal in the directive value. Raw and verbatim string literals are not supported in directive values. Found: {0} + {0} is the offending C# token text, for example '"""abc"""'. + This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it. @@ -52,6 +62,11 @@ File included via #:include directive (or Compile item) not found: {0} {Locked="#:include"}{Locked="Compile"}. {0} is file path. + + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + Directive metadata must be in the form 'Name=Value'. Invalid metadata: '{0}'. + {Locked="'Name=Value'"}{0} is the offending metadata text. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. 指示詞應包含不含特殊字元的名稱,以及 '{1}' 分隔的選用值,例如 '#:{0} Name{1}Value'。 @@ -77,11 +92,21 @@ '#:project' 指示詞無效: {0} {0} is the inner error message. + + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + Double quotes (") in a directive must enclose an entire value, for example: 'Name="a b"' or '"a b"'. + {Locked="Name="a b""}{Locked=""a b""} + The '#:ref' directive is invalid: {0} '#:ref' 指示詞無效: {0} {Locked="#:ref"}{0} is the inner error message. + + Invalid quoted value in directive: {0} + Invalid quoted value in directive: {0} + {0} is the underlying C# string literal error message, for example 'Unrecognized escape sequence.'. + Missing name of '{0}'. 缺少 '{0}' 的名稱。 @@ -102,21 +127,26 @@ 屬性指示詞必須有兩個部分,其以 '=' 分隔,例如 '#:property PropertyName=PropertyValue'。 {Locked="#:property"} - - Directives currently cannot contain double quotes ("). - 指令目前不能包含雙引號 (")。 - - Static graph restore is not supported for file-based apps. Remove the '#:property'. 檔案型應用程式不支援靜態圖表還原。移除 ''#:property'。 {Locked="#:property"} + + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + The '{0}' directive has unexpected content. To include whitespace in a value, enclose it in double quotes ("). + {0} is the directive kind like 'property' or 'sdk'. + Unrecognized directive '{0}'. 無法識別的指示詞 '{0}'。 {0} is the directive name like 'package' or 'sdk'. + + Unterminated double quote (") in directive. + Unterminated double quote (") in directive. + + \ No newline at end of file diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs index 7b9bcebc48d5..a3b0df0b9f7f 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs @@ -539,6 +539,7 @@ ImmutableArray UpdateDirectives(ImmutableArray result.Add(new CSharpDirective.Project(refDirective.Info, relativePath) { OriginalName = refDirective.OriginalName, + Metadata = refDirective.Metadata, }); continue; } diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.csproj b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.csproj index 3a8b01c49130..f7d72d8ab124 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.csproj +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.csproj @@ -17,6 +17,10 @@ + + + + diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/CSharpPreferQuotedFileBasedProgramDirective.Fixer.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/CSharpPreferQuotedFileBasedProgramDirective.Fixer.cs new file mode 100644 index 000000000000..b162d03e62d0 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/CSharpPreferQuotedFileBasedProgramDirective.Fixer.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.NetCore.Analyzers; +using Microsoft.NetCore.Analyzers.Usage; + +namespace Microsoft.NetCore.CSharp.Analyzers.Usage +{ + [ExportCodeFixProvider(LanguageNames.CSharp), Shared] + public sealed class CSharpPreferQuotedFileBasedProgramDirectiveFixer : PreferQuotedFileBasedProgramDirectiveFixer + { + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + var trivia = root.FindTrivia(context.Span.Start); + if (!FileBasedProgramDirectiveQuoting.TryParse(trivia, out var kind, out var value) || + !FileBasedProgramDirectiveQuoting.TryGetQuotedForm(kind, value, out var newValue)) + { + return; + } + + var triviaSpan = trivia.Span; + var newDirectiveText = "#:" + kind + " " + newValue; + + var codeAction = CodeAction.Create( + MicrosoftNetCoreAnalyzersResources.PreferQuotedFileBasedProgramDirectiveCodeFixTitle, + async ct => + { + var text = await context.Document.GetTextAsync(ct).ConfigureAwait(false); + return context.Document.WithText(text.Replace(triviaSpan, newDirectiveText)); + }, + nameof(MicrosoftNetCoreAnalyzersResources.PreferQuotedFileBasedProgramDirectiveCodeFixTitle)); + context.RegisterCodeFix(codeAction, context.Diagnostics); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/CSharpPreferQuotedFileBasedProgramDirective.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/CSharpPreferQuotedFileBasedProgramDirective.cs new file mode 100644 index 000000000000..bebba9299ca4 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/CSharpPreferQuotedFileBasedProgramDirective.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Analyzer.Utilities.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.NetCore.Analyzers.Usage; + +namespace Microsoft.NetCore.CSharp.Analyzers.Usage +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class CSharpPreferQuotedFileBasedProgramDirective : PreferQuotedFileBasedProgramDirective + { + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSyntaxTreeAction(context => + { + var root = context.Tree.GetRoot(context.CancellationToken); + foreach (var trivia in root.GetLeadingTrivia()) + { + if (!FileBasedProgramDirectiveQuoting.TryParse(trivia, out var kind, out var value)) + { + continue; + } + + if (!FileBasedProgramDirectiveQuoting.TryGetQuotedForm(kind, value, out _)) + { + continue; + } + + context.ReportDiagnostic(trivia.GetLocation().CreateDiagnostic(Rule, kind)); + } + }); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/FileBasedProgramDirectiveQuoting.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/FileBasedProgramDirectiveQuoting.cs new file mode 100644 index 000000000000..77690c6c0c57 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/FileBasedProgramDirectiveQuoting.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.DotNet.FileBasedPrograms; +using static Microsoft.DotNet.FileBasedPrograms.FileBasedProgramDirectiveValueHelpers; + +namespace Microsoft.NetCore.CSharp.Analyzers.Usage +{ + /// + /// Shared logic for detecting the deprecated unquoted-whitespace form of a file-based program + /// #: directive and for computing its quoted replacement. This mirrors (a conservative + /// subset of) the directive parser in Microsoft.DotNet.FileBasedPrograms and reuses that + /// parser's value-level primitives (see ) 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. + /// + internal static class FileBasedProgramDirectiveQuoting + { + /// + /// Extracts the directive kind (e.g. property) and its value text from a file-based program + /// #: directive trivia. Returns for any other trivia. + /// + public static bool TryParse(SyntaxTrivia trivia, out string kind, out string value) + { + kind = string.Empty; + value = 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); + value = text.Substring(whitespaceIndex).TrimStart(); + } + + return true; + } + + /// + /// Returns whether the directive uses the deprecated unquoted-whitespace form and, if so, + /// computes the equivalent quoted (the text that should follow the + /// directive kind). + /// + public static bool TryGetQuotedForm(string kind, string value, out string newValue) + { + newValue = 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.Count <= 1) + { + return false; + } + + switch (kind) + { + case "property": + // Value after the first '='; the name must be valid so this is deprecated (not invalid). + return TryQuoteAfterSeparator(value, out newValue); + + case "sdk": + case "package": + // A trailing run of valid 'Name=Value' tokens is the new metadata form, not legacy. + if (kind == "package" && AllValidMetadata(tokens, start: 1)) + { + return false; + } + + return TryCollapseNameAndVersion(value, out newValue); + + case "project": + case "ref": + if (AllValidMetadata(tokens, start: 1)) + { + return false; + } + + newValue = QuoteIfNeeded(value); + return true; + + 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 innerValue = value.Substring(separatorIndex + 1).TrimStart(); + newValue = name + "=" + QuoteIfNeeded(innerValue); + return true; + } + + private static bool TryCollapseNameAndVersion(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; + } + + // The version follows '@'; the parser does not allow quoting there, so a version with internal + // whitespace has no valid quoted form and is left alone (it is a broken version anyway). + var version = value.Substring(separatorIndex + 1).TrimStart(); + if (version.Length == 0 || IndexOfWhitespace(version) >= 0) + { + return false; + } + + newValue = name + "@" + version; + 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 List SplitWhitespace(string text) + { + var tokens = new List(); + 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; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md index 7ad294c26c92..8936fb0fb892 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -2826,6 +2826,18 @@ When a file-based program consists of multiple files, the entry point file shoul |CodeFix|True| --- +## [CA2267](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2267): Quote whitespace in file-based program directive values + +Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + +|Item|Value| +|-|-| +|Category|Usage| +|Enabled|True| +|Severity|Warning| +|CodeFix|True| +--- + ## [CA2300](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2300): Do not use insecure deserializer BinaryFormatter The method '{0}' is insecure when deserializing untrusted data. If you need to instead detect BinaryFormatter deserialization without a SerializationBinder set, then disable rule CA2300, and enable rules CA2301 and CA2302. diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif.template b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif.template index d47ec84d407a..2adfb8188574 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif.template +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif.template @@ -608,6 +608,25 @@ ] } }, + "CA2267": { + "id": "CA2267", + "shortDescription": "Quote whitespace in file-based program directive values", + "fullDescription": "Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously.", + "defaultLevel": "warning", + "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2267", + "properties": { + "category": "Usage", + "isEnabledByDefault": true, + "typeName": "CSharpPreferQuotedFileBasedProgramDirective", + "languages": [ + "C#" + ], + "tags": [ + "Telemetry", + "EnabledRuleInAggressiveMode" + ] + } + }, "CA2352": { "id": "CA2352", "shortDescription": "Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks", diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md index 4054f549ae6b..65754c81d850 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md @@ -10,3 +10,4 @@ CA1877 | Performance | Info | CollapseMultiplePathOperationsAnalyzer, [Documenta CA2026 | Reliability | Info | PreferJsonElementParse, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026) CA2027 | Reliability | Info | DoNotUseNonCancelableTaskDelayWithWhenAny, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2027) CA2028 | Reliability | Info | AvoidRedundantRegexIsMatchBeforeMatch, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2028) +CA2267 | Usage | Warning | PreferQuotedFileBasedProgramDirective, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2267) diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index 256dd41ce7bd..c907ed2f44c6 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -2300,6 +2300,18 @@ Widening and user defined conversions are not supported with generic types. Add '#!' (shebang) + + Quote whitespace in file-based program directive values + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + Add quotes around the directive value + Collapse consecutive Path.Combine or Path.Join operations diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/PreferQuotedFileBasedProgramDirective.Fixer.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/PreferQuotedFileBasedProgramDirective.Fixer.cs new file mode 100644 index 000000000000..2e099ce3ac04 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/PreferQuotedFileBasedProgramDirective.Fixer.cs @@ -0,0 +1,15 @@ +// 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.CodeFixes; + +namespace Microsoft.NetCore.Analyzers.Usage +{ + public abstract class PreferQuotedFileBasedProgramDirectiveFixer : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(PreferQuotedFileBasedProgramDirective.RuleId); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/PreferQuotedFileBasedProgramDirective.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/PreferQuotedFileBasedProgramDirective.cs new file mode 100644 index 000000000000..d06d99bd81d0 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Usage/PreferQuotedFileBasedProgramDirective.cs @@ -0,0 +1,30 @@ +// 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 Analyzer.Utilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.NetCore.Analyzers.Usage +{ + using static MicrosoftNetCoreAnalyzersResources; + + public abstract class PreferQuotedFileBasedProgramDirective : DiagnosticAnalyzer + { + internal const string RuleId = "CA2267"; + + internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( + RuleId, + CreateLocalizableResourceString(nameof(PreferQuotedFileBasedProgramDirectiveTitle)), + CreateLocalizableResourceString(nameof(PreferQuotedFileBasedProgramDirectiveMessage)), + DiagnosticCategory.Usage, + RuleLevel.BuildWarning, + CreateLocalizableResourceString(nameof(PreferQuotedFileBasedProgramDirectiveDescription)), + isPortedFxCopRule: false, + isDataflowRule: false, + isReportedAtCompilationEnd: false); + + public sealed override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf index 21c82a63bcf3..e7466a8c847a 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf @@ -2413,6 +2413,26 @@ Rozšíření a uživatelem definované převody se u obecných typů nepodporuj Upřednostňujte porovnání vlastnosti Length s 0 místo použití metody Any(), a to jak pro přehlednost, tak pro výkon. + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf index 7687e52a062f..3e7124b61855 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf @@ -2413,6 +2413,26 @@ Erweiterungen und benutzerdefinierte Konvertierungen werden bei generischen Type Sowohl aus Gründen der Klarheit als auch der Leistung ist der Vergleich von „Length“ mit 0 der Verwendung von „Any()“ vorzuziehen + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf index 189eb68a6429..85f873aacbb2 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf @@ -2413,6 +2413,26 @@ La ampliación y las conversiones definidas por el usuario no se admiten con tip Es preferible comparar "Length" con 0 en lugar de usar "Any()", tanto por claridad como por rendimiento. + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf index 2999c9b1ca12..74dcb300ee5d 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf @@ -2413,6 +2413,26 @@ Les conversions étendues et définies par l’utilisateur ne sont pas prises en Préférez comparer 'Length' à 0 au lieu d’utiliser 'Any()', à la fois pour plus de clarté et pour des performances + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf index fdc1c7248cc5..9532b60eafad 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf @@ -2413,6 +2413,26 @@ L'ampliamento e le conversioni definite dall'utente non sono supportate con tipi Preferire il confronto 'Length' con 0 anziché usare 'Any()', sia per chiarezza che per prestazioni + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf index a57f740b60c7..7b7505db474d 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf @@ -2413,6 +2413,26 @@ Enumerable.OfType<T> で使用されるジェネリック型チェック ( 明確性とパフォーマンスの両方のために、'Any()' を使用するのではなく、'Length' を 0 と比較することを優先してください + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf index 341f558da4fb..e13df8b51c26 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf @@ -2413,6 +2413,26 @@ Enumerable.OfType<T>에서 사용하는 제네릭 형식 검사(C# 'is' 명확성과 성능을 위해 'Any()'를 사용하는 것보다 'Length'를 0과 비교하는 것이 좋습니다. + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf index 280c0d957246..032c46cfbc9f 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf @@ -2413,6 +2413,26 @@ Konwersje poszerzane i zdefiniowane przez użytkownika nie są obsługiwane w pr Preferuj porównywanie wartości „Length” z wartością 0 zamiast używania elementu „Any()”, zarówno w celu zapewnienia przejrzystości, jak i wydajności + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf index b3e0b085f9f0..73f315d08973 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf @@ -2413,6 +2413,26 @@ As ampliação e conversões definidas pelo usuário não são compatíveis com Prefira comparar 'Length' com 0 em vez de usar 'Any()', tanto para clareza quanto para desempenho + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf index 3b140fd234ee..ec2266c1b5cc 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf @@ -2413,6 +2413,26 @@ Widening and user defined conversions are not supported with generic types.Для ясности и для обеспечения производительности старайтесь сравнивать 'Length' с 0 вместо того, чтобы использовать 'Any()' + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf index 81c2b0092892..91b07583f556 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf @@ -2413,6 +2413,26 @@ Genel türlerde genişletme ve kullanıcı tanımlı dönüştürmeler desteklen Hem kolay anlaşılırlık hem de performans için 'Length' değerini 'Any()' yerine 0 ile karşılaştırmayı tercih edin + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf index c560fa0114c7..9981a7d9b904 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf @@ -2413,6 +2413,26 @@ Enumerable.OfType<T> 使用的泛型类型检查(C# 'is' operator/IL 'isin 为了清楚起见和提高性能,首选将 'Length'与 0 进行比较,而不是使用 'Any()'。 + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf index 415915337821..941f3c2f62dd 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf @@ -2413,6 +2413,26 @@ Enumerable.OfType<T> 使用的一般型別檢查 (C# 'is' operator/IL 'isi 為了清楚明瞭和為了提升效能,偏好比較 'Length' 與 0,而不是使用 'Any()' + + Add quotes around the directive value + Add quotes around the directive value + + + + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + Before quoting was supported, whitespace in a file-based program '#:' directive value was taken literally. That form still works but is deprecated; wrap values that contain whitespace in double quotes so they are parsed unambiguously. + + + + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + Wrap the value of the '#:{0}' directive in quotes; the unquoted-whitespace form is deprecated + + + + Quote whitespace in file-based program directive values + Quote whitespace in file-based program directive values + + Change to '{0}' Change to '{0}' diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt index 3f0cfc547dc9..cb5727f83d6b 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt @@ -14,7 +14,7 @@ Globalization: CA2101, CA1300-CA1311 Mobility: CA1600-CA1601 Performance: HA, CA1800-CA1877 Security: CA2100-CA2153, CA2300-CA2330, CA3000-CA3147, CA5300-CA5405 -Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2266 +Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2267 Naming: CA1700-CA1727 Interoperability: CA1400-CA1422 Maintainability: CA1500-CA1517 diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Usage/PreferQuotedFileBasedProgramDirectiveTests.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Usage/PreferQuotedFileBasedProgramDirectiveTests.cs new file mode 100644 index 000000000000..318d0d531f01 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Usage/PreferQuotedFileBasedProgramDirectiveTests.cs @@ -0,0 +1,322 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using VerifyCS = Test.Utilities.CSharpSecurityCodeFixVerifier< + Microsoft.NetCore.CSharp.Analyzers.Usage.CSharpPreferQuotedFileBasedProgramDirective, + Microsoft.NetCore.CSharp.Analyzers.Usage.CSharpPreferQuotedFileBasedProgramDirectiveFixer>; + +namespace Microsoft.NetCore.Analyzers.Usage.UnitTests +{ + [TestClass] + public class PreferQuotedFileBasedProgramDirectiveTests + { + private static DiagnosticResult Expected(string kind, int line = 1) + => new DiagnosticResult(PreferQuotedFileBasedProgramDirective.Rule).WithLocation("Test0.cs", line, 1).WithArguments(kind); + + [TestMethod] + public async Task PropertyUnquotedValue_WarningAndFixAsync() + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Test0.cs", """ + #:property Description=Hello World + class Program { static void Main() { } } + """), + }, + ExpectedDiagnostics = { Expected("property") }, + }, + FixedState = + { + Sources = + { + ("Test0.cs", """ + #:property Description="Hello World" + class Program { static void Main() { } } + """), + }, + }, + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + SolutionTransforms = { EnableFileBasedProgramFeature }, + }.RunAsync(CancellationToken.None); + } + + [TestMethod] + public async Task PropertySpacesAroundSeparator_FixCollapsesAsync() + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Test0.cs", """ + #:property Prop = Value + class Program { static void Main() { } } + """), + }, + ExpectedDiagnostics = { Expected("property") }, + }, + FixedState = + { + Sources = + { + ("Test0.cs", """ + #:property Prop=Value + class Program { static void Main() { } } + """), + }, + }, + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + SolutionTransforms = { EnableFileBasedProgramFeature }, + }.RunAsync(CancellationToken.None); + } + + [TestMethod] + [DataRow("project")] + [DataRow("ref")] + [DataRow("include")] + [DataRow("exclude")] + public async Task WholeValueWithWhitespace_WarningAndFixAsync(string kind) + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Test0.cs", $$""" + #:{{kind}} ../My Library/thing + class Program { static void Main() { } } + """), + }, + ExpectedDiagnostics = { Expected(kind) }, + }, + FixedState = + { + Sources = + { + ("Test0.cs", $$""" + #:{{kind}} "../My Library/thing" + class Program { static void Main() { } } + """), + }, + }, + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + SolutionTransforms = { EnableFileBasedProgramFeature }, + }.RunAsync(CancellationToken.None); + } + + [TestMethod] + public async Task WholeValueWithBackslash_FixEscapesAsync() + { + // The quoted form is a regular C# string literal, so a backslash in the value must be + // escaped for the fix to round-trip (an unescaped '\M' would be an invalid escape sequence). + await new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Test0.cs", """ + #:project ..\My Library + class Program { static void Main() { } } + """), + }, + ExpectedDiagnostics = { Expected("project") }, + }, + FixedState = + { + Sources = + { + ("Test0.cs", """ + #:project "..\\My Library" + class Program { static void Main() { } } + """), + }, + }, + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + SolutionTransforms = { EnableFileBasedProgramFeature }, + }.RunAsync(CancellationToken.None); + } + + [TestMethod] + [DataRow("sdk")] + [DataRow("package")] + public async Task SpacesAroundNameVersionSeparator_FixCollapsesAsync(string kind) + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Test0.cs", $$""" + #:{{kind}} First @ 1.0 + class Program { static void Main() { } } + """), + }, + ExpectedDiagnostics = { Expected(kind) }, + }, + FixedState = + { + Sources = + { + ("Test0.cs", $$""" + #:{{kind}} First@1.0 + class Program { static void Main() { } } + """), + }, + }, + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + SolutionTransforms = { EnableFileBasedProgramFeature }, + }.RunAsync(CancellationToken.None); + } + + [TestMethod] + public async Task MultipleDirectives_AllFixedAsync() + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Test0.cs", """ + #:property Description=Hello World + #:project ../My Library + class Program { static void Main() { } } + """), + }, + ExpectedDiagnostics = + { + Expected("property", line: 1), + Expected("project", line: 2), + }, + }, + FixedState = + { + Sources = + { + ("Test0.cs", """ + #:property Description="Hello World" + #:project "../My Library" + class Program { static void Main() { } } + """), + }, + }, + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + SolutionTransforms = { EnableFileBasedProgramFeature }, + }.RunAsync(CancellationToken.None); + } + + [TestMethod] + [DataRow("#:property Description=\"Hello World\"")] + [DataRow("#:property Description=Hello")] + [DataRow("#:package Foo@1.0.0")] + [DataRow("#:package Foo@1.0.0 ExcludeAssets=runtime PrivateAssets=all")] + [DataRow("#:project ../Lib Private=false")] + [DataRow("#:ref ../lib.cs Aliases=lib")] + [DataRow("#:package Foo@1.0 ExtraToken")] + public async Task NewOrUnfixableForm_NoDiagnosticAsync(string directive) + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Test0.cs", $$""" + {{directive}} + class Program { static void Main() { } } + """), + }, + }, + SolutionTransforms = { EnableFileBasedProgramFeature }, + }.RunAsync(CancellationToken.None); + } + + [TestMethod] + public async Task NoEntryPointFilePath_StillFiresAsync() + { + // The analyzer inspects every ignored directive trivia regardless of EntryPointFilePath. + await new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Test0.cs", """ + #:property Description=Hello World + class Program { static void Main() { } } + """), + }, + ExpectedDiagnostics = { Expected("property") }, + }, + FixedState = + { + Sources = + { + ("Test0.cs", """ + #:property Description="Hello World" + class Program { static void Main() { } } + """), + }, + }, + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + SolutionTransforms = { EnableFileBasedProgramFeature }, + }.RunAsync(CancellationToken.None); + } + + [TestMethod] + public async Task DirectiveInNonEntryPointFile_StillFiresAsync() + { + // A legacy directive in any file is flagged, not only the entry point. + await new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Test0.cs", """class Program { static void Main() { } }"""), + ("Other.cs", """ + #:property Description=Hello World + class Other { } + """), + }, + ExpectedDiagnostics = + { + new DiagnosticResult(PreferQuotedFileBasedProgramDirective.Rule).WithLocation("Other.cs", 1, 1).WithArguments("property"), + }, + }, + FixedState = + { + Sources = + { + ("Test0.cs", """class Program { static void Main() { } }"""), + ("Other.cs", """ + #:property Description="Hello World" + class Other { } + """), + }, + }, + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + SolutionTransforms = { EnableFileBasedProgramFeature }, + }.RunAsync(CancellationToken.None); + } + + private static Solution EnableFileBasedProgramFeature(Solution solution, ProjectId projectId) + { + var parseOptions = (CSharpParseOptions)solution.GetProject(projectId)!.ParseOptions!; + return solution.WithProjectParseOptions(projectId, + parseOptions.WithFeatures(parseOptions.Features.Concat( + [new KeyValuePair("FileBasedProgram", "true")]))); + } + } +} diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index 053a67e07004..97ff2506e003 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -267,6 +267,56 @@ public static class Greeter .And.HaveStdOut(expectedOutput); } + [TestMethod] + public void RefDirective_Metadata_Convert() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $""" + + + <{CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective}>true + + + """); + + File.WriteAllText(Path.Join(testInstance.Path, "lib.cs"), """ + #:property OutputType=Library + namespace MyLib; + public static class Greeter + { + public static string Greet(string name) => $"Hello, {name}!"; + } + """); + + File.WriteAllText(Path.Join(testInstance.Path, "app.cs"), """ + #!/usr/bin/env dotnet + #:ref lib.cs Category=test + Console.WriteLine(MyLib.Greeter.Greet("World")); + """); + + var outputDirFullPath = Path.Join(testInstance.Path, "Project"); + new DotnetCommand(Log, "project", "convert", "app.cs", "-o", outputDirFullPath) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + // #:ref metadata should be carried over to the converted ProjectReference as a child element. + var appProject = File.ReadAllText(Path.Join(outputDirFullPath, "app", "app.csproj")); + appProject.Should().Contain($""" + + test + + """); + + // The converted project should build and produce the same output. + new DotnetCommand(Log, "run") + .WithWorkingDirectory(Path.Join(outputDirFullPath, "app")) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, World!"); + } + [TestMethod] public void RefDirective_Transitive_Convert() { @@ -2318,6 +2368,252 @@ public void Directives_Separators() expectedCSharp: ""); } + [TestMethod] + public void Directives_PackageMetadata() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: """ + #:package Microsoft.Build@18.0.2 ExcludeAssets=runtime PrivateAssets=all + #:package NoVersion IncludeAssets=build + """, + expectedProject: $""" + + + + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + + + + + runtime + all + + + build + + + + + + """, + expectedCSharp: ""); + } + + [TestMethod] + public void Directives_ProjectMetadata() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Lib.csproj"), """ + + """); + + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: """ + #:project Lib.csproj Private=false OutputItemType=Analyzer + """, + expectedProject: $""" + + + + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + + + + + false + Analyzer + + + + + + """, + expectedCSharp: ""); + } + + [TestMethod] + public void Directives_Quoting() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: """ + #:property Description="Hello World" + #:package Foo@1.0.0 Note="see the docs" + """, + expectedProject: $""" + + + + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + Hello World + + + + + see the docs + + + + + + """, + expectedCSharp: ""); + } + + [TestMethod] + public void Directives_QuoteEscapes() + { + // A quoted value is lexed as a regular C# string literal, so escape sequences are decoded. + var testInstance = TestAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: """ + #:property Quote="a\"b" + #:property Backslash="a\\b" + #:property Tab="a\tb c" + #:package Foo@1.0.0 Note="quote\"and\\slash" + """, + expectedProject: $""" + + + + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + a"b + a\b + a{"\t"}b c + + + + + quote"and\slash + + + + + + """, + expectedCSharp: ""); + } + + [TestMethod] + public void Directives_UnterminatedQuote() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: """ + #:property Description="unterminated + """, + expectedErrors: + [ + (1, FileBasedProgramsResources.UnterminatedQuoteInDirective), + ]); + } + + [TestMethod] + public void Directives_InvalidEscapeSequence() + { + // A terminated literal with a bad escape reports the underlying C# lexer error (not "unterminated"). + var testInstance = TestAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: """ + #:property Description="a\qb" + """, + expectedErrors: + [ + (1, string.Format(FileBasedProgramsResources.InvalidStringLiteralInDirective, "Unrecognized escape sequence")), + ]); + } + + [TestMethod] + [DataRow("#:property A=B\"C\"")] + [DataRow("#:property A=B\"C\"D")] + [DataRow("#:property A=\"B\"C")] + [DataRow("#:property A\"B\"=C")] + public void Directives_InvalidQuote(string directive) + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: directive, + expectedErrors: + [ + (1, FileBasedProgramsResources.InvalidQuoteInDirective), + ]); + } + + [TestMethod] + [DataRow("#:property Description=\"\"\"abc\"\"\"", "\"\"\"abc\"\"\"")] + public void Directives_RawStringLiteralRejected(string directive, string expectedTokenText) + { + // Raw string literals ('"""..."""') lex to a different token kind and are not supported; the + // error shows the offending token text rather than assuming a specific kind. + var testInstance = TestAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: directive, + expectedErrors: + [ + (1, string.Format(FileBasedProgramsResources.ExpectedSimpleStringLiteralInDirective, expectedTokenText)), + ]); + } + + [TestMethod] + public void Directives_InvalidMetadataName() + { + // A quote forces the strict (new) form, so the metadata name is validated. + var testInstance = TestAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: """ + #:package Foo@1.0.0 1Invalid="value" + """, + expectedErrors: + [ + (1, string.Format(FileBasedProgramsResources.DirectiveMetadataInvalidName, "1Invalid", "Name cannot begin with the '1' character, hexadecimal value 0x31.")), + ]); + } + + [TestMethod] + public void Directives_EmptyMetadataName() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + inputCSharp: """ + #:package Foo@1.0.0 ="value" + """, + expectedErrors: + [ + (1, string.Format(FileBasedProgramsResources.InvalidDirectiveMetadata, "=value")), + ]); + } + [TestMethod] [DataRow("invalid")] [DataRow("SDK")] @@ -2487,15 +2783,13 @@ public void Directives_Escaping() VerifyConversion( baseDirectory: testInstance.Path, inputCSharp: """ - #:property Prop= - #:sdk @="<>te'st - #:package @="<>te'st - #:property Pro'p=Single' - #:property Prop2=\"Value\" - #:property Prop3='Value' + #:property Prop=&x + #:sdk Name@<>te'st + #:package Pack@1.0 Meta= + #:property Desc="a c" """, expectedProject: $""" - + Exe @@ -2504,30 +2798,20 @@ public void Directives_Escaping() enable true true - <test"> - \"Value\" - 'Value' + <te'st>&x + a <b> c - + + <a&b> + """, - expectedCSharp: """ - #:property Pro'p=Single' - - """, - expectedErrors: - [ - (1, FileBasedProgramsResources.QuoteInDirective), - (2, FileBasedProgramsResources.QuoteInDirective), - (3, FileBasedProgramsResources.QuoteInDirective), - (4, string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, "The ''' character, hexadecimal value 0x27, cannot be included in a name.")), - (5, FileBasedProgramsResources.QuoteInDirective), - ]); + expectedCSharp: ""); } [TestMethod] @@ -2555,7 +2839,7 @@ public void Directives_Whitespace() true true Value - "My package with spaces" + My package with spaces @@ -2565,11 +2849,7 @@ public void Directives_Whitespace() # ! /test #! /program x # :property Name=Value - """, - expectedErrors: - [ - (3, FileBasedProgramsResources.QuoteInDirective), - ]); + """); } [TestMethod] diff --git a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs index f11802a5bc62..2d0b8b8dd198 100644 --- a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs @@ -1,6 +1,7 @@ // 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.Text; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.FileBasedPrograms; @@ -313,6 +314,87 @@ public void Comment_MultiLine_NoNewLine_Multiple() """)); } + [TestMethod] + public void AddWithMetadataAndQuoting() + { + Verify( + """ + Console.WriteLine(); + """, + (static editor => editor.Add(new CSharpDirective.Package(default) + { + Name = "MyPackage", + Version = "1.0.0", + Metadata = ImmutableArray.Create(("ExcludeAssets", "runtime"), ("Note", "with spaces")), + }), + """ + #:package MyPackage@1.0.0 ExcludeAssets=runtime Note="with spaces" + + Console.WriteLine(); + """)); + } + + [TestMethod] + public void AddWithSpecialCharactersEscapes() + { + // Values containing a double quote are emitted as an escaped C# string literal; a bare backslash + // (no whitespace or quote) needs no quoting and round-trips as-is. + Verify( + """ + Console.WriteLine(); + """, + (static editor => editor.Add(new CSharpDirective.Package(default) + { + Name = "MyPackage", + Version = "1.0.0", + Metadata = ImmutableArray.Create(("Quote", "a\"b"), ("Path", "a\\b"), ("Spaced", "a\"b c")), + }), + """ + #:package MyPackage@1.0.0 Quote="a\"b" Path=a\b Spaced="a\"b c" + + Console.WriteLine(); + """)); + } + + [TestMethod] + public void RefWithMetadataRoundTrips() + { + // A #:ref directive with trailing metadata is parsed and preserved verbatim when other edits happen. + Verify( + """ + #:ref lib.cs Aliases=lib Note="with spaces" + Console.WriteLine(); + """, + (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }), + """ + #:package MyPackage@1.0.0 + #:ref lib.cs Aliases=lib Note="with spaces" + Console.WriteLine(); + """)); + } + + [TestMethod] + public void LegacyWhitespacePreservedVerbatim() + { + // Directives using the deprecated unquoted-whitespace form are still parsed and are + // preserved verbatim when unrelated edits happen (no breaking change). + Verify( + """ + #:package Existing@1.0 + #:property Description=Hello World + #:project ../My Library + Console.WriteLine(); + """, + (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }), + """ + #:package Existing@1.0 + #:package MyPackage@1.0.0 + #:property Description=Hello World + #:project ../My Library + Console.WriteLine(); + """)); + } + [TestMethod] public void Group() { diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs index fca90a0d5a5f..7029ef39bb10 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_BuildOptions.cs @@ -1153,6 +1153,24 @@ class Util { public static string Greet() => "hello from util"; } .And.HaveStdOutContaining("hello"); } + [TestMethod] + public void UnquotedDirectiveWarning() + { + var testInstance = TestAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:property Description=value with a space + Console.WriteLine("hello"); + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("warning CA2267") + .And.HaveStdOutContaining("hello"); + } + /// /// File-based projects using the default SDK do not include embedded resources by default. ///