Skip to content

Commit 9df7d00

Browse files
authored
Implement editorconfig serializer for naming style preferences (17.12) (#76376)
The serializer is required when saving naming style preferences specified in Tools > Options to solution fallback options. The option was throwing NotSupportedException, which was reported via NFW but for some reason does not appear in telemetry data (TBD why). In order to preserve ordering of the naming style rules as specified in Tools > Options settings we introduce a new editor option `dotnet_naming_rule.{rule-name}.priority`. The highest priority is 0, which is the default. When saving VS options we generate priorities 0..N-1 where N is the number of rules. This causes VS option order to be preserved when the preferences are deserialized from fallback options. Note that if any naming style is set in .editorconfig file, all naming style settings in VS options are ignored. This behavior is consistent with VS 2019. Fixes https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2297536
2 parents dfa7fc6 + d144829 commit 9df7d00

File tree

17 files changed

+523
-121
lines changed

17 files changed

+523
-121
lines changed

src/EditorFeatures/CSharpTest/Diagnostics/NamingStyles/EditorConfigNamingStyleParserTests.cs

Lines changed: 155 additions & 39 deletions
Large diffs are not rendered by default.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Collections.Immutable;
8+
using System.Linq;
9+
using System.Xml.Linq;
10+
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
11+
using Microsoft.CodeAnalysis.NamingStyles;
12+
13+
namespace Microsoft.CodeAnalysis.Test.Utilities;
14+
15+
internal static class NamingStyleTestUtilities
16+
{
17+
public static string Inspect(this NamingRule rule)
18+
=> $"{rule.NamingStyle.Inspect()} {rule.SymbolSpecification.Inspect()} {rule.EnforcementLevel}";
19+
20+
public static string Inspect(this NamingStyle style)
21+
=> $"{style.Name} prefix='{style.Prefix}' suffix='{style.Suffix}' separator='{style.WordSeparator}'";
22+
23+
public static string Inspect(this SymbolSpecification symbol)
24+
=> $"{symbol.Name} {Inspect(symbol.ApplicableSymbolKindList)} {Inspect(symbol.ApplicableAccessibilityList)} {Inspect(symbol.RequiredModifierList)}";
25+
26+
public static string Inspect<T>(ImmutableArray<T> items) where T : notnull
27+
=> string.Join(",", items.Select(item => item.ToString()));
28+
29+
public static string Inspect(this NamingStylePreferences preferences, string[]? excludeNodes = null)
30+
{
31+
var xml = preferences.CreateXElement();
32+
33+
// filter out insignificant elements:
34+
var elementsToRemove = new List<XElement>();
35+
foreach (var element in xml.DescendantsAndSelf())
36+
{
37+
if (excludeNodes != null && excludeNodes.Contains(element.Name.LocalName))
38+
{
39+
elementsToRemove.Add(element);
40+
}
41+
}
42+
43+
foreach (var element in elementsToRemove)
44+
{
45+
element.Remove();
46+
}
47+
48+
// replaces GUIDs with unique deterministic numbers:
49+
var ordinal = 0;
50+
var guidMap = new Dictionary<Guid, int>();
51+
foreach (var element in xml.DescendantsAndSelf())
52+
{
53+
foreach (var attribute in element.Attributes())
54+
{
55+
if (Guid.TryParse(attribute.Value, out var guid))
56+
{
57+
if (!guidMap.TryGetValue(guid, out var existingOrdinal))
58+
{
59+
existingOrdinal = ordinal++;
60+
guidMap.Add(guid, existingOrdinal);
61+
}
62+
63+
attribute.Value = existingOrdinal.ToString();
64+
}
65+
}
66+
}
67+
68+
return xml.ToString();
69+
}
70+
}

src/LanguageServer/Protocol/Features/Options/SolutionAnalyzerConfigOptionsUpdater.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
using System.Composition;
99
using System.Diagnostics;
1010
using System.Linq;
11-
using System.Threading;
1211
using Microsoft.CodeAnalysis;
1312
using Microsoft.CodeAnalysis.Diagnostics;
13+
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
1414
using Microsoft.CodeAnalysis.ErrorReporting;
1515
using Microsoft.CodeAnalysis.Host;
1616
using Microsoft.CodeAnalysis.Host.Mef;
@@ -86,8 +86,21 @@ Solution UpdateOptions(Solution oldSolution)
8686

8787
// update changed values:
8888
var configName = key.Option.Definition.ConfigName;
89-
var configValue = key.Option.Definition.Serializer.Serialize(value);
90-
lazyBuilder[configName] = configValue;
89+
if (value is NamingStylePreferences preferences)
90+
{
91+
NamingStylePreferencesEditorConfigSerializer.WriteNamingStylePreferencesToEditorConfig(
92+
preferences.SymbolSpecifications,
93+
preferences.NamingStyles,
94+
preferences.NamingRules,
95+
language,
96+
entryWriter: (name, value) => lazyBuilder[name] = value,
97+
triviaWriter: null,
98+
setPrioritiesToPreserveOrder: true);
99+
}
100+
else
101+
{
102+
lazyBuilder[configName] = key.Option.Definition.Serializer.Serialize(value);
103+
}
91104
}
92105

