提交 7ac17725 编写于 作者: J Jason Malinowski

Ensure we give .editorconfig keys in the same order as in the file

Right now our naming styles .editorconfig format implicitly relies on
the order of keys being preserved from the .editorconfig file on disk
into the IDictionary<string, object> that's handed to the parser. This
is problematic in many ways, but until we either redesign the format
to avoid the need of ordering, or make the ordering explicit, we should
ensure we actually maintain that ordering.

Fixes https://github.com/dotnet/roslyn/issues/35488
上级 7f4a0800
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
......@@ -40,10 +41,10 @@ public bool TryGetDocumentOption(OptionKey option, OptionSet underlyingOptions,
// Dictionary<string, object> from the old system.
//
// We cache this with a conditional weak table so we're able to maintain the assumptions in EditorConfigNamingStyleParser
// that the instance doesn't regularly change and thus can be used for further caching
// that the instance doesn't regularly change and thus can be used for further caching.
var allRawConventions = s_convertedDictionaryCache.GetValue(
_codingConventionSnapshot.AllRawConventions,
d => ImmutableDictionary.CreateRange(d.Select(c => KeyValuePairUtil.Create(c.Key, c.Value.ToString()))));
d => new StringConvertingDictionary(d));
try
{
......@@ -57,6 +58,57 @@ public bool TryGetDocumentOption(OptionKey option, OptionSet underlyingOptions,
return false;
}
}
/// <summary>
/// A class that implements <see cref="IReadOnlyDictionary{String, String}" /> atop a <see cref="IReadOnlyDictionary{String, Object}" />
/// where we just convert the values to strings with ToString(). Ordering of the underlying dictionary is preserved, so that way
/// code that relies on the underlying ordering of the underlying dictionary isn't affected.
/// </summary>
private class StringConvertingDictionary : IReadOnlyDictionary<string, string>
{
private readonly IReadOnlyDictionary<string, object> _underlyingDictionary;
public StringConvertingDictionary(IReadOnlyDictionary<string, object> underlyingDictionary)
{
_underlyingDictionary = underlyingDictionary ?? throw new ArgumentNullException(nameof(underlyingDictionary));
}
public string this[string key] => _underlyingDictionary[key]?.ToString();
public IEnumerable<string> Keys => _underlyingDictionary.Keys;
public IEnumerable<string> Values => _underlyingDictionary.Values.Select(s => s?.ToString());
public int Count => _underlyingDictionary.Count;
public bool ContainsKey(string key) => _underlyingDictionary.ContainsKey(key);
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
{
foreach (var pair in _underlyingDictionary)
{
yield return new KeyValuePair<string, string>(pair.Key, pair.Value?.ToString());
}
}
public bool TryGetValue(string key, out string value)
{
if (_underlyingDictionary.TryGetValue(key, out object objectValue))
{
value = objectValue?.ToString();
return true;
}
else
{
value = null;
return false;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
}
}
using System;
using System.Collections.Generic;
using System.Composition;
using System.IO;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.Editor.Options;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.VisualStudio.CodingConventions;
using Microsoft.VisualStudio.Composition;
using Xunit;
namespace Microsoft.CodeAnalysis.UnitTests.EditorConfigStorageLocation
{
[UseExportProvider]
public class LegacyEditorConfigDocumentOptionsProviderTests
{
[Fact]
public void OrderingOfEditorConfigMaintained()
{
using var tempRoot = new TempRoot();
var tempDirectory = tempRoot.CreateDirectory();
// Write out an .editorconfig. We'll write out 100 random GUIDs
var expectedKeysInOrder = new List<string>();
using (var writer = new StreamWriter(tempDirectory.CreateFile(".editorconfig").Path))
{
writer.WriteLine("root = true");
writer.WriteLine("[*.cs]");
for (int i = 0; i < 100; i++)
{
var key = Guid.NewGuid().ToString();
expectedKeysInOrder.Add(key);
writer.WriteLine($"{key} = value");
}
}
// Create a workspace with a file in that path
var codingConventionsCatalog = ExportProviderCache.GetOrCreateAssemblyCatalog(typeof(ICodingConventionsManager).Assembly).WithPart(typeof(MockFileWatcher));
var exportProvider = ExportProviderCache.GetOrCreateExportProviderFactory(TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic.WithParts(codingConventionsCatalog)).CreateExportProvider();
using var workspace = TestWorkspace.CreateWorkspace(
new XElement("Workspace",
new XElement("Project", new XAttribute("Language", "C#"),
new XElement("Document", new XAttribute("FilePath", tempDirectory.CreateFile("Test.cs").Path)))), exportProvider: exportProvider);
var document = workspace.CurrentSolution.Projects.Single().Documents.Single();
var providerFactory = workspace.ExportProvider.GetExportedValues<IDocumentOptionsProviderFactory>().OfType<LegacyEditorConfigDocumentOptionsProviderFactory>().Single();
var provider = providerFactory.TryCreate(workspace);
var option = new Option<List<string>>(nameof(LegacyEditorConfigDocumentOptionsProviderTests), nameof(OrderingOfEditorConfigMaintained), null, new[] { new KeysReturningStorageLocation() });
var optionKey = new OptionKey(option);
// Fetch the underlying option order with a "option" that returns the keys
provider.GetOptionsForDocumentAsync(document, CancellationToken.None).Result.TryGetDocumentOption(optionKey, workspace.Options, out object actualKeysInOrderObject);
var actualKeysInOrder = Assert.IsAssignableFrom<IEnumerable<string>>(actualKeysInOrderObject);
Assert.Equal(expectedKeysInOrder, actualKeysInOrder);
}
[PartNotDiscoverable]
[Export(typeof(IFileWatcher))]
[Shared]
private class MockFileWatcher : IFileWatcher
{
#pragma warning disable CS0067 // the event is unused
public event ConventionsFileChangedAsyncEventHandler ConventionFileChanged;
public event ContextFileMovedAsyncEventHandler ContextFileMoved;
#pragma warning restore CS0067
public void Dispose()
{
}
public void StartWatching(string fileName, string directoryPath)
{
}
public void StopWatching(string fileName, string directoryPath)
{
}
}
/// <summary>
/// An option storage location that returns as the value all the keys in the order they came from the underlying storage.
/// </summary>
private class KeysReturningStorageLocation : OptionStorageLocation, IEditorConfigStorageLocation
{
public bool TryGetOption(object underlyingOption, IReadOnlyDictionary<string, string> rawOptions, Type type, out object value)
{
value = rawOptions.Keys;
return true;
}
}
}
}
......@@ -56,6 +56,7 @@
<Reference Include="System.Xml.Linq" />
<Reference Include="WindowsBase" />
<PackageReference Include="BasicUndo" Version="$(BasicUndoVersion)" />
<PackageReference Include="Microsoft.VisualStudio.CodingConventions" Version="$(MicrosoftVisualStudioCodingConventionsVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Composition" Version="$(MicrosoftVisualStudioCompositionVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Platform.VSEditor" Version="$(MicrosoftVisualStudioPlatformVSEditorVersion)" />
<PackageReference Include="Microsoft.VisualStudio.InteractiveWindow" Version="$(MicrosoftVisualStudioInteractiveWindowVersion)" />
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册