diff --git a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 9.md b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 9.md index 698ae888af3c0..5477b0c17da8b 100644 --- a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 9.md +++ b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 9.md @@ -44,3 +44,26 @@ unsafe class C // unsafe context ``` You can work around the break simply by adding the `unsafe` modifier to the local function. + +## .editorconfig values no longer support trailing comments + +***Introduced in Visual Studio 2022 version 17.[TBD]*** + +The compiler is updated based on the EditorConfig specification clarification in +[editorconfig/specification#31](https://github.com/editorconfig/specification/pull/31). Following this change, comments +in **.editorconfig** and **.globalconfig** files must now appear on their own line. Comments which appear at the end of +a property value are now treated as part of the property value itself. This changes the way values are passed to +analyzers for lines with the following form: + +```ini +[*.cs] +key = value # text +key2 = value2 ; text2 +``` + +The following table shows how this change affects values passed to analyzers: + +| EditorConfig line | New compiler interpretation | Old interpretation | +| ----------------------- | --------------------------- | ------------------ | +| `key = value # text` | `value # text` | `value` | +| `key2 = value2 ; text2` | `value2 ; text2` | `value2` | diff --git a/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs b/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs index 862e18b9ff69f..1491021e062f0 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs @@ -270,15 +270,18 @@ public void SpacesInProperties() properties); } - [Fact] - public void EndOfLineComments() + [Theory] + [WorkItem(44596, "https://github.com/dotnet/roslyn/issues/44596")] + [InlineData(";")] + [InlineData("#")] + public void EndOfLineComments(string commentStartCharacter) { - var config = ParseConfigFile(@" -my_prop2 = my val2 # Comment"); + var config = ParseConfigFile($@" +my_prop2 = my val2 {commentStartCharacter} Not A Comment"); var properties = config.GlobalSection.Properties; AssertEx.SetEqual( - new[] { KeyValuePair.Create("my_prop2", "my val2") }, + new[] { KeyValuePair.Create("my_prop2", $"my val2 {commentStartCharacter} Not A Comment") }, properties); } @@ -994,6 +997,82 @@ public void EditorConfigToDiagnostics_DoubleSlash(string prefixEditorConfig, str ], options.Select(o => o.TreeOptions).ToArray()); } + [Theory] + [InlineData("default")] + [InlineData("none")] + [InlineData("silent")] + [InlineData("refactoring")] + [InlineData("suggestion")] + [InlineData("warning")] + [InlineData("error")] + public void EditorConfigToDiagnosticsValidSeverity(string severity) + { + var expected = severity switch + { + "default" => ReportDiagnostic.Default, + "none" => ReportDiagnostic.Suppress, + "silent" => ReportDiagnostic.Hidden, + "refactoring" => ReportDiagnostic.Hidden, + "suggestion" => ReportDiagnostic.Info, + "warning" => ReportDiagnostic.Warn, + "error" => ReportDiagnostic.Error, + _ => throw ExceptionUtilities.UnexpectedValue(severity), + }; + + var configs = ArrayBuilder.GetInstance(); + configs.Add(Parse($@" +[*.cs] +dotnet_diagnostic.cs000.severity = {severity} +", "/.editorconfig")); + + var options = GetAnalyzerConfigOptions(["/test.cs"], configs); + configs.Free(); + + Assert.Equal([CreateImmutableDictionary(("cs000", expected))], Array.ConvertAll(options, o => o.TreeOptions)); + } + + [Theory] + [InlineData("unset")] + [InlineData("warn")] + [InlineData("")] + public void EditorConfigToDiagnosticsInvalidSeverity(string severity) + { + var configs = ArrayBuilder.GetInstance(); + configs.Add(Parse($@" +[*.cs] +dotnet_diagnostic.cs000.severity = {severity} +", "/.editorconfig")); + + var options = GetAnalyzerConfigOptions(["/test.cs"], configs); + configs.Free(); + + Assert.Equal([ImmutableDictionary.Empty], Array.ConvertAll(options, o => o.TreeOptions)); + } + + /// + /// Verifies that the AnalyzerConfig parser ignores comment-like trailing text when parsing severity values. + /// This ensures the change from https://github.com/dotnet/roslyn/pull/51625 does not affect the parsing of + /// diagnostic severities. + /// + [Theory] + [InlineData("#")] + [InlineData(";")] + [WorkItem("https://github.com/dotnet/roslyn/pull/51625")] + public void EditorConfigToDiagnosticsIgnoresCommentLikeText(string delimiter) + { + var configs = ArrayBuilder.GetInstance(); + configs.Add(Parse($@" +[*.cs] +dotnet_diagnostic.cs000.severity = error {delimiter} ignored text +dotnet_diagnostic.cs001.severity = warning {delimiter} ignored text +", "/.editorconfig")); + + var options = GetAnalyzerConfigOptions(["/test.cs"], configs); + configs.Free(); + + Assert.Equal([CreateImmutableDictionary(("cs000", ReportDiagnostic.Error), ("cs001", ReportDiagnostic.Warn))], Array.ConvertAll(options, o => o.TreeOptions)); + } + [Fact] public void LaterSectionOverrides() { diff --git a/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.cs b/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.cs index 46a89ceeaad0d..92e5b983eb143 100644 --- a/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.cs +++ b/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.cs @@ -22,7 +22,7 @@ public sealed partial class AnalyzerConfig private const string s_sectionMatcherPattern = @"^\s*\[(([^#;]|\\#|\\;)+)\]\s*([#;].*)?$"; // Matches EditorConfig property such as "indent_style = space", see https://editorconfig.org for details - private const string s_propertyMatcherPattern = @"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$"; + private const string s_propertyMatcherPattern = @"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*$"; #if NETCOREAPP diff --git a/src/Compilers/Core/Portable/CommandLine/AnalyzerConfigSet.cs b/src/Compilers/Core/Portable/CommandLine/AnalyzerConfigSet.cs index 7daa31f64d97f..ff87edd78f19e 100644 --- a/src/Compilers/Core/Portable/CommandLine/AnalyzerConfigSet.cs +++ b/src/Compilers/Core/Portable/CommandLine/AnalyzerConfigSet.cs @@ -330,33 +330,40 @@ static void freeKey(List
sectionKey, ObjectPool> pool) internal static bool TryParseSeverity(string value, out ReportDiagnostic severity) { + ReadOnlySpan commentStartCharacters = stackalloc char[] { ';', '#' }; + var trimmed = value.AsSpan(); + var commentStartIndex = trimmed.IndexOfAny(commentStartCharacters); + if (commentStartIndex >= 0) + trimmed = trimmed[0..commentStartIndex].TrimEnd(); + var comparer = StringComparer.OrdinalIgnoreCase; - if (comparer.Equals(value, "default")) + if (trimmed.Equals("default".AsSpan(), StringComparison.OrdinalIgnoreCase)) { severity = ReportDiagnostic.Default; return true; } - else if (comparer.Equals(value, "error")) + else if (trimmed.Equals("error".AsSpan(), StringComparison.OrdinalIgnoreCase)) { severity = ReportDiagnostic.Error; return true; } - else if (comparer.Equals(value, "warning")) + else if (trimmed.Equals("warning".AsSpan(), StringComparison.OrdinalIgnoreCase)) { severity = ReportDiagnostic.Warn; return true; } - else if (comparer.Equals(value, "suggestion")) + else if (trimmed.Equals("suggestion".AsSpan(), StringComparison.OrdinalIgnoreCase)) { severity = ReportDiagnostic.Info; return true; } - else if (comparer.Equals(value, "silent") || comparer.Equals(value, "refactoring")) + else if (trimmed.Equals("silent".AsSpan(), StringComparison.OrdinalIgnoreCase) + || trimmed.Equals("refactoring".AsSpan(), StringComparison.OrdinalIgnoreCase)) { severity = ReportDiagnostic.Hidden; return true; } - else if (comparer.Equals(value, "none")) + else if (trimmed.Equals("none".AsSpan(), StringComparison.OrdinalIgnoreCase)) { severity = ReportDiagnostic.Suppress; return true; diff --git a/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs b/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs index e339295f7bbd9..9f3c0e815379b 100644 --- a/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs +++ b/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs @@ -34,6 +34,16 @@ public class EditorConfigCodeStyleParserTests [InlineData("false : warning", false, ReportDiagnostic.Warn)] [InlineData("true : error", true, ReportDiagnostic.Error)] [InlineData("false : error", false, ReportDiagnostic.Error)] + + [WorkItem("https://github.com/dotnet/roslyn/issues/44596")] + [InlineData("true:warning # comment", true, ReportDiagnostic.Warn)] + [InlineData("false:warning # comment", false, ReportDiagnostic.Warn)] + [InlineData("true:error # comment", true, ReportDiagnostic.Error)] + [InlineData("false:error # comment", false, ReportDiagnostic.Error)] + [InlineData("true:warning ; comment", true, ReportDiagnostic.Warn)] + [InlineData("false:warning ; comment", false, ReportDiagnostic.Warn)] + [InlineData("true:error ; comment", true, ReportDiagnostic.Error)] + [InlineData("false:error ; comment", false, ReportDiagnostic.Error)] public void TestParseEditorConfigCodeStyleOption(string args, bool isEnabled, ReportDiagnostic severity) { CodeStyleHelpers.TryParseBoolEditorConfigCodeStyleOption(args, defaultValue: CodeStyleOption2.Default, out var result); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CodeStyle/CodeStyleHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CodeStyle/CodeStyleHelpers.cs index 2230937ca52dd..09e1322b3b69d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CodeStyle/CodeStyleHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CodeStyle/CodeStyleHelpers.cs @@ -13,6 +13,13 @@ namespace Microsoft.CodeAnalysis.CodeStyle; internal static class CodeStyleHelpers { + /// + /// Delimiters which the compiler previously treated as embedded comment delimiters for parsing EditorConfig + /// files. For code style options maintained by Roslyn, we continue to detect and remove these "comments" for + /// backwards compatibility. + /// + private static readonly char[] s_commentDelimiters = ['#', ';']; + public static bool TryParseStringEditorConfigCodeStyleOption(string arg, CodeStyleOption2 defaultValue, [NotNullWhen(true)] out CodeStyleOption2? option) { if (TryGetCodeStyleValueAndOptionalNotification( @@ -62,6 +69,16 @@ public static bool TryGetCodeStyleValue( public static bool TryGetCodeStyleValueAndOptionalNotification( string arg, NotificationOption2 defaultNotification, [NotNullWhen(true)] out string? value, [NotNullWhen(true)] out NotificationOption2 notification) { + var embeddedCommentTextIndex = arg.IndexOfAny(s_commentDelimiters); + if (embeddedCommentTextIndex >= 0) + { + // For backwards compatibility, remove the embedded comment before continuing. In following the + // EditorConfig specification, the compiler no longer treats text after a '#' or ';' as an embedded + // comment. + // https://github.com/dotnet/roslyn/issues/44596 + arg = arg[..embeddedCommentTextIndex]; + } + var firstColonIndex = arg.IndexOf(':'); // We allow a single value to be provided without an explicit notification.