93106
if (lazyBuilder != null)

src/LanguageServer/ProtocolUnitTests/Options/SolutionAnalyzerConfigOptionsUpdaterTests.cs

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Collections.Generic;
6+
using System.IO;
67
using System.Linq;
8+
using System.Threading;
9+
using Microsoft.CodeAnalysis.CodeStyle;
10+
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
711
using Microsoft.CodeAnalysis.Formatting;
812
using Microsoft.CodeAnalysis.Host;
13+
using Microsoft.CodeAnalysis.Shared.Extensions;
914
using Microsoft.CodeAnalysis.Test.Utilities;
10-
using Roslyn.Utilities;
15+
using Microsoft.CodeAnalysis.UnitTests;
16+
using Roslyn.Test.Utilities;
1117
using Xunit;
1218

1319
namespace Microsoft.CodeAnalysis.Options.UnitTests;
@@ -95,6 +101,154 @@ void AssertOptionValue(IOption2 option, string language, string expectedValue)
95101
}
96102
}
97103

104+
[Fact]
105+
[WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2297536")]
106+
public void FlowsNamingStylePreferencesToWorkspace()
107+
{
108+
using var workspace = CreateWorkspace();
109+
110+
var testProjectWithoutConfig = new TestHostProject(workspace, "proj_without_config", LanguageNames.CSharp);
111+
112+
testProjectWithoutConfig.AddDocument(new TestHostDocument("""
113+
class MyClass1;
114+
""",
115+
filePath: Path.Combine(TempRoot.Root, "proj_without_config", "test.cs")));
116+
117+
var testProjectWithConfig = new TestHostProject(workspace, "proj_with_config", LanguageNames.CSharp);
118+
119+
// explicitly specified style should override style specified in the fallback:
120+
testProjectWithConfig.AddAnalyzerConfigDocument(new TestHostDocument(
121+
"""
122+
[*.cs]
123+
dotnet_naming_rule.rule1.severity = warning
124+
dotnet_naming_rule.rule1.symbols = symbols1
125+
dotnet_naming_rule.rule1.style = style1
126+
127+
dotnet_naming_symbols.symbols1.applicable_kinds = class
128+
dotnet_naming_symbols.symbols1.applicable_accessibilities = *
129+
dotnet_naming_style.style1.capitalization = camel_case
130+
""",
131+
filePath: Path.Combine(TempRoot.Root, "proj_with_config", ".editorconfig")));
132+
133+
testProjectWithConfig.AddDocument(new TestHostDocument("""
134+
class MyClass2;
135+
""",
136+
filePath: Path.Combine(TempRoot.Root, "proj_with_config", "test.cs")));
137+
138+
workspace.AddTestProject(testProjectWithoutConfig);
139+
workspace.AddTestProject(testProjectWithConfig);
140+
141+
var globalOptions = workspace.GetService<IGlobalOptionService>();
142+
143+
var hostPeferences = OptionsTestHelpers.CreateNamingStylePreferences(
144+
([MethodKind.Ordinary], Capitalization.PascalCase, ReportDiagnostic.Error),
145+
([MethodKind.Ordinary, SymbolKind.Field], Capitalization.PascalCase, ReportDiagnostic.Error));
146+
147+
globalOptions.SetGlobalOption(NamingStyleOptions.NamingPreferences, LanguageNames.CSharp, hostPeferences);
148+
149+
Assert.True(workspace.CurrentSolution.FallbackAnalyzerOptions.TryGetValue(LanguageNames.CSharp, out var fallbackOptions));
150+
151+
// Note: rules are ordered but symbol and naming style specifications are not.
152+
AssertEx.Equal(
153+
hostPeferences.Rules.NamingRules.Select(r => r.Inspect()),
154+
fallbackOptions.GetNamingStylePreferences().Rules.NamingRules.Select(r => r.Inspect()));
155+
156+
var projectWithConfig = workspace.CurrentSolution.GetRequiredProject(testProjectWithConfig.Id);
157+
var treeWithConfig = projectWithConfig.Documents.Single().GetSyntaxTreeSynchronously(CancellationToken.None);
158+
Assert.NotNull(treeWithConfig);
159+
var documentOptions = projectWithConfig.HostAnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(treeWithConfig);
160+
161+
Assert.True(documentOptions.TryGetEditorConfigOption<NamingStylePreferences>(NamingStyleOptions.NamingPreferences, out var documentPreferences));
162+
Assert.NotNull(documentPreferences);
163+
164+
// Only naming styles specified in the editorconfig are present.
165+
// Host preferences are ignored. This behavior is consistent with VS 16.11.
166+
AssertEx.EqualOrDiff("""
167+
<NamingPreferencesInfo SerializationVersion="5">
168+
<SymbolSpecifications>
169+
<SymbolSpecification ID="0" Name="symbols1">
170+
<ApplicableSymbolKindList>
171+
<TypeKind>Class</TypeKind>
172+
</ApplicableSymbolKindList>
173+
<ApplicableAccessibilityList>
174+
<AccessibilityKind>NotApplicable</AccessibilityKind>
175+
<AccessibilityKind>Public</AccessibilityKind>
176+
<AccessibilityKind>Internal</AccessibilityKind>
177+
<AccessibilityKind>Private</AccessibilityKind>
178+
<AccessibilityKind>Protected</AccessibilityKind>
179+
<AccessibilityKind>ProtectedAndInternal</AccessibilityKind>
180+
<AccessibilityKind>ProtectedOrInternal</AccessibilityKind>
181+
</ApplicableAccessibilityList>
182+
<RequiredModifierList />
183+
</SymbolSpecification>
184+
</SymbolSpecifications>
185+
<NamingStyles>
186+
<NamingStyle ID="1" Name="style1" Prefix="" Suffix="" WordSeparator="" CapitalizationScheme="CamelCase" />
187+
</NamingStyles>
188+
<NamingRules>
189+
<SerializableNamingRule SymbolSpecificationID="0" NamingStyleID="1" EnforcementLevel="Warning" />
190+
</NamingRules>
191+
</NamingPreferencesInfo>
192+
""",
193+
documentPreferences.Inspect());
194+
195+
var projectWithoutConfig = workspace.CurrentSolution.GetRequiredProject(testProjectWithoutConfig.Id);
196+
var treeWithoutConfig = projectWithoutConfig.Documents.Single().GetSyntaxTreeSynchronously(CancellationToken.None);
197+
Assert.NotNull(treeWithoutConfig);
198+
documentOptions = projectWithoutConfig.HostAnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(treeWithoutConfig);
199+
200+
Assert.True(documentOptions.TryGetEditorConfigOption(NamingStyleOptions.NamingPreferences, out documentPreferences));
201+
Assert.NotNull(documentPreferences);
202+
203+
// Host preferences:
204+
AssertEx.EqualOrDiff("""
205+
<NamingPreferencesInfo SerializationVersion="5">
206+
<SymbolSpecifications>
207+
<SymbolSpecification ID="0" Name="symbols1">
208+
<ApplicableSymbolKindList>
209+
<MethodKind>Ordinary</MethodKind>
210+
<SymbolKind>Field</SymbolKind>
211+
</ApplicableSymbolKindList>
212+
<ApplicableAccessibilityList>
213+
<AccessibilityKind>NotApplicable</AccessibilityKind>
214+
<AccessibilityKind>Public</AccessibilityKind>
215+
<AccessibilityKind>Internal</AccessibilityKind>
216+
<AccessibilityKind>Private</AccessibilityKind>
217+
<AccessibilityKind>Protected</AccessibilityKind>
218+
<AccessibilityKind>ProtectedAndInternal</AccessibilityKind>
219+
<AccessibilityKind>ProtectedOrInternal</AccessibilityKind>
220+
</ApplicableAccessibilityList>
221+
<RequiredModifierList />
222+
</SymbolSpecification>
223+
<SymbolSpecification ID="1" Name="symbols0">
224+
<ApplicableSymbolKindList>
225+
<MethodKind>Ordinary</MethodKind>
226+
</ApplicableSymbolKindList>
227+
<ApplicableAccessibilityList>
228+
<AccessibilityKind>NotApplicable</AccessibilityKind>
229+
<AccessibilityKind>Public</AccessibilityKind>
230+
<AccessibilityKind>Internal</AccessibilityKind>
231+
<AccessibilityKind>Private</AccessibilityKind>
232+
<AccessibilityKind>Protected</AccessibilityKind>
233+
<AccessibilityKind>ProtectedAndInternal</AccessibilityKind>
234+
<AccessibilityKind>ProtectedOrInternal</AccessibilityKind>
235+
</ApplicableAccessibilityList>
236+
<RequiredModifierList />
237+
</SymbolSpecification>
238+
</SymbolSpecifications>
239+
<NamingStyles>
240+
<NamingStyle ID="2" Name="style1" Prefix="" Suffix="" WordSeparator="" CapitalizationScheme="PascalCase" />
241+
<NamingStyle ID="3" Name="style0" Prefix="" Suffix="" WordSeparator="" CapitalizationScheme="PascalCase" />
242+
</NamingStyles>
243+
<NamingRules>
244+
<SerializableNamingRule SymbolSpecificationID="1" NamingStyleID="3" EnforcementLevel="Error" />
245+
<SerializableNamingRule SymbolSpecificationID="0" NamingStyleID="2" EnforcementLevel="Error" />
246+
</NamingRules>
247+
</NamingPreferencesInfo>
248+
""",
249+
documentPreferences.Inspect());
250+
}
251+
98252
[Fact]
99253
public void IgnoresNonEditorConfigOptions()
100254
{

src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,18 @@ public void LanguageSpecificOptionsHaveCorrectPrefix(string configName)
107107
return;
108108
}
109109

110-
if (!info.Option.Definition.IsEditorConfigOption)
110+
// TODO: https://github.com/dotnet/roslyn/issues/65787
111+
if (info.Option.Name is
112+
"csharp_format_on_return" or
113+
"csharp_format_on_typing" or
114+
"csharp_format_on_semicolon" or
115+
"csharp_format_on_close_brace" or
116+
"csharp_enable_inlay_hints_for_types" or
117+
"csharp_enable_inlay_hints_for_implicit_variable_types" or
118+
"csharp_enable_inlay_hints_for_lambda_parameter_types" or
119+
"csharp_enable_inlay_hints_for_implicit_object_creation" or
120+
"csharp_enable_inlay_hints_for_collection_expressions")
111121
{
112-
// TODO: remove condition once all options have config name https://github.com/dotnet/roslyn/issues/65787
113122
return;
114123
}
115124

src/Workspaces/CoreTestUtilities/Options/OptionsTestHelpers.cs

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Collections.Immutable;
1010
using Microsoft.CodeAnalysis.CodeStyle;
1111
using Microsoft.CodeAnalysis.CSharp.Formatting;
12+
using Microsoft.CodeAnalysis.Diagnostics;
1213
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
1314
using Microsoft.CodeAnalysis.Formatting;
1415
using Microsoft.CodeAnalysis.NamingStyles;
@@ -147,33 +148,53 @@ private static object GetDifferentEnumValue(Type type, object defaultValue)
147148
}
148149

