File: Squiggles\ErrorSquiggleProducerTests.cs
Web Access
Project: ..\..\..\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.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Documents;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Diagnostics.SimplifyTypeNames;
using Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryImports;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests.Squiggles;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Test.Utilities.QuickInfo;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Squiggles
{
    [UseExportProvider]
    [Trait(Traits.Feature, Traits.Features.ErrorSquiggles), Trait(Traits.Feature, Traits.Features.Tagging)]
    public class ErrorSquiggleProducerTests
    {
        [WpfTheory, CombinatorialData]
        public async Task ErrorTagGeneratedForError(bool pull)
        {
            var spans = await GetTagSpansAsync("class C {", pull);
            var firstSpan = Assert.Single(spans);
            Assert.Equal(PredefinedErrorTypeNames.SyntaxError, firstSpan.Tag.ErrorType);
        }
 
        [WpfTheory, CombinatorialData]
        public async Task ErrorTagGeneratedForErrorInSourceGeneratedDocument(bool pull)
        {
            var spans = await GetTagSpansInSourceGeneratedDocumentAsync("class C {", pull);
            var firstSpan = Assert.Single(spans);
            Assert.Equal(PredefinedErrorTypeNames.SyntaxError, firstSpan.Tag.ErrorType);
        }
 
        [WpfTheory, CombinatorialData]
        public async Task ErrorTagGeneratedForWarning(bool pull)
        {
            var spans = await GetTagSpansAsync("class C { long x = 5l; }", pull);
            Assert.Equal(1, spans.Count());
            Assert.Equal(PredefinedErrorTypeNames.Warning, spans.First().Tag.ErrorType);
        }
 
        [WpfTheory, CombinatorialData]
        public async Task ErrorTagGeneratedForWarningAsError(bool pull)
        {
            var workspaceXml =
@"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"">
        <CompilationOptions ReportDiagnostic = ""Error"" />
            <Document FilePath = ""Test.cs"" >
                class Program
                {
                    void Test()
                    {
                        int a = 5;
                    }
                }
        </Document>
    </Project>
</Workspace>";
 
            using var workspace = TestWorkspace.Create(workspaceXml);
            workspace.GlobalOptions.SetGlobalOption(DiagnosticTaggingOptionsStorage.PullDiagnosticTagging, pull);
 
            var spans = (await TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.GetDiagnosticsAndErrorSpans(workspace)).Item2;
 
            Assert.Equal(1, spans.Count());
            Assert.Equal(PredefinedErrorTypeNames.SyntaxError, spans.First().Tag.ErrorType);
        }
 
        [WpfTheory, CombinatorialData]
        public async Task CustomizableTagsForUnnecessaryCode(bool pull)
        {
            var workspaceXml =
@"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"">
        <Document FilePath = ""Test.cs"" >
// System is used - rest are unused.
using System.Collections;
using System;
using System.Diagnostics;
using System.Collections.Generic;
 
class Program
{
    void Test()
    {
        Int32 x = 2; // Int32 can be simplified.
        x += 1;
    }
}
        </Document>
    </Project>
</Workspace>";
 
            using var workspace = TestWorkspace.Create(workspaceXml, composition: SquiggleUtilities.CompositionWithSolutionCrawler);
            var language = workspace.Projects.Single().Language;
 
            workspace.GlobalOptions.SetGlobalOption(DiagnosticTaggingOptionsStorage.PullDiagnosticTagging, pull);
 
            workspace.GlobalOptions.SetGlobalOption(
                CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInDeclaration, language,
                new CodeStyleOption2<bool>(value: true, notification: NotificationOption2.Error));
 
            var analyzerMap = new Dictionary<string, ImmutableArray<DiagnosticAnalyzer>>
                {
                    {
                        LanguageNames.CSharp,
                        ImmutableArray.Create<DiagnosticAnalyzer>(
                            new CSharpSimplifyTypeNamesDiagnosticAnalyzer(),
                            new CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer(),
                            new ReportOnClassWithLink())
                    }
                };
 
            var diagnosticsAndSpans = await TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.GetDiagnosticsAndErrorSpans(workspace, analyzerMap);
 
            var spans =
                diagnosticsAndSpans.Item1
                    .Zip(diagnosticsAndSpans.Item2, (diagnostic, span) => (diagnostic, span))
                    .OrderBy(s => s.span.Span.Span.Start).ToImmutableArray();
 
            Assert.Equal(4, spans.Length);
            var first = spans[0].span;
            var second = spans[1].span;
            var third = spans[2].span;
            var fourth = spans[3].span;
 
            var expectedToolTip = new ContainerElement(
                ContainerElementStyle.Wrapped,
                new ClassifiedTextElement(
                    new ClassifiedTextRun(ClassificationTypeNames.Text, "IDE0005", QuickInfoHyperLink.TestAccessor.CreateNavigationAction(new Uri("https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005", UriKind.Absolute)), "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005"),
                    new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ":"),
                    new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
                    new ClassifiedTextRun(ClassificationTypeNames.Text, CSharpAnalyzersResources.Using_directive_is_unnecessary)));
 
            Assert.Equal(PredefinedErrorTypeNames.Suggestion, first.Tag.ErrorType);
            ToolTipAssert.EqualContent(expectedToolTip, first.Tag.ToolTipContent);
            Assert.Equal(40, first.Span.Start);
            Assert.Equal(25, first.Span.Length);
 
            expectedToolTip = new ContainerElement(
                ContainerElementStyle.Wrapped,
                new ClassifiedTextElement(
                    new ClassifiedTextRun(ClassificationTypeNames.Text, "IDE0005", QuickInfoHyperLink.TestAccessor.CreateNavigationAction(new Uri("https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005", UriKind.Absolute)), "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005"),
                    new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ":"),
                    new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
                    new ClassifiedTextRun(ClassificationTypeNames.Text, CSharpAnalyzersResources.Using_directive_is_unnecessary)));
 
            Assert.Equal(PredefinedErrorTypeNames.Suggestion, second.Tag.ErrorType);
            ToolTipAssert.EqualContent(expectedToolTip, second.Tag.ToolTipContent);
            Assert.Equal(82, second.Span.Start);
            Assert.Equal(60, second.Span.Length);
 
            expectedToolTip = new ContainerElement(
                ContainerElementStyle.Wrapped,
                new ClassifiedTextElement(
                    new ClassifiedTextRun(ClassificationTypeNames.Text, "id", QuickInfoHyperLink.TestAccessor.CreateNavigationAction(new Uri("https://github.com/dotnet/roslyn", UriKind.Absolute)), "https://github.com/dotnet/roslyn"),
                    new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ":"),
                    new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
                    new ClassifiedTextRun(ClassificationTypeNames.Text, "messageFormat")));
 
            Assert.Equal(PredefinedErrorTypeNames.Warning, third.Tag.ErrorType);
            ToolTipAssert.EqualContent(expectedToolTip, third.Tag.ToolTipContent);
            Assert.Equal(152, third.Span.Start);
            Assert.Equal(7, third.Span.Length);
 
            expectedToolTip = new ContainerElement(
                ContainerElementStyle.Wrapped,
                new ClassifiedTextElement(
                    new ClassifiedTextRun(ClassificationTypeNames.Text, "IDE0049", QuickInfoHyperLink.TestAccessor.CreateNavigationAction(new Uri("https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0049", UriKind.Absolute)), "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0049"),
                    new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ":"),
                    new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
                    new ClassifiedTextRun(ClassificationTypeNames.Text, AnalyzersResources.Name_can_be_simplified)));
 
            Assert.Equal(PredefinedErrorTypeNames.SyntaxError, fourth.Tag.ErrorType);
            ToolTipAssert.EqualContent(expectedToolTip, fourth.Tag.ToolTipContent);
            Assert.Equal(196, fourth.Span.Start);
            Assert.Equal(5, fourth.Span.Length);
        }
 
        [WpfTheory, CombinatorialData]
        public async Task ErrorDoesNotCrashPastEOF(bool pull)
        {
            var spans = await GetTagSpansAsync("class C { int x =", pull);
            Assert.Equal(3, spans.Count());
        }
 
        [WpfTheory, CombinatorialData]
        public async Task SemanticErrorReported(bool pull)
        {
            using var workspace = TestWorkspace.CreateCSharp("class C : Bar { }", composition: SquiggleUtilities.CompositionWithSolutionCrawler);
 
            workspace.GlobalOptions.SetGlobalOption(DiagnosticTaggingOptionsStorage.PullDiagnosticTagging, pull);
 
            var spans = await TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.GetDiagnosticsAndErrorSpans(workspace);
 
            Assert.Equal(1, spans.Item2.Count());
 
            var firstDiagnostic = spans.Item1.First();
            var firstSpan = spans.Item2.First();
            Assert.Equal(PredefinedErrorTypeNames.SyntaxError, firstSpan.Tag.ErrorType);
 
            var expectedToolTip = new ContainerElement(
                ContainerElementStyle.Wrapped,
                new ClassifiedTextElement(
                    new ClassifiedTextRun(ClassificationTypeNames.Text, "CS0246", QuickInfoHyperLink.TestAccessor.CreateNavigationAction(new Uri("https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0246)", UriKind.Absolute)), "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0246)"),
                    new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ":"),
                    new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
                    new ClassifiedTextRun(ClassificationTypeNames.Text, firstDiagnostic.Message)));
 
            ToolTipAssert.EqualContent(expectedToolTip, firstSpan.Tag.ToolTipContent);
        }
 
        [WpfTheory, CombinatorialData]
        public async Task TestNoErrorsAfterDocumentRemoved(bool pull)
        {
            using var workspace = TestWorkspace.CreateCSharp("class");
            workspace.GlobalOptions.SetGlobalOption(DiagnosticTaggingOptionsStorage.PullDiagnosticTagging, pull);
 
            using var wrapper = new DiagnosticTaggerWrapper<DiagnosticsSquiggleTaggerProvider, IErrorTag>(workspace);
 
            var firstDocument = workspace.Documents.First();
            var tagger = wrapper.TaggerProvider.CreateTagger<IErrorTag>(firstDocument.GetTextBuffer());
            using var disposable = tagger as IDisposable;
            await wrapper.WaitForTags();
 
            var snapshot = firstDocument.GetTextBuffer().CurrentSnapshot;
            var spans = tagger.GetTags(snapshot.GetSnapshotSpanCollection()).ToList();
 
            // Initially, while the buffer is associated with a Document, we should get
            // error squiggles.
            Assert.True(spans.Count > 0);
 
            // Now remove the document.
            workspace.CloseDocument(firstDocument.Id);
            workspace.OnDocumentRemoved(firstDocument.Id);
            await wrapper.WaitForTags();
            spans = tagger.GetTags(snapshot.GetSnapshotSpanCollection()).ToList();
 
            // And we should have no errors for this document.
            Assert.True(spans.Count == 0);
        }
 
        [WpfTheory, CombinatorialData]
        public async Task TestNoErrorsAfterProjectRemoved(bool pull)
        {
            using var workspace = TestWorkspace.CreateCSharp("class");
            workspace.GlobalOptions.SetGlobalOption(DiagnosticTaggingOptionsStorage.PullDiagnosticTagging, pull);
 
            using var wrapper = new DiagnosticTaggerWrapper<DiagnosticsSquiggleTaggerProvider, IErrorTag>(workspace);
 
            var firstDocument = workspace.Documents.First();
            var tagger = wrapper.TaggerProvider.CreateTagger<IErrorTag>(firstDocument.GetTextBuffer());
            using var disposable = tagger as IDisposable;
            await wrapper.WaitForTags();
 
            var snapshot = firstDocument.GetTextBuffer().CurrentSnapshot;
            var spans = tagger.GetTags(snapshot.GetSnapshotSpanCollection()).ToList();
 
            // Initially, while the buffer is associated with a Document, we should get
            // error squiggles.
            Assert.True(spans.Count > 0);
 
            // Now remove the project.
            workspace.CloseDocument(firstDocument.Id);
            workspace.OnDocumentRemoved(firstDocument.Id);
            workspace.OnProjectRemoved(workspace.Projects.First().Id);
            await wrapper.WaitForTags();
            spans = tagger.GetTags(snapshot.GetSnapshotSpanCollection()).ToList();
 
            // And we should have no errors for this document.
            Assert.True(spans.Count == 0);
        }
 
        private static readonly TestComposition s_mockComposition = EditorTestCompositions.EditorFeatures
            .AddExcludedPartTypes(typeof(IDiagnosticAnalyzerService))
            .AddParts(typeof(MockDiagnosticAnalyzerService));
 
        [WpfTheory, CombinatorialData]
        public async Task BuildErrorZeroLengthSpan(bool pull)
        {
            var workspaceXml =
@"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"">
        <Document FilePath = ""Test.cs"" >
            class Test
{
}
        </Document>
    </Project>
</Workspace>";
 
            using var workspace = TestWorkspace.Create(workspaceXml, composition: s_mockComposition);
            workspace.GlobalOptions.SetGlobalOption(DiagnosticTaggingOptionsStorage.PullDiagnosticTagging, pull);
 
            var document = workspace.Documents.First();
 
            var updateArgs = DiagnosticsUpdatedArgs.DiagnosticsCreated(
                new object(), workspace, workspace.CurrentSolution, document.Project.Id, document.Id,
                ImmutableArray.Create(
                    TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.CreateDiagnosticData(document, new TextSpan(0, 0)),
                    TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.CreateDiagnosticData(document, new TextSpan(0, 1))));
 
            var spans = await TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.GetErrorsFromUpdateSource(workspace, updateArgs, DiagnosticKind.CompilerSyntax);
 
            if (pull)
            {
                Assert.Equal(2, spans.Count());
                var first = spans.First();
                var second = spans.Last();
 
                Assert.Equal(1, first.Span.Span.Length);
                Assert.Equal(1, second.Span.Span.Length);
            }
            else
            {
                Assert.Equal(1, spans.Count());
                var first = spans.First();
                Assert.Equal(1, first.Span.Span.Length);
            }
        }
 
        [WpfTheory, CombinatorialData]
        public async Task LiveErrorZeroLengthSpan(bool pull)
        {
            var workspaceXml =
@"<Workspace>
    <Project Language=""C#"" CommonReferences=""true"">
        <Document FilePath = ""Test.cs"" >
            class Test
{
}
        </Document>
    </Project>
</Workspace>";
 
            using var workspace = TestWorkspace.Create(workspaceXml, composition: s_mockComposition);
            workspace.GlobalOptions.SetGlobalOption(DiagnosticTaggingOptionsStorage.PullDiagnosticTagging, pull);
 
            var document = workspace.Documents.First();
 
            var updateArgs = DiagnosticsUpdatedArgs.DiagnosticsCreated(
                new LiveId(), workspace, workspace.CurrentSolution, document.Project.Id, document.Id,
                ImmutableArray.Create(
                    TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.CreateDiagnosticData(document, new TextSpan(0, 0)),
                    TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.CreateDiagnosticData(document, new TextSpan(0, 1))));
 
            var spans = await TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.GetErrorsFromUpdateSource(workspace, updateArgs, DiagnosticKind.CompilerSyntax);
 
            Assert.Equal(2, spans.Count());
            var first = spans.First();
            var second = spans.Last();
 
            Assert.Equal(1, first.Span.Span.Length);
            Assert.Equal(1, second.Span.Span.Length);
        }
 
        private class LiveId : ISupportLiveUpdate
        {
            public LiveId()
            {
            }
        }
 
        private static async Task<ImmutableArray<ITagSpan<IErrorTag>>> GetTagSpansAsync(string content, bool pull)
        {
            using var workspace = TestWorkspace.CreateCSharp(content, composition: SquiggleUtilities.CompositionWithSolutionCrawler);
            return await GetTagSpansAsync(workspace, pull);
        }
 
        private static async Task<ImmutableArray<ITagSpan<IErrorTag>>> GetTagSpansInSourceGeneratedDocumentAsync(string content, bool pull)
        {
            using var workspace = TestWorkspace.CreateCSharp(
                files: Array.Empty<string>(),
                sourceGeneratedFiles: new[] { content },
                composition: SquiggleUtilities.WpfCompositionWithSolutionCrawler);
            return await GetTagSpansAsync(workspace, pull);
        }
 
        private static async Task<ImmutableArray<ITagSpan<IErrorTag>>> GetTagSpansAsync(TestWorkspace workspace, bool pull)
        {
            workspace.GlobalOptions.SetGlobalOption(DiagnosticTaggingOptionsStorage.PullDiagnosticTagging, pull);
 
            return (await TestDiagnosticTagProducer<DiagnosticsSquiggleTaggerProvider, IErrorTag>.GetDiagnosticsAndErrorSpans(workspace)).Item2;
        }
 
        private sealed class ReportOnClassWithLink : DiagnosticAnalyzer
        {
            public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
                "id",
                "title",
                "messageFormat",
                "category",
                DiagnosticSeverity.Warning,
                isEnabledByDefault: true,
                "description",
                "https://github.com/dotnet/roslyn");
 
            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
 
            public override void Initialize(AnalysisContext context)
            {
                context.EnableConcurrentExecution();
                context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 
                context.RegisterSymbolAction(
                    context =>
                    {
                        if (!context.Symbol.IsImplicitlyDeclared && context.Symbol.Locations.First().IsInSource)
                        {
                            context.ReportDiagnostic(Diagnostic.Create(Rule, context.Symbol.Locations.First()));
                        }
                    },
                    SymbolKind.NamedType);
            }
        }
    }
}