File: Diagnostics\DiagnosticExtensions.cs
Web Access
Project: src\src\Compilers\Test\Core\Microsoft.CodeAnalysis.Test.Utilities.csproj (Microsoft.CodeAnalysis.Test.Utilities)
// 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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis
{
    public static class DiagnosticExtensions
    {
        /// <summary>
        /// This is obsolete. Use Verify instead.
        /// </summary>
        public static void VerifyErrorCodes(this IEnumerable<Diagnostic> actual, params DiagnosticDescription[] expected)
        {
            Verify(actual, expected, errorCodeOnly: true);
        }
 
        public static void VerifyErrorCodes(this ImmutableArray<Diagnostic> actual, params DiagnosticDescription[] expected)
        {
            VerifyErrorCodes((IEnumerable<Diagnostic>)actual, expected);
        }
 
        internal static void Verify(this DiagnosticBag actual, params DiagnosticDescription[] expected)
        {
            Verify(actual.AsEnumerable(), expected, errorCodeOnly: false);
        }
 
        public static void Verify(this IEnumerable<Diagnostic> actual, params DiagnosticDescription[] expected)
        {
            Verify(actual, expected, errorCodeOnly: false);
        }
 
        public static void Verify(this IEnumerable<Diagnostic> actual, bool fallbackToErrorCodeOnlyForNonEnglish, params DiagnosticDescription[] expected)
        {
            Verify(actual, expected, errorCodeOnly: fallbackToErrorCodeOnlyForNonEnglish && EnsureEnglishUICulture.PreferredOrNull != null);
        }
 
        public static void VerifyWithFallbackToErrorCodeOnlyForNonEnglish(this IEnumerable<Diagnostic> actual, params DiagnosticDescription[] expected)
        {
            Verify(actual, true, expected);
        }
 
        public static void Verify(this ImmutableArray<Diagnostic> actual, params DiagnosticDescription[] expected)
        {
            Verify((IEnumerable<Diagnostic>)actual, expected);
        }
 
        private static void Verify(IEnumerable<Diagnostic> actual, DiagnosticDescription[] expected, bool errorCodeOnly)
        {
            if (expected == null)
            {
                throw new ArgumentException("Must specify expected errors.", nameof(expected));
            }
 
            var includeDefaultSeverity = expected.Any() && expected.All(e => e.DefaultSeverity != null);
            var includeEffectiveSeverity = expected.Any() && expected.All(e => e.EffectiveSeverity != null);
            var unmatchedActualDescription = actual.Select(d => new DiagnosticDescription(d, errorCodeOnly, includeDefaultSeverity, includeEffectiveSeverity))
                                  .ToList();
            var unmatchedActualIndex = actual.Select((_, i) => i).ToList();
            var unmatchedExpected = ArrayBuilder<DiagnosticDescription>.GetInstance();
 
            // Try to match each of the 'expected' errors to one of the 'actual' ones.
            // If any of the expected errors don't appear, fail test.
            foreach (var d in expected)
            {
                int index = unmatchedActualDescription.IndexOf(d);
                if (index > -1)
                {
                    unmatchedActualDescription.RemoveAt(index);
                    unmatchedActualIndex.RemoveAt(index);
                }
                else
                {
                    unmatchedExpected.Add(d);
                }
            }
 
            if (unmatchedActualDescription.Count > 0 || unmatchedExpected.Count > 0)
            {
                var text = DiagnosticDescription.GetAssertText(expected, actual, unmatchedExpected.ToArray(), actual.Select((a, i) => (a, i)).Join(unmatchedActualIndex, ai => ai.i, i => i, (ai, _) => ai.a));
                Assert.True(false, text);
            }
 
            unmatchedExpected.Free();
        }
 
        public static TCompilation VerifyDiagnostics<TCompilation>(this TCompilation c, params DiagnosticDescription[] expected)
            where TCompilation : Compilation
        {
            var diagnostics = c.GetDiagnostics();
            diagnostics.Verify(expected);
            VerifyAssemblyIds(c, diagnostics);
 
            return c;
        }
 
        private static void VerifyAssemblyIds<TCompilation>(
            TCompilation c, ImmutableArray<Diagnostic> diagnostics) where TCompilation : Compilation
        {
            foreach (var diagnostic in diagnostics)
            {
                // If this is a diagnostic about a missing assembly, make sure that we can get back
                // an AssemblyIdentity when we query the compiler.  If it's not a diagnostic about
                // a missing assembly, make sure we get no results back.
                if (c.IsUnreferencedAssemblyIdentityDiagnosticCode(diagnostic.Code))
                {
                    var assemblyIds = c.GetUnreferencedAssemblyIdentities(diagnostic);
                    Assert.False(assemblyIds.IsEmpty);
 
                    var diagnosticMessage = diagnostic.GetMessage();
                    foreach (var id in assemblyIds)
                    {
                        Assert.Contains(id.GetDisplayName(), diagnosticMessage);
                    }
                }
                else
                {
                    var assemblyIds = c.GetUnreferencedAssemblyIdentities(diagnostic);
                    Assert.True(assemblyIds.IsEmpty);
                }
            }
        }
 
        public static void VerifyAnalyzerOccurrenceCount<TCompilation>(
            this TCompilation c,
            DiagnosticAnalyzer[] analyzers,
            int expectedCount,
            Action<Exception, DiagnosticAnalyzer, Diagnostic> onAnalyzerException = null)
            where TCompilation : Compilation
        {
            Assert.Equal(expectedCount, c.GetAnalyzerDiagnostics(analyzers, null, onAnalyzerException).Length);
        }
 
        public static TCompilation VerifyAnalyzerDiagnostics<TCompilation>(
            this TCompilation c,
            DiagnosticAnalyzer[] analyzers,
            AnalyzerOptions options = null,
            Action<Exception, DiagnosticAnalyzer, Diagnostic> onAnalyzerException = null,
            params DiagnosticDescription[] expected)
            where TCompilation : Compilation
        {
            return VerifyAnalyzerDiagnostics(c, analyzers, reportSuppressedDiagnostics: false, options, onAnalyzerException, expected);
        }
 
        public static TCompilation VerifyAnalyzerDiagnostics<TCompilation>(
            this TCompilation c,
            DiagnosticAnalyzer[] analyzers,
            bool reportSuppressedDiagnostics,
            AnalyzerOptions options = null,
            Action<Exception, DiagnosticAnalyzer, Diagnostic> onAnalyzerException = null,
            params DiagnosticDescription[] expected)
            where TCompilation : Compilation
        {
            var newCompilation = c.GetCompilationWithAnalyzerDiagnostics(analyzers, options, onAnalyzerException, reportSuppressedDiagnostics, includeCompilerDiagnostics: false, CancellationToken.None, out var diagnostics);
            diagnostics.Verify(expected);
            return newCompilation;
        }
 
        public static ImmutableArray<Diagnostic> GetAnalyzerDiagnostics<TCompilation>(
            this TCompilation c,
            DiagnosticAnalyzer[] analyzers,
            AnalyzerOptions options = null,
            Action<Exception, DiagnosticAnalyzer, Diagnostic> onAnalyzerException = null,
            CancellationToken cancellationToken = default)
            where TCompilation : Compilation
        {
            return GetAnalyzerDiagnostics(c, analyzers, reportSuppressedDiagnostics: false, options, onAnalyzerException, cancellationToken);
        }
 
        public static ImmutableArray<Diagnostic> GetAnalyzerDiagnostics<TCompilation>(
            this TCompilation c,
            DiagnosticAnalyzer[] analyzers,
            bool reportSuppressedDiagnostics,
            AnalyzerOptions options = null,
            Action<Exception, DiagnosticAnalyzer, Diagnostic> onAnalyzerException = null,
            CancellationToken cancellationToken = default)
            where TCompilation : Compilation
        {
            _ = GetCompilationWithAnalyzerDiagnostics(c, analyzers, options, onAnalyzerException, reportSuppressedDiagnostics, includeCompilerDiagnostics: false, cancellationToken, out var diagnostics);
            return diagnostics;
        }
 
        public static TCompilation VerifySuppressedDiagnostics<TCompilation>(
            this TCompilation c,
            DiagnosticAnalyzer[] analyzers,
            AnalyzerOptions options = null,
            Action<Exception, DiagnosticAnalyzer, Diagnostic> onAnalyzerException = null,
            CancellationToken cancellationToken = default,
            params DiagnosticDescription[] expected)
            where TCompilation : Compilation
        {
            // Verify suppression is unaffected by toggling /warnaserror.
            // Only perform this additional verification if the caller hasn't
            // explicitly overridden specific or general diagnostic options.
            if (c.Options.GeneralDiagnosticOption == ReportDiagnostic.Default &&
                c.Options.SpecificDiagnosticOptions.IsEmpty)
            {
                _ = c.VerifySuppressedDiagnostics(toggleWarnAsError: true, analyzers, options, onAnalyzerException, expected, cancellationToken);
            }
 
            return c.VerifySuppressedDiagnostics(toggleWarnAsError: false, analyzers, options, onAnalyzerException, expected, cancellationToken);
        }
 
        private static TCompilation VerifySuppressedDiagnostics<TCompilation>(
            this TCompilation c,
            bool toggleWarnAsError,
            DiagnosticAnalyzer[] analyzers,
            AnalyzerOptions options,
            Action<Exception, DiagnosticAnalyzer, Diagnostic> onAnalyzerException,
            DiagnosticDescription[] expectedDiagnostics,
            CancellationToken cancellationToken)
            where TCompilation : Compilation
        {
            if (toggleWarnAsError)
            {
                var toggledOption = c.Options.GeneralDiagnosticOption == ReportDiagnostic.Error ?
                    ReportDiagnostic.Default :
                    ReportDiagnostic.Error;
                c = (TCompilation)c.WithOptions(c.Options.WithGeneralDiagnosticOption(toggledOption));
 
                var builder = ArrayBuilder<DiagnosticDescription>.GetInstance(expectedDiagnostics.Length);
                foreach (var expected in expectedDiagnostics)
                {
                    // Toggle warnaserror and effective severity if following are true:
                    //  1. Default severity is not specified or specified as Warning
                    //  2. Effective severity is not specified or specified as Warning or Error
                    var defaultSeverityCheck = !expected.DefaultSeverity.HasValue ||
                        expected.DefaultSeverity.Value == DiagnosticSeverity.Warning;
                    var effectiveSeverityCheck = !expected.EffectiveSeverity.HasValue ||
                         expected.EffectiveSeverity.Value == DiagnosticSeverity.Warning ||
                         expected.EffectiveSeverity.Value == DiagnosticSeverity.Error;
 
                    DiagnosticDescription newExpected;
                    if (defaultSeverityCheck && effectiveSeverityCheck)
                    {
                        newExpected = expected.WithWarningAsError(!expected.IsWarningAsError);
 
                        if (expected.EffectiveSeverity.HasValue)
                        {
                            var newEffectiveSeverity = expected.EffectiveSeverity.Value == DiagnosticSeverity.Error ?
                                DiagnosticSeverity.Warning :
                                DiagnosticSeverity.Error;
                            newExpected = newExpected.WithEffectiveSeverity(newEffectiveSeverity);
                        }
                    }
                    else
                    {
                        newExpected = expected;
                    }
 
                    builder.Add(newExpected);
                }
 
                expectedDiagnostics = builder.ToArrayAndFree();
            }
 
            c = c.GetCompilationWithAnalyzerDiagnostics(analyzers, options, onAnalyzerException, reportSuppressedDiagnostics: true, includeCompilerDiagnostics: true, cancellationToken, out var diagnostics);
            diagnostics = diagnostics.WhereAsArray(d => d.IsSuppressed);
            diagnostics.Verify(expectedDiagnostics);
            return c; // note this is a new compilation
        }
 
        public static TCompilation VerifySuppressedAndFilteredDiagnostics<TCompilation>(
            this TCompilation c,
            DiagnosticAnalyzer[] analyzers,
            AnalyzerOptions options = null,
            Action<Exception, DiagnosticAnalyzer, Diagnostic> onAnalyzerException = null,
            CancellationToken cancellationToken = default)
            where TCompilation : Compilation
        {
            // Verify suppressed diagnostics are filtered when reportSuppressedDiagnostics is false.
            // The actual verification is handled in GetCompilationWithAnalyzerDiagnostics.
            c = c.GetCompilationWithAnalyzerDiagnostics(analyzers, options, onAnalyzerException, reportSuppressedDiagnostics: false, includeCompilerDiagnostics: true, cancellationToken, out var diagnostics);
            return c; // note this is a new compilation
        }
 
        private static TCompilation GetCompilationWithAnalyzerDiagnostics<TCompilation>(
            this TCompilation c,
            DiagnosticAnalyzer[] analyzers,
            AnalyzerOptions options,
            Action<Exception, DiagnosticAnalyzer, Diagnostic> onAnalyzerException,
            bool reportSuppressedDiagnostics,
            bool includeCompilerDiagnostics,
            CancellationToken cancellationToken,
            out ImmutableArray<Diagnostic> diagnostics)
            where TCompilation : Compilation
        {
            var analyzersArray = analyzers.ToImmutableArray();
            if (reportSuppressedDiagnostics != c.Options.ReportSuppressedDiagnostics)
            {
                c = (TCompilation)c.WithOptions(c.Options.WithReportSuppressedDiagnostics(reportSuppressedDiagnostics));
            }
 
            var analyzerManager = new AnalyzerManager(analyzersArray);
            var driver = AnalyzerDriver.CreateAndAttachToCompilation(c, analyzersArray, options, analyzerManager, onAnalyzerException,
                analyzerExceptionFilter: null, reportAnalyzer: false, severityFilter: SeverityFilter.None, trackSuppressedDiagnosticIds: false,
                out var newCompilation, cancellationToken);
            Debug.Assert(newCompilation.SemanticModelProvider != null);
            var compilerDiagnostics = newCompilation.GetDiagnostics(cancellationToken);
            var analyzerDiagnostics = driver.GetDiagnosticsAsync(newCompilation, cancellationToken).Result;
            var allDiagnostics = includeCompilerDiagnostics ?
                compilerDiagnostics.AddRange(analyzerDiagnostics) :
                analyzerDiagnostics;
            diagnostics = driver.ApplyProgrammaticSuppressionsAndFilterDiagnostics(allDiagnostics, newCompilation, cancellationToken);
 
            if (!reportSuppressedDiagnostics)
            {
                Assert.True(diagnostics.All(d => !d.IsSuppressed));
            }
 
            return (TCompilation)newCompilation; // note this is a new compilation
        }
 
        /// <summary>
        /// Given a set of compiler or <see cref="DiagnosticAnalyzer"/> generated <paramref name="diagnostics"/>, returns the effective diagnostics after applying the below filters:
        /// 1) <see cref="CompilationOptions.SpecificDiagnosticOptions"/> specified for the given <paramref name="compilation"/>.
        /// 2) <see cref="CompilationOptions.GeneralDiagnosticOption"/> specified for the given <paramref name="compilation"/>.
        /// 3) Diagnostic suppression through applied <see cref="System.Diagnostics.CodeAnalysis.SuppressMessageAttribute"/>.
        /// 4) Pragma directives for the given <paramref name="compilation"/>.
        /// </summary>
        public static IEnumerable<Diagnostic> GetEffectiveDiagnostics(this Compilation compilation, IEnumerable<Diagnostic> diagnostics)
        {
            return CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilation);
        }
 
        /// <summary>
        /// Returns true if all the diagnostics that can be produced by this analyzer are suppressed through options.
        /// </summary>
        [Obsolete("This API is no longer supported. See https://github.com/dotnet/roslyn/issues/67592 for details")]
        public static bool IsDiagnosticAnalyzerSuppressed(this DiagnosticAnalyzer analyzer, CompilationOptions options)
        {
            return CompilationWithAnalyzers.IsDiagnosticAnalyzerSuppressed(analyzer, options);
        }
 
        public static TCompilation VerifyEmitDiagnostics<TCompilation>(this TCompilation c, EmitOptions options, params DiagnosticDescription[] expected)
            where TCompilation : Compilation
        {
            c.GetEmitDiagnostics(options: options).Verify(expected);
            return c;
        }
 
        public static ImmutableArray<Diagnostic> GetEmitDiagnostics<TCompilation>(
            this TCompilation c,
            EmitOptions options = null,
            IEnumerable<ResourceDescription> manifestResources = null)
            where TCompilation : Compilation
        {
            var pdbStream = MonoHelpers.IsRunningOnMono() || options?.EmitMetadataOnly == true ? null : new MemoryStream();
            return c.Emit(new MemoryStream(), pdbStream: pdbStream, options: options, manifestResources: manifestResources).Diagnostics;
        }
 
        public static TCompilation VerifyEmitDiagnostics<TCompilation>(this TCompilation c, params DiagnosticDescription[] expected)
            where TCompilation : Compilation
        {
            return VerifyEmitDiagnostics(c, EmitOptions.Default, expected);
        }
 
        public static ImmutableArray<Diagnostic> GetEmitDiagnostics<TCompilation>(this TCompilation c)
            where TCompilation : Compilation
        {
            return GetEmitDiagnostics(c, EmitOptions.Default);
        }
 
        public static TCompilation VerifyEmitDiagnostics<TCompilation>(this TCompilation c, IEnumerable<ResourceDescription> manifestResources, params DiagnosticDescription[] expected)
            where TCompilation : Compilation
        {
            c.GetEmitDiagnostics(manifestResources: manifestResources).Verify(expected);
            return c;
        }
 
        public static string Concat(this string[] str)
        {
            return str.Aggregate(new StringBuilder(), (sb, s) => sb.AppendLine(s), sb => sb.ToString());
        }
 
        public static DiagnosticAnalyzer GetCompilerDiagnosticAnalyzer(string languageName)
        {
            return languageName == LanguageNames.CSharp ?
                (DiagnosticAnalyzer)new Diagnostics.CSharp.CSharpCompilerDiagnosticAnalyzer() :
                new Diagnostics.VisualBasic.VisualBasicCompilerDiagnosticAnalyzer();
        }
 
        public static ImmutableDictionary<string, ImmutableArray<DiagnosticAnalyzer>> GetCompilerDiagnosticAnalyzersMap()
        {
            var builder = ImmutableDictionary.CreateBuilder<string, ImmutableArray<DiagnosticAnalyzer>>();
            builder.Add(LanguageNames.CSharp, ImmutableArray.Create(GetCompilerDiagnosticAnalyzer(LanguageNames.CSharp)));
            builder.Add(LanguageNames.VisualBasic, ImmutableArray.Create(GetCompilerDiagnosticAnalyzer(LanguageNames.VisualBasic)));
            return builder.ToImmutable();
        }
 
        public static AnalyzerReference GetCompilerDiagnosticAnalyzerReference(string languageName)
        {
            var analyzer = GetCompilerDiagnosticAnalyzer(languageName);
            return new AnalyzerImageReference(ImmutableArray.Create(analyzer), display: analyzer.GetType().FullName);
        }
 
        public static ImmutableDictionary<string, ImmutableArray<AnalyzerReference>> GetCompilerDiagnosticAnalyzerReferencesMap()
        {
            var builder = ImmutableDictionary.CreateBuilder<string, ImmutableArray<AnalyzerReference>>();
            builder.Add(LanguageNames.CSharp, ImmutableArray.Create(GetCompilerDiagnosticAnalyzerReference(LanguageNames.CSharp)));
            builder.Add(LanguageNames.VisualBasic, ImmutableArray.Create(GetCompilerDiagnosticAnalyzerReference(LanguageNames.VisualBasic)));
            return builder.ToImmutable();
        }
 
        internal static string GetExpectedErrorLogHeader(CommonCompiler compiler)
        {
            var expectedToolName = compiler.GetToolName();
            var expectedVersion = compiler.GetAssemblyVersion();
            var expectedSemanticVersion = compiler.GetAssemblyVersion().ToString(fieldCount: 3);
            var expectedFileVersion = compiler.GetCompilerVersion();
            var expectedLanguage = compiler.GetCultureName();
 
            return string.Format(@"{{
  ""$schema"": ""http://json.schemastore.org/sarif-1.0.0"",
  ""version"": ""1.0.0"",
  ""runs"": [
    {{
      ""tool"": {{
        ""name"": ""{0}"",
        ""version"": ""{1}"",
        ""fileVersion"": ""{2}"",
        ""semanticVersion"": ""{3}"",
        ""language"": ""{4}""
      }},", expectedToolName, expectedVersion, expectedFileVersion, expectedSemanticVersion, expectedLanguage);
        }
 
        public static string Inspect(this Diagnostic e)
            => e.Location.IsInSource ? $"{e.Severity} {e.Id}: {e.GetMessage(CultureInfo.CurrentCulture)}" :
               e.Location.IsInMetadata ? "metadata: " : "no location: ";
 
        public static string ToString(this Diagnostic d, IFormatProvider formatProvider)
        {
            IFormattable formattable = d;
            return formattable.ToString(null, formatProvider);
        }
    }
}