diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs index 35819de23..b10bfb8c7 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs @@ -1137,6 +1137,12 @@ protected virtual async Task CreateProjectImplAsync(EvaluatedProjectSta var documentId = DocumentId.CreateNewId(additionalProjectId, debugName: newFileName); solution = solution.AddAdditionalDocument(documentId, newFileName, source, filePath: newFileName); } + + foreach (var (newFileName, source) in projectState.AnalyzerConfigFiles) + { + var documentId = DocumentId.CreateNewId(additionalProjectId, debugName: newFileName); + solution = solution.AddAnalyzerConfigDocument(documentId, newFileName, source, filePath: newFileName); + } } solution = solution.AddMetadataReferences(projectId, primaryProject.AdditionalReferences); @@ -1153,6 +1159,12 @@ protected virtual async Task CreateProjectImplAsync(EvaluatedProjectSta solution = solution.AddAdditionalDocument(documentId, newFileName, source, filePath: newFileName); } + foreach (var (newFileName, source) in primaryProject.AnalyzerConfigFiles) + { + var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + solution = solution.AddAnalyzerConfigDocument(documentId, newFileName, source, filePath: newFileName); + } + solution = AddProjectReferences(solution, projectId, primaryProject.AdditionalProjectReferences.Select(name => projectIdMap[name])); foreach (var projectState in additionalProjects) { diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs index 1a0bb2f24..58489fb18 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs @@ -65,13 +65,15 @@ protected static bool CodeActionExpected(SolutionState state) || state.MarkupHandling != null || state.Sources.Any() || state.AdditionalFiles.Any() + || state.AnalyzerConfigFiles.Any() || state.AdditionalFilesFactories.Any(); } protected static bool HasAnyChange(SolutionState oldState, SolutionState newState) { return !oldState.Sources.SequenceEqual(newState.Sources, SourceFileEqualityComparer.Instance) - || !oldState.AdditionalFiles.SequenceEqual(newState.AdditionalFiles, SourceFileEqualityComparer.Instance); + || !oldState.AdditionalFiles.SequenceEqual(newState.AdditionalFiles, SourceFileEqualityComparer.Instance) + || !oldState.AnalyzerConfigFiles.SequenceEqual(newState.AnalyzerConfigFiles, SourceFileEqualityComparer.Instance); } protected static CodeAction? TryGetCodeActionToApply(ImmutableArray actions, int? codeActionIndex, string? codeActionEquivalenceKey, Action? codeActionVerifier, IVerifier verifier) diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/ProjectExtensions.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/ProjectExtensions.cs new file mode 100644 index 000000000..c11cdb8a1 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/ProjectExtensions.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Microsoft.CodeAnalysis.Testing +{ + internal static class ProjectExtensions + { + private static readonly Func> s_analyzerConfigDocuments; + + static ProjectExtensions() + { + var analyzerConfigDocumentType = typeof(Project).GetTypeInfo().Assembly.GetType("Microsoft.CodeAnalysis.AnalyzerConfigDocument"); + if (analyzerConfigDocumentType is { }) + { + var analyzerConfigDocumentsProperty = typeof(Project).GetProperty(nameof(AnalyzerConfigDocuments), typeof(IEnumerable<>).MakeGenericType(analyzerConfigDocumentType)); + if (analyzerConfigDocumentsProperty is { GetMethod: { } getMethod }) + { + s_analyzerConfigDocuments = (Func>)getMethod.CreateDelegate(typeof(Func>), target: null); + } + else + { + s_analyzerConfigDocuments = project => Enumerable.Empty(); + } + } + else + { + s_analyzerConfigDocuments = project => Enumerable.Empty(); + } + } + + public static IEnumerable AnalyzerConfigDocuments(this Project project) + => s_analyzerConfigDocuments(project); + } +} diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/SolutionExtensions.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/SolutionExtensions.cs new file mode 100644 index 000000000..2b4e18236 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/SolutionExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Testing +{ + internal static class SolutionExtensions + { + private static readonly Func?, string?, Solution> s_addAnalyzerConfigDocument; + + static SolutionExtensions() + { + var methodInfo = typeof(Solution).GetMethod(nameof(AddAnalyzerConfigDocument), new[] { typeof(DocumentId), typeof(string), typeof(SourceText), typeof(IEnumerable), typeof(string) }); + if (methodInfo is { }) + { + s_addAnalyzerConfigDocument = (Func?, string?, Solution>)methodInfo.CreateDelegate(typeof(Func, string, Solution>), target: null); + } + else + { + s_addAnalyzerConfigDocument = (solution, documentId, name, text, folders, filePath) => throw new NotSupportedException(); + } + } + + public static Solution AddAnalyzerConfigDocument(this Solution solution, DocumentId documentId, string name, SourceText text, IEnumerable? folders = null, string? filePath = null) + { + return s_addAnalyzerConfigDocument(solution, documentId, name, text, folders, filePath); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj index 073fd7155..d108fdc13 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Model/EvaluatedProjectState.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Model/EvaluatedProjectState.cs index 138dcb23e..104b30880 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Model/EvaluatedProjectState.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Model/EvaluatedProjectState.cs @@ -22,6 +22,7 @@ public EvaluatedProjectState(ProjectState state, ReferenceAssemblies defaultRefe state.DocumentationMode ?? DocumentationMode.Diagnose, state.Sources.ToImmutableArray(), state.AdditionalFiles.ToImmutableArray(), + state.AnalyzerConfigFiles.ToImmutableArray(), state.AdditionalProjectReferences.ToImmutableArray(), state.AdditionalReferences.ToImmutableArray()) { @@ -36,6 +37,7 @@ private EvaluatedProjectState( DocumentationMode documentationMode, ImmutableArray<(string filename, SourceText content)> sources, ImmutableArray<(string filename, SourceText content)> additionalFiles, + ImmutableArray<(string filename, SourceText content)> analyzerConfigFiles, ImmutableArray additionalProjectReferences, ImmutableArray additionalReferences) { @@ -47,6 +49,7 @@ private EvaluatedProjectState( DocumentationMode = documentationMode; Sources = sources; AdditionalFiles = additionalFiles; + AnalyzerConfigFiles = analyzerConfigFiles; AdditionalProjectReferences = additionalProjectReferences; AdditionalReferences = additionalReferences; } @@ -67,6 +70,8 @@ private EvaluatedProjectState( public ImmutableArray<(string filename, SourceText content)> AdditionalFiles { get; } + public ImmutableArray<(string filename, SourceText content)> AnalyzerConfigFiles { get; } + public ImmutableArray AdditionalProjectReferences { get; } public ImmutableArray AdditionalReferences { get; } @@ -90,6 +95,7 @@ private EvaluatedProjectState With( Optional documentationMode = default, Optional> sources = default, Optional> additionalFiles = default, + Optional> analyzerConfigFiles = default, Optional> additionalProjectReferences = default, Optional> additionalReferences = default) { @@ -102,6 +108,7 @@ private EvaluatedProjectState With( GetValueOrDefault(documentationMode, DocumentationMode), GetValueOrDefault(sources, Sources), GetValueOrDefault(additionalFiles, AdditionalFiles), + GetValueOrDefault(analyzerConfigFiles, AnalyzerConfigFiles), GetValueOrDefault(additionalProjectReferences, AdditionalProjectReferences), GetValueOrDefault(additionalReferences, AdditionalReferences)); } diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectState.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectState.cs index 8131415cc..4dcbd966e 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectState.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectState.cs @@ -33,6 +33,7 @@ internal ProjectState(ProjectState sourceState) Sources.AddRange(sourceState.Sources); AdditionalFiles.AddRange(sourceState.AdditionalFiles); + AnalyzerConfigFiles.AddRange(sourceState.AnalyzerConfigFiles); AdditionalFilesFactories.AddRange(sourceState.AdditionalFilesFactories); AdditionalProjectReferences.AddRange(sourceState.AdditionalProjectReferences); } @@ -65,6 +66,8 @@ internal ProjectState(ProjectState sourceState) public SourceFileCollection AdditionalFiles { get; } = new SourceFileCollection(); + public SourceFileCollection AnalyzerConfigFiles { get; } = new SourceFileCollection(); + public List>> AdditionalFilesFactories { get; } = new List>>(); public List AdditionalProjectReferences { get; } = new List(); diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt index 842920f86..4f0c18c6f 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt @@ -122,6 +122,7 @@ Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState.AdditionalFiles.get -> System.Collections.Immutable.ImmutableArray<(string filename, Microsoft.CodeAnalysis.Text.SourceText content)> Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState.AdditionalProjectReferences.get -> System.Collections.Immutable.ImmutableArray Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState.AdditionalReferences.get -> System.Collections.Immutable.ImmutableArray +Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState.AnalyzerConfigFiles.get -> System.Collections.Immutable.ImmutableArray<(string filename, Microsoft.CodeAnalysis.Text.SourceText content)> Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState.AssemblyName.get -> string Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState.DocumentationMode.get -> Microsoft.CodeAnalysis.DocumentationMode Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState.EvaluatedProjectState(Microsoft.CodeAnalysis.Testing.ProjectState state, Microsoft.CodeAnalysis.Testing.ReferenceAssemblies defaultReferenceAssemblies) -> void @@ -144,6 +145,7 @@ Microsoft.CodeAnalysis.Testing.ProjectState.AdditionalFiles.get -> Microsoft.Cod Microsoft.CodeAnalysis.Testing.ProjectState.AdditionalFilesFactories.get -> System.Collections.Generic.List>> Microsoft.CodeAnalysis.Testing.ProjectState.AdditionalProjectReferences.get -> System.Collections.Generic.List Microsoft.CodeAnalysis.Testing.ProjectState.AdditionalReferences.get -> Microsoft.CodeAnalysis.Testing.MetadataReferenceCollection +Microsoft.CodeAnalysis.Testing.ProjectState.AnalyzerConfigFiles.get -> Microsoft.CodeAnalysis.Testing.SourceFileCollection Microsoft.CodeAnalysis.Testing.ProjectState.AssemblyName.get -> string Microsoft.CodeAnalysis.Testing.ProjectState.DocumentationMode.get -> Microsoft.CodeAnalysis.DocumentationMode? Microsoft.CodeAnalysis.Testing.ProjectState.DocumentationMode.set -> void diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/SolutionState.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/SolutionState.cs index 21be8da19..ac58cd419 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/SolutionState.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/SolutionState.cs @@ -140,6 +140,11 @@ public SolutionState WithInheritedValuesApplied(SolutionState? baseState, Immuta result.AdditionalFiles.AddRange(baseState.AdditionalFiles); } + if (AnalyzerConfigFiles.Count == 0) + { + result.AnalyzerConfigFiles.AddRange(baseState.AnalyzerConfigFiles); + } + if (AdditionalProjects.Count == 0) { result.AdditionalProjects.AddRange(baseState.AdditionalProjects); @@ -172,6 +177,7 @@ public SolutionState WithInheritedValuesApplied(SolutionState? baseState, Immuta result.InheritanceMode = StateInheritanceMode.Explicit; result.Sources.AddRange(Sources); result.AdditionalFiles.AddRange(AdditionalFiles); + result.AnalyzerConfigFiles.AddRange(AnalyzerConfigFiles); result.AdditionalProjects.AddRange(AdditionalProjects); result.AdditionalProjectReferences.AddRange(AdditionalProjectReferences); result.AdditionalReferences.AddRange(AdditionalReferences); @@ -202,6 +208,11 @@ private static bool HasAnyContentChanges(bool willInherit, SolutionState state, return true; } + if ((!willInherit || state.AnalyzerConfigFiles.Any()) && !ContentEqual(state.AnalyzerConfigFiles, baseState.AnalyzerConfigFiles)) + { + return true; + } + if ((!willInherit || state.AdditionalReferences.Any()) && !state.AdditionalReferences.SequenceEqual(baseState.AdditionalReferences)) { return true; @@ -241,8 +252,8 @@ private static bool ContentEqual(SourceFileCollection x, SourceFileCollection y) /// /// Processes the markup syntax for this according to the current /// , and returns a new with the - /// , , and - /// updated accordingly. + /// , , + /// , and updated accordingly. /// /// Additional options to apply during markup processing. /// The diagnostic descriptor to use for markup spans without an explicit name, @@ -265,7 +276,8 @@ public SolutionState WithProcessedMarkup(MarkupOptions markupOptions, Diagnostic var markupLocations = ImmutableDictionary.Empty; (var expected, var testSources) = ProcessMarkupSources(Sources, ExpectedDiagnostics, ref markupLocations, markupOptions, defaultDiagnostic, supportedDiagnostics, fixableDiagnostics, defaultPath); - var (additionalExpected, additionalFiles) = ProcessMarkupSources(AdditionalFiles.Concat(AdditionalFilesFactories.SelectMany(factory => factory())), expected, ref markupLocations, markupOptions, defaultDiagnostic, supportedDiagnostics, fixableDiagnostics, defaultPath); + var (additionalExpected1, additionalFiles) = ProcessMarkupSources(AdditionalFiles.Concat(AdditionalFilesFactories.SelectMany(factory => factory())), expected, ref markupLocations, markupOptions, defaultDiagnostic, supportedDiagnostics, fixableDiagnostics, defaultPath); + var (additionalExpected, analyzerConfigFiles) = ProcessMarkupSources(AnalyzerConfigFiles, additionalExpected1, ref markupLocations, markupOptions, defaultDiagnostic, supportedDiagnostics, fixableDiagnostics, defaultPath); var result = new SolutionState(Name, Language, DefaultPrefix, DefaultExtension); result.MarkupHandling = MarkupMode.None; @@ -275,11 +287,13 @@ public SolutionState WithProcessedMarkup(MarkupOptions markupOptions, Diagnostic result.DocumentationMode = DocumentationMode; result.Sources.AddRange(testSources); result.AdditionalFiles.AddRange(additionalFiles); + result.AnalyzerConfigFiles.AddRange(analyzerConfigFiles); foreach (var (projectName, projectState) in AdditionalProjects) { var (correctedIntermediateDiagnostics, additionalProjectSources) = ProcessMarkupSources(projectState.Sources, additionalExpected, ref markupLocations, markupOptions, defaultDiagnostic, supportedDiagnostics, fixableDiagnostics, defaultPath); - var (correctedDiagnostics, additionalProjectAdditionalFiles) = ProcessMarkupSources(projectState.AdditionalFiles.Concat(projectState.AdditionalFilesFactories.SelectMany(factory => factory())), correctedIntermediateDiagnostics, ref markupLocations, markupOptions, defaultDiagnostic, supportedDiagnostics, fixableDiagnostics, defaultPath); + var (correctedDiagnostics1, additionalProjectAdditionalFiles) = ProcessMarkupSources(projectState.AdditionalFiles.Concat(projectState.AdditionalFilesFactories.SelectMany(factory => factory())), correctedIntermediateDiagnostics, ref markupLocations, markupOptions, defaultDiagnostic, supportedDiagnostics, fixableDiagnostics, defaultPath); + var (correctedDiagnostics, additionalProjectAnalyzerConfigFiles) = ProcessMarkupSources(projectState.AnalyzerConfigFiles, correctedDiagnostics1, ref markupLocations, markupOptions, defaultDiagnostic, supportedDiagnostics, fixableDiagnostics, defaultPath); var processedProjectState = new ProjectState(projectState); processedProjectState.Sources.Clear(); @@ -287,6 +301,8 @@ public SolutionState WithProcessedMarkup(MarkupOptions markupOptions, Diagnostic processedProjectState.AdditionalFiles.Clear(); processedProjectState.AdditionalFilesFactories.Clear(); processedProjectState.AdditionalFiles.AddRange(additionalProjectAdditionalFiles); + processedProjectState.AnalyzerConfigFiles.Clear(); + processedProjectState.AnalyzerConfigFiles.AddRange(additionalProjectAnalyzerConfigFiles); result.AdditionalProjects.Add(projectName, processedProjectState); additionalExpected = correctedDiagnostics; diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs index adc7a363d..fc4738bd2 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs @@ -413,7 +413,7 @@ private async Task VerifyFixAsync( { var actual = await GetSourceTextFromDocumentAsync(updatedDocuments[i], cancellationToken).ConfigureAwait(false); verifier.EqualOrDiff(newState.Sources[i].content.ToString(), actual.ToString(), $"content of '{newState.Sources[i].filename}' did not match. Diff shown with expected as baseline:"); - verifier.Equal(newState.Sources[i].content.Encoding, actual.Encoding, $"encoding of '{newState.Sources[i].filename}' was expected to be '{newState.Sources[i].content.Encoding}' but was '{actual.Encoding}'"); + verifier.Equal(newState.Sources[i].content.Encoding, actual.Encoding, $"encoding of '{newState.Sources[i].filename}' was expected to be '{newState.Sources[i].content.Encoding?.WebName}' but was '{actual.Encoding?.WebName}'"); verifier.Equal(newState.Sources[i].content.ChecksumAlgorithm, actual.ChecksumAlgorithm, $"checksum algorithm of '{newState.Sources[i].filename}' was expected to be '{newState.Sources[i].content.ChecksumAlgorithm}' but was '{actual.ChecksumAlgorithm}'"); verifier.Equal(newState.Sources[i].filename, updatedDocuments[i].Name, $"file name was expected to be '{newState.Sources[i].filename}' but was '{updatedDocuments[i].Name}'"); } @@ -426,11 +426,24 @@ private async Task VerifyFixAsync( { var actual = await updatedAdditionalDocuments[i].GetTextAsync(cancellationToken).ConfigureAwait(false); verifier.EqualOrDiff(newState.AdditionalFiles[i].content.ToString(), actual.ToString(), $"content of '{newState.AdditionalFiles[i].filename}' did not match. Diff shown with expected as baseline:"); - verifier.Equal(newState.AdditionalFiles[i].content.Encoding, actual.Encoding, $"encoding of '{newState.AdditionalFiles[i].filename}' was expected to be '{newState.AdditionalFiles[i].content.Encoding}' but was '{actual.Encoding}'"); + verifier.Equal(newState.AdditionalFiles[i].content.Encoding, actual.Encoding, $"encoding of '{newState.AdditionalFiles[i].filename}' was expected to be '{newState.AdditionalFiles[i].content.Encoding?.WebName}' but was '{actual.Encoding?.WebName}'"); verifier.Equal(newState.AdditionalFiles[i].content.ChecksumAlgorithm, actual.ChecksumAlgorithm, $"checksum algorithm of '{newState.AdditionalFiles[i].filename}' was expected to be '{newState.AdditionalFiles[i].content.ChecksumAlgorithm}' but was '{actual.ChecksumAlgorithm}'"); verifier.Equal(newState.AdditionalFiles[i].filename, updatedAdditionalDocuments[i].Name, $"file name was expected to be '{newState.AdditionalFiles[i].filename}' but was '{updatedAdditionalDocuments[i].Name}'"); } + var updatedAnalyzerConfigDocuments = project.AnalyzerConfigDocuments().ToArray(); + + verifier.Equal(newState.AnalyzerConfigFiles.Count, updatedAnalyzerConfigDocuments.Length, $"expected '{nameof(newState)}.{nameof(SolutionState.AnalyzerConfigFiles)}' and '{nameof(updatedAnalyzerConfigDocuments)}' to be equal but '{nameof(newState)}.{nameof(SolutionState.AnalyzerConfigFiles)}' contains '{newState.AnalyzerConfigFiles.Count}' documents and '{nameof(updatedAnalyzerConfigDocuments)}' contains '{updatedAnalyzerConfigDocuments.Length}' documents"); + + for (var i = 0; i < updatedAnalyzerConfigDocuments.Length; i++) + { + var actual = await updatedAnalyzerConfigDocuments[i].GetTextAsync(cancellationToken).ConfigureAwait(false); + verifier.EqualOrDiff(newState.AnalyzerConfigFiles[i].content.ToString(), actual.ToString(), $"content of '{newState.AnalyzerConfigFiles[i].filename}' did not match. Diff shown with expected as baseline:"); + verifier.Equal(newState.AnalyzerConfigFiles[i].content.Encoding, actual.Encoding, $"encoding of '{newState.AnalyzerConfigFiles[i].filename}' was expected to be '{newState.AnalyzerConfigFiles[i].content.Encoding?.WebName}' but was '{actual.Encoding?.WebName}'"); + verifier.Equal(newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm, actual.ChecksumAlgorithm, $"checksum algorithm of '{newState.AnalyzerConfigFiles[i].filename}' was expected to be '{newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm}' but was '{actual.ChecksumAlgorithm}'"); + verifier.Equal(newState.AnalyzerConfigFiles[i].filename, updatedAnalyzerConfigDocuments[i].Name, $"file name was expected to be '{newState.AnalyzerConfigFiles[i].filename}' but was '{updatedAnalyzerConfigDocuments[i].Name}'"); + } + // Validate the iteration counts after validating the content iterationCountFailure?.Throw(); } diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/AnalyzerConfigFilesTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/AnalyzerConfigFilesTests.cs new file mode 100644 index 000000000..89d7cff77 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/AnalyzerConfigFilesTests.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !NETCOREAPP1_1 && !NET46 + +using System; +using System.Collections.Immutable; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Xunit; +using CSharpTest = Microsoft.CodeAnalysis.Testing.TestAnalyzers.CSharpAnalyzerTest< + Microsoft.CodeAnalysis.Testing.AnalyzerConfigFilesTests.HighlightBracesIfAnalyzerConfigMissingAnalyzer>; + +namespace Microsoft.CodeAnalysis.Testing +{ + public class AnalyzerConfigFilesTests + { + private const string RootEditorConfig = @" +root = true + +[*] +key = value +"; + + [Fact] + public async Task TestDiagnosticInNormalFile() + { + await new CSharpTest + { + TestState = + { + Sources = { "namespace MyNamespace { }" }, + ExpectedDiagnostics = { new DiagnosticResult(HighlightBracesIfAnalyzerConfigMissingAnalyzer.Descriptor).WithLocation(1, 23) }, + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TestDiagnosticInNormalFileNotDeclared() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest + { + TestState = + { + Sources = { "namespace MyNamespace { }" }, + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + }.RunAsync(); + }); + + var expected = + "Mismatch between number of diagnostics returned, expected \"0\" actual \"1\"" + Environment.NewLine + + Environment.NewLine + + "Diagnostics:" + Environment.NewLine + + "// /0/Test0.cs(1,23): warning Brace: message" + Environment.NewLine + + "VerifyCS.Diagnostic().WithSpan(1, 23, 1, 24)," + Environment.NewLine + + Environment.NewLine; + Assert.Equal(expected, exception.Message); + } + + [Fact] + public async Task TestDiagnosticInAnalyzerConfigFileWithCombinedSyntaxDuplicate() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest + { + TestState = + { + Sources = { "[assembly: System.Reflection.AssemblyVersion{|#0:(|}\"1.0.0.0\")]" }, + ExpectedDiagnostics = { new DiagnosticResult(HighlightBracesIfAnalyzerConfigMissingAnalyzer.Descriptor).WithLocation(0) }, + AnalyzerConfigFiles = + { + ("/.editorconfig", "# Content with {|#0:{|} braces }"), + }, + }, + }.RunAsync(); + }); + + var expected = "Input contains multiple markup locations with key '#0'"; + new DefaultVerifier().EqualOrDiff(expected, exception.Message); + } + + [Fact] + public async Task TestDiagnosticInAnalyzerConfigFileBraceNotTreatedAsMarkup() + { + var editorConfig = @" +root = true + +[*] +key = {|Literal:value|} +"; + + await new CSharpTest + { + TestState = + { + Sources = { "namespace MyNamespace { }" }, + ExpectedDiagnostics = { new DiagnosticResult(HighlightBracesIfAnalyzerConfigMissingAnalyzer.Descriptor).WithLocation(1, 23) }, + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(editorConfig, Encoding.UTF8)), + }, + MarkupHandling = MarkupMode.None, + }, + }.RunAsync(); + } + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class HighlightBracesIfAnalyzerConfigMissingAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("Brace", "title", "message", "category", DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSyntaxTreeAction(HandleSyntaxTree); + } + + private void HandleSyntaxTree(SyntaxTreeAnalysisContext context) + { + if (!context.Options.AnalyzerConfigOptionsProvider.GetOptions(context.Tree).TryGetValue("key", out _)) + { + return; + } + + foreach (var token in context.Tree.GetRoot(context.CancellationToken).DescendantTokens()) + { + if (!token.IsKind(SyntaxKind.OpenBraceToken)) + { + continue; + } + + context.ReportDiagnostic(Diagnostic.Create(Descriptor, token.GetLocation())); + } + } + } + } +} + +#endif diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/AnalyzerConfigFilesFixTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/AnalyzerConfigFilesFixTests.cs new file mode 100644 index 000000000..fc5247108 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/AnalyzerConfigFilesFixTests.cs @@ -0,0 +1,396 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !NETCOREAPP1_1 && !NET46 + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.TestFixes; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace Microsoft.CodeAnalysis.Testing +{ + public class AnalyzerConfigFilesFixTests + { + private const string RootEditorConfig = @" +root = true + +[*] +key = value +"; + + [Fact] + public async Task TestDiagnosticFixedByAddingAnalyzerConfigFile() + { + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileExists) + { + TestState = + { + Sources = { "namespace MyNamespace { }" }, + ExpectedDiagnostics = { new DiagnosticResult(HighlightBracesAnalyzer.Descriptor).WithLocation(1, 23) }, + }, + FixedState = + { + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TestMarkupDiagnosticFixedByAddingAnalyzerConfigFile() + { + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileExists) + { + TestState = + { + Sources = { "namespace MyNamespace {|Brace:{|} }" }, + }, + FixedState = + { + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TestMarkupDiagnosticFixedByAddingAnalyzerConfigFileFailsIfTextIncorrect() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileExists) + { + TestState = + { + Sources = { "namespace MyNamespace {|Brace:{|} }" }, + }, + FixedState = + { + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig + "# Wrong line", Encoding.UTF8)), + }, + }, + }.RunAsync(); + }); + + new DefaultVerifier().EqualOrDiff($"Context: Iterative code fix application{Environment.NewLine}content of '/.editorconfig' did not match. Diff shown with expected as baseline:{Environment.NewLine} {Environment.NewLine} root = true{Environment.NewLine} {Environment.NewLine} [*]{Environment.NewLine} key = value{Environment.NewLine}-# Wrong line{Environment.NewLine}+{Environment.NewLine}", exception.Message); + } + + [Fact] + public async Task TestMarkupDiagnosticFixedByAddingAnalyzerConfigFileFailsIfEncodingIncorrect() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileExists) + { + TestState = + { + Sources = { "namespace MyNamespace {|Brace:{|} }" }, + }, + FixedState = + { + AnalyzerConfigFiles = + { + ("/.editorconfig", RootEditorConfig), + }, + }, + }.RunAsync(); + }); + + new DefaultVerifier().EqualOrDiff($"Context: Iterative code fix application{Environment.NewLine}encoding of '/.editorconfig' was expected to be '' but was 'utf-8'", exception.Message); + } + + [Fact] + public async Task TestDiagnosticFixedByRemovingAnalyzerConfigFile() + { + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileMissing) + { + TestState = + { + Sources = { "namespace MyNamespace { }" }, + ExpectedDiagnostics = { new DiagnosticResult(HighlightBracesAnalyzer.Descriptor).WithLocation(1, 23) }, + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + FixedState = + { + InheritanceMode = StateInheritanceMode.Explicit, + Sources = { "namespace MyNamespace { }" }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TestDiagnosticFixedByRemovingAnalyzerConfigFileWithUndeclaredCompileError() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileMissing) + { + TestState = + { + Sources = { "namespace MyNamespace {" }, + ExpectedDiagnostics = + { + new DiagnosticResult(HighlightBracesAnalyzer.Descriptor).WithLocation(1, 23), + DiagnosticResult.CompilerError("CS1513").WithLocation(1, 24), + }, + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + FixedState = + { + // When Explicit mode is used, compile errors in the original ExpectedDiagnostics are not inherited. + InheritanceMode = StateInheritanceMode.Explicit, + Sources = { "namespace MyNamespace {" }, + }, + }.RunAsync(); + }); + + var expected = + "Context: Diagnostics of fixed state" + Environment.NewLine + + "Mismatch between number of diagnostics returned, expected \"0\" actual \"1\"" + Environment.NewLine + + Environment.NewLine + + "Diagnostics:" + Environment.NewLine + + "// /0/Test0.cs(1,24): error CS1513: } expected" + Environment.NewLine + + "DiagnosticResult.CompilerError(\"CS1513\").WithSpan(1, 24, 1, 24)," + Environment.NewLine + + Environment.NewLine; + new DefaultVerifier().EqualOrDiff(expected, exception.Message); + } + + [Fact] + public async Task TestMarkupDiagnosticFixedByRemovingAnalyzerConfigFile() + { + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileMissing) + { + TestState = + { + Sources = { "namespace MyNamespace {|Brace:{|} }" }, + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + FixedState = + { + InheritanceMode = StateInheritanceMode.Explicit, + Sources = { "namespace MyNamespace { }" }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TestMarkupDiagnosticFixedByRemovingAnalyzerConfigFileWithCompileError() + { + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileMissing) + { + TestState = + { + Sources = { "namespace MyNamespace {|Brace:{|}{|CS1513:|}" }, + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + FixedState = + { + InheritanceMode = StateInheritanceMode.Explicit, + Sources = { "namespace MyNamespace {{|CS1513:|}" }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TestMarkupDiagnosticFixedByRemovingAnalyzerConfigFileAllowsMarkupInFixedState() + { + var testCode = "namespace MyNamespace {|Brace:{|} }"; + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileMissing) + { + TestState = + { + Sources = { testCode }, + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + FixedState = + { + InheritanceMode = StateInheritanceMode.Explicit, + Sources = { testCode }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TestMarkupDiagnosticFixedByRemovingAnalyzerConfigFileAllowsMarkupInFixedStateKeepsCompileErrors() + { + var testCode = "namespace MyNamespace {|Brace:{|}{|CS1513:|}"; + await new CSharpTest(SuppressDiagnosticIf.AnalyzerConfigFileMissing) + { + TestState = + { + Sources = { testCode }, + AnalyzerConfigFiles = + { + ("/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8)), + }, + }, + FixedState = + { + InheritanceMode = StateInheritanceMode.Explicit, + Sources = { testCode }, + }, + }.RunAsync(); + } + + private enum SuppressDiagnosticIf + { + AnalyzerConfigFileExists, + AnalyzerConfigFileMissing, + } + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + private class HighlightBracesAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("Brace", "title", "message", "category", DiagnosticSeverity.Warning, isEnabledByDefault: true); + + private readonly SuppressDiagnosticIf _suppressDiagnosticIf; + + public HighlightBracesAnalyzer(SuppressDiagnosticIf suppressDiagnosticIf) + { + _suppressDiagnosticIf = suppressDiagnosticIf; + } + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSyntaxTreeAction(HandleSyntaxTree); + } + + private void HandleSyntaxTree(SyntaxTreeAnalysisContext context) + { + if (_suppressDiagnosticIf == SuppressDiagnosticIf.AnalyzerConfigFileExists && context.Options.AnalyzerConfigOptionsProvider.GetOptions(context.Tree).TryGetValue("key", out _)) + { + return; + } + else if (_suppressDiagnosticIf == SuppressDiagnosticIf.AnalyzerConfigFileMissing && !context.Options.AnalyzerConfigOptionsProvider.GetOptions(context.Tree).TryGetValue("key", out _)) + { + return; + } + + foreach (var token in context.Tree.GetRoot(context.CancellationToken).DescendantTokens()) + { + if (!token.IsKind(SyntaxKind.OpenBraceToken)) + { + continue; + } + + context.ReportDiagnostic(Diagnostic.Create(Descriptor, token.GetLocation())); + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp)] + [PartNotDiscoverable] + private class ToggleAnalyzerConfigFileFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds + => ImmutableArray.Create(HighlightBracesAnalyzer.Descriptor.Id); + + public override FixAllProvider GetFixAllProvider() + => new FixAll(); + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var hasAnalyzerConfigFiles = context.Document.Project.AnalyzerConfigDocuments.Any(); + foreach (var diagnostic in context.Diagnostics) + { + context.RegisterCodeFix( + CodeAction.Create( + "ToggleFile", + ct => CreateChangedSolution(context.Document, remove: hasAnalyzerConfigFiles, ct), + nameof(ToggleAnalyzerConfigFileFix)), + diagnostic); + } + + return Task.CompletedTask; + } + + private static Task CreateChangedSolution(Document document, bool remove, CancellationToken cancellationToken) + { + var solution = document.Project.Solution; + if (remove) + { + foreach (var config in document.Project.AnalyzerConfigDocuments) + { + solution = solution.RemoveAnalyzerConfigDocument(config.Id); + } + } + else + { + var id = DocumentId.CreateNewId(document.Project.Id, "/.editorconfig"); + solution = solution.AddAnalyzerConfigDocument(id, "/.editorconfig", SourceText.From(RootEditorConfig, Encoding.UTF8), filePath: "/.editorconfig"); + } + + return Task.FromResult(solution); + } + + private class FixAll : FixAllProvider + { + public override Task GetFixAsync(FixAllContext fixAllContext) + { + var hasAnalyzerConfigFiles = fixAllContext.Solution.Projects.Single().AnalyzerConfigDocuments.Any(); + return Task.FromResult(CodeAction.Create( + "ToggleFile", + ct => CreateChangedSolution(fixAllContext.Solution.Projects.Single().Documents.First(), remove: hasAnalyzerConfigFiles, ct), + nameof(ToggleAnalyzerConfigFileFix))); + } + } + } + + private class CSharpTest : CSharpCodeFixTest + { + private readonly SuppressDiagnosticIf _suppressDiagnosticIf; + + public CSharpTest(SuppressDiagnosticIf suppressDiagnosticIf) + { + _suppressDiagnosticIf = suppressDiagnosticIf; + } + + protected override IEnumerable GetDiagnosticAnalyzers() + { + yield return new HighlightBracesAnalyzer(_suppressDiagnosticIf); + } + } + } +} + +#endif