Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EditorConfig comments cannot follow a property value #51625

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 9.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]***
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tagging @jaredpar to advise on which release to pull this change into, since it involves a compat break. Feels like we should hold this off rather than squeeze it late in the release cycle.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should either update this before merging, or open an issue to track updating it once we know which release it landed in.


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` |
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,18 @@ public void SpacesInProperties()
properties);
}

[Fact]
public void EndOfLineComments()
[Theory]
Copy link
Member

@jcouv jcouv Mar 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Documented for .NET 9

[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);
}

Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would prefer these just be arguments to this test method, so the switch isn't needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would prefer these just be arguments to this test method, so the switch isn't needed.

➡️ The current form is intentional. Arguments to the test method are input state and are used to define the unique ID of the test itself. Expected results should not be inputs, and changing an expected outcome should not be part of the test identity.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd agree with Cyrus here -- we normally put the expected as an input too...

Copy link
Member Author

@sharwell sharwell Jan 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we normally put the expected as an input too

➡️ I try to fix these when they occur in a method I'm working on. Test parameterization should only ever exist on the inputs, not the outputs. The entire test identity and reporting strategy (e.g. test historical pass rates, red/green developer iterations in TDD) is built around this premise.

_ => throw ExceptionUtilities.UnexpectedValue(severity),
};

var configs = ArrayBuilder<AnalyzerConfig>.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<AnalyzerConfig>.GetInstance();
configs.Add(Parse($@"
[*.cs]
dotnet_diagnostic.cs000.severity = {severity}
", "/.editorconfig"));

var options = GetAnalyzerConfigOptions(["/test.cs"], configs);
configs.Free();

Assert.Equal([ImmutableDictionary<string, ReportDiagnostic>.Empty], Array.ConvertAll(options, o => o.TreeOptions));
}

/// <summary>
/// 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.
/// </summary>
[Theory]
[InlineData("#")]
[InlineData(";")]
Comment on lines +1058 to +1059
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider tweaking the inline data to add another case like:

Suggested change
[InlineData("#")]
[InlineData(";")]
[InlineData("error # ignored text")]
[InlineData("error ; ignored text")]
[InlineData("warning ; ignored text")]

As otherwise this test could incorrectly pass if all parse errors were simply converted to ReportDiagnostic.Error.

Copy link
Member Author

@sharwell sharwell Jan 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Added a second line inside the test itself.

[WorkItem("https://github.com/dotnet/roslyn/pull/51625")]
public void EditorConfigToDiagnosticsIgnoresCommentLikeText(string delimiter)
{
var configs = ArrayBuilder<AnalyzerConfig>.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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 13 additions & 6 deletions src/Compilers/Core/Portable/CommandLine/AnalyzerConfigSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,33 +330,40 @@ static void freeKey(List<Section> sectionKey, ObjectPool<List<Section>> pool)

internal static bool TryParseSeverity(string value, out ReportDiagnostic severity)
{
ReadOnlySpan<char> commentStartCharacters = stackalloc char[] { ';', '#' };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider leaving a comment ("for backcompat, ...")

var trimmed = value.AsSpan();
var commentStartIndex = trimmed.IndexOfAny(commentStartCharacters);
if (commentStartIndex >= 0)
trimmed = trimmed[0..commentStartIndex].TrimEnd();
Copy link
Member

@jcouv jcouv Mar 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, this change is to maintain the existing behavior and I assume we already have tests covering this. Is that right? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Yes, and tests have now been added.


var comparer = StringComparer.OrdinalIgnoreCase;
if (comparer.Equals(value, "default"))
if (trimmed.Equals("default".AsSpan(), StringComparison.OrdinalIgnoreCase))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparer local on line above (339) no longer seems to be used

{
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>.Default, out var result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ namespace Microsoft.CodeAnalysis.CodeStyle;

internal static class CodeStyleHelpers
{
/// <summary>
/// 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.
/// </summary>
private static readonly char[] s_commentDelimiters = ['#', ';'];

public static bool TryParseStringEditorConfigCodeStyleOption(string arg, CodeStyleOption2<string> defaultValue, [NotNullWhen(true)] out CodeStyleOption2<string>? option)
{
if (TryGetCodeStyleValueAndOptionalNotification(
Expand Down Expand Up @@ -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.
Expand Down