File: QuickInfo\DiagnosticAnalyzerQuickInfoSourceTests.cs
Web Access
Project: src\src\EditorFeatures\CSharpTest\Microsoft.CodeAnalysis.CSharp.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.EditorFeatures.UnitTests)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.QuickInfo;
using Microsoft.CodeAnalysis.CSharp.RemoveUnusedMembers;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Diagnostics.CSharp;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.QuickInfo;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.QuickInfo;
 
[UseExportProvider]
[Trait(Traits.Feature, Traits.Features.QuickInfo)]
public class DiagnosticAnalyzerQuickInfoSourceTests
{
    [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/46604")]
    public async Task ErrorTitleIsShownOnDisablePragma()
    {
        await TestInMethodAsync(
            """
            #pragma warning disable CS0219$$
                        var i = 0;
            #pragma warning restore CS0219
            """, GetFormattedErrorTitle(ErrorCode.WRN_UnreferencedVarAssg));
    }
 
    [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/46604")]
    public async Task ErrorTitleIsShownOnRestorePragma()
    {
        await TestInMethodAsync(
            """
            #pragma warning disable CS0219
                        var i = 0;
            #pragma warning restore CS0219$$
            """, GetFormattedErrorTitle(ErrorCode.WRN_UnreferencedVarAssg));
    }
 
    [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/46604")]
    public async Task DisabledWarningNotExistingInCodeIsDisplayedByTitleWithoutCodeDetails()
    {
        await TestInMethodAsync(
            """
            #pragma warning disable CS0219$$
            """, GetFormattedErrorTitle(ErrorCode.WRN_UnreferencedVarAssg));
    }
 
    [WorkItem("https://github.com/dotnet/roslyn/issues/49102")]
    [WpfTheory]
    [InlineData("CS0219$$")]
    [InlineData("219$$")]
    [InlineData("0219$$")]
    [InlineData("CS02$$19")]
    [InlineData("2$$19")]
    [InlineData("02$$19")]
    public async Task PragmaWarningCompilerWarningSyntaxKinds(string warning)
    {
        // Reference: https://docs.microsoft.com/en-US/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning
        // "A comma-separated list of warning numbers. The "CS" prefix is optional."
        await TestInMethodAsync(
@$"
#pragma warning disable {warning}
", GetFormattedErrorTitle(ErrorCode.WRN_UnreferencedVarAssg));
    }
 
    [WorkItem("https://github.com/dotnet/roslyn/issues/49102")]
    [WpfTheory]
    [InlineData("#pragma warning $$CS0219", null)]
    [InlineData("#pragma warning disable$$", null)]
    [InlineData("#pragma warning disable $$true", null)]
    [InlineData("#pragma warning disable $$219.0", (int)ErrorCode.WRN_UnreferencedVarAssg)]
    [InlineData("#pragma warning disable $$219.5", (int)ErrorCode.WRN_UnreferencedVarAssg)]
    public async Task PragmaWarningDoesNotThrowInBrokenSyntax(string pragma, int? errorCode)
    {
        var expectedDescription = errorCode is int errorCodeValue
            ? GetFormattedErrorTitle((ErrorCode)errorCodeValue)
            : "";
        await TestInMethodAsync(
@$"
{pragma}
", expectedDescription);
    }
 
    [WorkItem("https://github.com/dotnet/roslyn/issues/46604")]
    [WpfTheory]
    [InlineData("#pragma warning disable $$CS0162", (int)ErrorCode.WRN_UnreachableCode)]
    [InlineData("#pragma warning disable $$CS0162, CS0219", (int)ErrorCode.WRN_UnreachableCode)]
    [InlineData("#pragma warning disable $$CS0219", (int)ErrorCode.WRN_UnreferencedVarAssg)]
    [InlineData("#pragma warning disable CS0162, $$CS0219", (int)ErrorCode.WRN_UnreferencedVarAssg)]
    [InlineData("#pragma warning disable CS0162, CS0219$$", (int)ErrorCode.WRN_UnreferencedVarAssg)]
    [InlineData("#pragma warning $$disable CS0162, CS0219", (int)ErrorCode.WRN_UnreachableCode)]
    [InlineData("#pragma warning $$disable CS0219, CS0162", (int)ErrorCode.WRN_UnreferencedVarAssg)]
    public async Task MultipleWarningsAreDisplayedDependingOnCursorPosition(string pragma, int errorCode)
    {
        await TestInMethodAsync(
@$"
{pragma}
        return;
        var i = 0;
", GetFormattedErrorTitle((ErrorCode)errorCode));
    }
 
    [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/46604")]
    public async Task ErrorTitleIsShwonInSupressMessageAttribute()
    {
        await TestAsync(
            """
            using System.Diagnostics.CodeAnalysis;
            namespace T
            {
                [SuppressMessage("CodeQuality", "IDE0051$$")]
                public class C
                {
                    private int _i;
                }
            }
            """, GetFormattedIDEAnalyzerTitle(51, nameof(AnalyzersResources.Remove_unused_private_members)), ImmutableArray<TextSpan>.Empty);
    }
 
    [WorkItem("https://github.com/dotnet/roslyn/issues/46604")]
    [WpfTheory]
    [InlineData(@"[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(""CodeQuality"", ""IDE0051$$"")]", true)]
    [InlineData(@"[System.Diagnostics.CodeAnalysis.SuppressMessage(""CodeQuality"", ""IDE0051$$"")]", true)]
    [InlineData(@"[System.Diagnostics.CodeAnalysis.SuppressMessage(""CodeQuality$$"", ""IDE0051"")]", false)]
    [InlineData(@"[System.Diagnostics.CodeAnalysis.SuppressMessage(""CodeQuality"", ""IDE0051"", Justification = ""WIP$$"")]", false)]
    [InlineData(@"[System.Diagnostics.CodeAnalysis.SuppressMessage$$(""CodeQuality"", ""IDE0051"")]", false)]
    [InlineData(@"[SuppressMessage(""CodeQuality"", ""$$IDE0051"")]", true)]
    [InlineData(@"[SuppressMessage(""CodeQuality"", ""$$IDE0051: Remove unused private member"")]", true)]
    [InlineData(@"[SuppressMessage(category: ""CodeQuality"", checkId$$: ""IDE0051: Remove unused private member"")]", true)]
    [InlineData(@"[SuppressMessage(category: ""CodeQuality"", $$checkId: ""IDE0051: Remove unused private member"")]", true)]
    [InlineData(@"[SuppressMessage(checkId$$: ""IDE0051: Remove unused private member"", category: ""CodeQuality"")]", true)]
    [InlineData(@"[SuppressMessage(checkId: ""IDE0051: Remove unused private member"", category$$: ""CodeQuality"")]", false)]
    [InlineData(@"[SuppressMessage(""CodeQuality"", DiagnosticIds.IDE0051 +$$ "": Remove unused private member"")]", true)]
    [InlineData(@"[SuppressMessage(""CodeQuality"", """" + (DiagnosticIds.IDE0051 +$$ "": Remove unused private member""))]", true)]
    [InlineData(@"[SuppressMessage(category: ""CodeQuality"", checkId$$: DiagnosticIds.IDE0051 + "": Remove unused private member"")]", true)]
    // False negative: Aliased attribute is not supported
    [InlineData("""
        [SM("CodeQuality", "IDE0051$$"
        """, false)]
    public async Task QuickInfoSuppressMessageAttributeUseCases(string suppressMessageAttribute, bool shouldShowQuickInfo)
    {
        var description = shouldShowQuickInfo
            ? GetFormattedIDEAnalyzerTitle(51, nameof(AnalyzersResources.Remove_unused_private_members))
            : null;
        await TestAsync(
@$"
using System.Diagnostics.CodeAnalysis;
using SM = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute;
namespace T
{{
    public static class DiagnosticIds
    {{
        public const string IDE0051 = ""IDE0051"";
    }}
 
    {suppressMessageAttribute}
    public class C
    {{
        private int _i;
    }}
}}
", description, ImmutableArray<TextSpan>.Empty);
    }
 
    protected static async Task AssertContentIsAsync(EditorTestWorkspace workspace, Document document, int position, string expectedDescription,
        ImmutableArray<TextSpan> relatedSpans)
    {
        var info = await GetQuickinfo(workspace, document, position);
        var description = info?.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description);
        Assert.NotNull(description);
        Assert.Equal(expectedDescription, description.Text);
        Assert.Collection(relatedSpans,
            info.RelatedSpans.Select(actualSpan => new Action<TextSpan>(expectedSpan => Assert.Equal(expectedSpan, actualSpan))).ToArray());
    }
 
    private static async Task<QuickInfoItem> GetQuickinfo(EditorTestWorkspace workspace, Document document, int position)
    {
        var sharedGlobalCache = workspace.ExportProvider.GetExportedValue<DiagnosticAnalyzerInfoCache.SharedGlobalCache>();
        var provider = new CSharpDiagnosticAnalyzerQuickInfoProvider(sharedGlobalCache);
        var info = await provider.GetQuickInfoAsync(new QuickInfoContext(document, position, SymbolDescriptionOptions.Default, CancellationToken.None));
        return info;
    }
 
    protected static async Task AssertNoContentAsync(EditorTestWorkspace workspace, Document document, int position)
    {
        var info = await GetQuickinfo(workspace, document, position);
        Assert.Null(info);
    }
 
    protected static async Task TestAsync(
        string code,
        string expectedDescription,
        ImmutableArray<TextSpan> relatedSpans,
        CSharpParseOptions parseOptions = null)
    {
        using var workspace = EditorTestWorkspace.CreateCSharp(code, parseOptions);
        var analyzerReference = new AnalyzerImageReference(ImmutableArray.Create<DiagnosticAnalyzer>(
            new CSharpCompilerDiagnosticAnalyzer(),
            new CSharpRemoveUnusedMembersDiagnosticAnalyzer()));
        workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences([analyzerReference]));
 
        var testDocument = workspace.Documents.Single();
        var position = testDocument.CursorPosition.Value;
        var document = workspace.CurrentSolution.Projects.First().Documents.First();
        if (string.IsNullOrEmpty(expectedDescription))
        {
            await AssertNoContentAsync(workspace, document, position);
        }
        else
        {
            await AssertContentIsAsync(workspace, document, position, expectedDescription, relatedSpans);
        }
    }
 
    private static string GetFormattedErrorTitle(ErrorCode errorCode)
    {
        var localizable = MessageProvider.Instance.GetTitle((int)errorCode);
        return $"CS{(int)errorCode:0000}: {localizable}";
    }
 
    private static string GetFormattedIDEAnalyzerTitle(int ideDiagnosticId, string nameOfLocalizableStringResource)
    {
        var localizable = new LocalizableResourceString(nameOfLocalizableStringResource, AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
        return $"IDE{ideDiagnosticId:0000}: {localizable}";
    }
 
    protected static Task TestInClassAsync(string code, string expectedDescription, params TextSpan[] relatedSpans)
        => TestAsync(
            """
            class C
            {
            """ + code + "}", expectedDescription, relatedSpans.ToImmutableArray());
 
    protected static Task TestInMethodAsync(string code, string expectedDescription, params TextSpan[] relatedSpans)
        => TestInClassAsync(
            """
            void M()
            {
            """ + code + "}", expectedDescription, relatedSpans);
}