149150
public static NamingStylePreferences GetNonDefaultNamingStylePreference()
151+
=> CreateNamingStylePreferences(([TypeKind.Class], Capitalization.PascalCase, ReportDiagnostic.Error));
152+
153+
public static NamingStylePreferences CreateNamingStylePreferences(
154+
params (SymbolSpecification.SymbolKindOrTypeKind[], Capitalization capitalization, ReportDiagnostic severity)[] rules)
150155
{
151-
var symbolSpecification = new SymbolSpecification(
152-
Guid.NewGuid(),
153-
"Name",
154-
ImmutableArray.Create(new SymbolSpecification.SymbolKindOrTypeKind(TypeKind.Class)),
155-
accessibilityList: default,
156-
modifiers: default);
157-
158-
var namingStyle = new NamingStyle(
159-
Guid.NewGuid(),
160-
capitalizationScheme: Capitalization.PascalCase,
161-
name: "Name",
162-
prefix: "",
163-
suffix: "",
164-
wordSeparator: "");
165-
166-
var namingRule = new SerializableNamingRule()
156+
var symbolSpecifications = new List<SymbolSpecification>();
157+
var namingStyles = new List<NamingStyle>();
158+
var namingRules = new List<SerializableNamingRule>();
159+
160+
foreach (var (kinds, capitalization, severity) in rules)
167161
{
168-
SymbolSpecificationID = symbolSpecification.ID,
169-
NamingStyleID = namingStyle.ID,
170-
EnforcementLevel = ReportDiagnostic.Error
171-
};
162+
var id = namingRules.Count;
163+
164+
var symbolSpecification = new SymbolSpecification(
165+
Guid.NewGuid(),
166+
name: $"symbols{id}",
167+
[.. kinds],
168+
accessibilityList: default,
169+
modifiers: default);
170+
171+
symbolSpecifications.Add(symbolSpecification);
172+
173+
var namingStyle = new NamingStyle(
174+
Guid.NewGuid(),
175+
capitalizationScheme: capitalization,
176+
name: $"style{id}",
177+
prefix: "",
178+
suffix: "",
179+
wordSeparator: "");
180+
181+
namingStyles.Add(namingStyle);
182+
183+
namingRules.Add(new SerializableNamingRule()
184+
{
185+
SymbolSpecificationID = symbolSpecification.ID,
186+
NamingStyleID = namingStyle.ID,
187+
EnforcementLevel = severity
188+
});
189+
}
172190

173191
return new NamingStylePreferences(
174-
ImmutableArray.Create(symbolSpecification),
175-
ImmutableArray.Create(namingStyle),
176-
ImmutableArray.Create(namingRule));
192+
[.. symbolSpecifications],
193+
[.. namingStyles],
194+
[.. namingRules]);
177195
}
196+
197+
public static NamingStylePreferences ParseNamingStylePreferences(Dictionary<string, string> options)
198+
=> EditorConfigNamingStyleParser.ParseDictionary(new DictionaryAnalyzerConfigOptions(options.ToImmutableDictionary()));
178199
}
179200
}

src/Workspaces/CoreTestUtilities/OptionsCollection.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ public StructuredAnalyzerConfigOptions ToAnalyzerConfigOptions()
109109
namingPreferences.NamingRules,
110110
LanguageName,
111111
entryWriter: builder.Add,
112-
triviaWriter: null);
112+
triviaWriter: null,
113+
setPrioritiesToPreserveOrder: false);
113114
}
114115
else
115116
{

0 commit comments

Comments
 (0)