File: TestDiagnosticAnalyzer.cs
Web Access
Project: src\src\Framework\AspNetCoreAnalyzers\test\Microsoft.AspNetCore.App.Analyzers.Test.csproj (Microsoft.AspNetCore.App.Analyzers.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using Microsoft.AspNetCore.Analyzer.Testing;
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage;
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.EmbeddedLanguages;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
 
namespace Microsoft.AspNetCore.Analyzers;
 
internal class TestDiagnosticAnalyzerRunner : DiagnosticAnalyzerRunner
{
    public TestDiagnosticAnalyzerRunner(DiagnosticAnalyzer analyzer)
    {
        Analyzer = analyzer;
    }
 
    public DiagnosticAnalyzer Analyzer { get; }
 
    public async Task<ClassifiedSpan[]> GetClassificationSpansAsync(TextSpan textSpan, params string[] sources)
    {
        var project = CreateProjectWithReferencesInBinDir(GetType().Assembly, sources);
        var doc = project.Solution.GetDocument(project.Documents.First().Id);
 
        var result = await Classifier.GetClassifiedSpansAsync(doc, textSpan, CancellationToken.None);
 
        return result.ToArray();
    }
 
    public Task<CompletionResult> GetCompletionsAndServiceAsync(int caretPosition, params string[] sources)
    {
        var source = sources.First();
        var insertionChar = source[caretPosition - 1];
        return GetCompletionsAndServiceAsync(caretPosition, CompletionTrigger.CreateInsertionTrigger(insertionChar), sources);
    }
 
    public async Task<CompletionResult> GetCompletionsAndServiceAsync(int caretPosition, CompletionTrigger completionTrigger, params string[] sources)
    {
        var project = CreateProjectWithReferencesInBinDir(GetType().Assembly, sources);
        var doc = project.Solution.GetDocument(project.Documents.First().Id);
        var originalText = await doc.GetTextAsync().ConfigureAwait(false);
 
        var completionService = CompletionService.GetService(doc);
        var shouldTriggerCompletion = completionService.ShouldTriggerCompletion(originalText, caretPosition, completionTrigger);
 
        if (shouldTriggerCompletion)
        {
            var result = await completionService.GetCompletionsAsync(doc, caretPosition, completionTrigger);
            var completionSpan = completionService.GetDefaultCompletionListSpan(originalText, caretPosition);
 
            return new(doc, completionService, result, completionSpan, shouldTriggerCompletion);
        }
        else
        {
            return new(doc, completionService, default, default, shouldTriggerCompletion);
        }
    }
 
    private async Task<(SyntaxToken token, SemanticModel model)> TryGetStringSyntaxTokenAtPositionAsync(int caretPosition, params string[] sources)
    {
        var project = CreateProjectWithReferencesInBinDir(GetType().Assembly, sources);
        var document = project.Solution.GetDocument(project.Documents.First().Id);
 
        var semanticModel = await document.GetSemanticModelAsync(CancellationToken.None).ConfigureAwait(false);
        if (semanticModel == null)
        {
            return default;
        }
 
        var root = await document.GetSyntaxRootAsync(CancellationToken.None).ConfigureAwait(false);
        if (root == null)
        {
            return default;
        }
 
        var stringToken = root.FindToken(caretPosition);
 
        return (token: stringToken, model: semanticModel);
    }
 
    public async Task<AspNetCoreBraceMatchingResult?> GetBraceMatchesAsync(int caretPosition, params string[] sources)
    {
        var (token, model) = await TryGetStringSyntaxTokenAtPositionAsync(caretPosition, sources);
        var braceMatcher = new RoutePatternBraceMatcher();
 
        return braceMatcher.FindBraces(model, token, caretPosition, CancellationToken.None);
    }
 
    public async Task<List<AspNetCoreHighlightSpan>> GetHighlightingAsync(int caretPosition, params string[] sources)
    {
        var (token, model) = await TryGetStringSyntaxTokenAtPositionAsync(caretPosition, sources);
        var highlighter = new RoutePatternHighlighter();
 
        var highlights = highlighter.GetDocumentHighlights(model, token, caretPosition, CancellationToken.None);
        return highlights.SelectMany(h => h.HighlightSpans).ToList();
    }
 
    public Task<Diagnostic[]> GetDiagnosticsAsync(params string[] sources)
    {
        var project = CreateProjectWithReferencesInBinDir(GetType().Assembly, sources);
 
        return GetDiagnosticsAsync(project);
    }
 
    private static readonly Lazy<IExportProviderFactory> ExportProviderFactory;
 
    static TestDiagnosticAnalyzerRunner()
    {
        ExportProviderFactory = new Lazy<IExportProviderFactory>(
            () =>
            {
#pragma warning disable VSTHRD011 // Use AsyncLazy<T>
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
                var assemblies = MefHostServices.DefaultAssemblies.ToList();
                assemblies.Add(RoutePatternClassifier.TestAccessor.ExternalAccessAssembly);
 
                var discovery = new AttributedPartDiscovery(Resolver.DefaultInstance, isNonPublicSupported: true);
                var parts = Task.Run(() => discovery.CreatePartsAsync(assemblies)).GetAwaiter().GetResult();
                var catalog = ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts); //.WithDocumentTextDifferencingService();
 
                var configuration = CompositionConfiguration.Create(catalog);
                var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
                return runtimeComposition.CreateExportProviderFactory();
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
#pragma warning restore VSTHRD011 // Use AsyncLazy<T>
            },
            LazyThreadSafetyMode.ExecutionAndPublication);
    }
 
    private static AdhocWorkspace CreateWorkspace()
    {
        var exportProvider = ExportProviderFactory.Value.CreateExportProvider();
        var host = MefHostServices.Create(exportProvider.AsCompositionContext());
        return new AdhocWorkspace(host);
    }
 
    public static Project CreateProjectWithReferencesInBinDir(Assembly testAssembly, params string[] source)
    {
        // The deps file in the project is incorrect and does not contain "compile" nodes for some references.
        // However these binaries are always present in the bin output. As a "temporary" workaround, we'll add
        // every dll file that's present in the test's build output as a metadatareference.
 
        Func<Workspace> createWorkspace = CreateWorkspace;
 
        var project = DiagnosticProject.Create(testAssembly, source, createWorkspace, typeof(RoutePatternClassifier));
        foreach (var assembly in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.dll"))
        {
            if (!project.MetadataReferences.Any(c => string.Equals(Path.GetFileNameWithoutExtension(c.Display), Path.GetFileNameWithoutExtension(assembly), StringComparison.OrdinalIgnoreCase)))
            {
                project = project.AddMetadataReference(MetadataReference.CreateFromFile(assembly));
            }
        }
 
        return project;
    }
 
    public Task<Diagnostic[]> GetDiagnosticsAsync(Project project)
    {
        return GetDiagnosticsAsync(new[] { project }, Analyzer, Array.Empty<string>());
    }
 
    protected override CompilationOptions ConfigureCompilationOptions(CompilationOptions options)
    {
        return options.WithOutputKind(OutputKind.ConsoleApplication);
    }
}
 
public record CompletionResult(Document Document, CompletionService Service, CompletionList Completions, TextSpan CompletionListSpan, bool ShouldTriggerCompletion);