File: MetaAnalyzers\ReleaseTrackingAnalyzerTests.cs
Web Access
Project: src\src\RoslynAnalyzers\Microsoft.CodeAnalysis.Analyzers\UnitTests\Microsoft.CodeAnalysis.Analyzers.UnitTests.csproj (Microsoft.CodeAnalysis.Analyzers.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.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Analyzers.MetaAnalyzers;
using Microsoft.CodeAnalysis.Analyzers.MetaAnalyzers.Fixers;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.ReleaseTracking;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.VisualBasic.Testing;
using Test.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Analyzers.UnitTests.MetaAnalyzers
{
    public class ReleaseTrackingAnalyzerTests
    {
        [Fact]
        public async Task TestNoDeclaredAnalyzersAsync()
        {
            await VerifyCSharpAsync(@"", @"", @"");
        }
 
        [InlineData(@"{|RS2008:""Id1""|}", null, null)]
        [InlineData("""
            "Id1"
            """, "", null)]
        [InlineData("""
            "Id1"
            """, null, "")]
        [InlineData(@"{|RS2000:""Id1""|}", "", "")]
        [Theory]
        public async Task TestMissingReleasesFilesAsync(string id, string shippedText, string unshippedText)
        {
            await VerifyCSharpAsync($$"""

                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    // Enabled by default descriptor.
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor({{id}}, "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText);
        }
 
        [Fact]
        public async Task TestCodeFixToEnableAnalyzerReleaseTrackingAsync()
        {
            var source = """
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor({|RS2008:"Id1"|}, "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """;
 
            var test = new CSharpCodeFixVerifier<DiagnosticDescriptorCreationAnalyzer, AnalyzerReleaseTrackingFix>.Test()
            {
                ReferenceAssemblies = AdditionalMetadataReferences.Default,
                NumberOfIncrementalIterations = 2,
                NumberOfFixAllIterations = 2,
                TestState =
                {
                    Sources = { source },
                },
                FixedState =
                {
                    Sources = { source },
                    AdditionalFiles = {
                        (ReleaseTrackingHelper.ShippedFileName,
                            AnalyzerReleaseTrackingFix.ShippedAnalyzerReleaseTrackingFileDefaultContent),
                        (ReleaseTrackingHelper.UnshippedFileName,
                            AnalyzerReleaseTrackingFix.UnshippedAnalyzerReleaseTrackingFileDefaultContent + DefaultUnshippedHeader + "Id1 | Category1 | Warning | MyAnalyzer")
                    }
                },
            };
 
            test.SolutionTransforms.Add(DisableNonReleaseTrackingWarnings);
            await test.RunAsync();
        }
 
        // Unshipped release with existing new rules table.
        [InlineData("", DefaultUnshippedHeader + "Id1 | Category1 | Warning |")]
        // Unshipped release with existing new rules table with borders.
        [InlineData("", DefaultUnshippedHeaderWithBorders + "| Id1 | Category1 | Warning | |")]
        // Shipped release with existing new rules table.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |", "")]
        // Shipped release with existing new rules table with borders.
        [InlineData(DefaultShippedHeaderWithBorders + "| Id1 | Category1 | Warning | |", "")]
        // Releases with separate new rules and changed rules table.
        [InlineData(DefaultShippedHeader + "Id1 | Category0 | Warning |" + BlankLine +
                    DefaultChangedShippedHeader2 + "Id1 | Category1 | Warning | Category0 | Warning |", "")]
        // Releases with separate new rules and changed rules table with borders.
        [InlineData(DefaultShippedHeaderWithBorders + "| Id1 | Category0 | Warning | |" + BlankLine +
                    DefaultChangedShippedHeader2WithBorders + "| Id1 | Category1 | Warning | Category0 | Warning | |", "")]
        // Releases with separate new rules and removed rules table.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + "Id2 | Category1 | Warning |" + BlankLine +
                    DefaultRemovedShippedHeader2 + "Id2 | Category1 | Warning ", "")]
        // Releases with separate new rules and removed rules table with borders.
        [InlineData(DefaultShippedHeaderWithBorders + "| Id1 | Category1 | Warning | |" + BlankLine + "| Id2 | Category1 | Warning | |" + BlankLine +
                    DefaultRemovedShippedHeader2WithBorders + "| Id2 | Category1 | Warning | |", "")]
        // Release with new rules and changed rules table.
        [InlineData(DefaultShippedHeader + "Id2 | Category0 | Warning |" + BlankLine +
                    DefaultShippedHeader2 + "Id1 | Category1 | Warning |" + BlankLine + DefaultChangedUnshippedHeader + "Id2 | Category1 | Warning | Category0 | Warning |",
                    DefaultRemovedUnshippedHeader + "Id2 | Category1 | Warning |")]
        // Release with new rules and changed rules table with borders.
        [InlineData(DefaultShippedHeaderWithBorders + "| Id2 | Category0 | Warning | |" + BlankLine +
                    DefaultShippedHeader2WithBorders + "| Id1 | Category1 | Warning | |" + BlankLine + DefaultChangedUnshippedHeaderWithBorders + "| Id2 | Category1 | Warning | Category0 | Warning | |",
                    DefaultRemovedUnshippedHeaderWithBorders + "| Id2 | Category1 | Warning | |")]
        // Release with new rules and removed rules table.
        [InlineData(DefaultShippedHeader + "Id2 | Category0 | Warning |" + BlankLine +
                    DefaultShippedHeader2 + "Id1 | Category1 | Warning |" + BlankLine + DefaultRemovedUnshippedHeader + "Id2 | Category0 | Warning |", "")]
        // Release with new rules and removed rules table with borders.
        [InlineData(DefaultShippedHeaderWithBorders + "| Id2 | Category0 | Warning | |" + BlankLine +
                    DefaultShippedHeader2WithBorders + "| Id1 | Category1 | Warning | |" + BlankLine + DefaultRemovedUnshippedHeaderWithBorders + "| Id2 | Category0 | Warning | |", "")]
        // Release with all 3 tables
        [InlineData(DefaultShippedHeader + "Id3 | Category1 | Warning |" + BlankLine + "Id2 | Category0 | Warning |" + BlankLine +
                    DefaultShippedHeader2 + "Id1 | Category1 | Warning |" + BlankLine + DefaultChangedUnshippedHeader + "Id3 | Category2 | Warning | Category1 | Warning |" + BlankLine + DefaultRemovedUnshippedHeader + "Id2 | Category1 | Warning |",
                    DefaultRemovedUnshippedHeader + "Id3 | Category2 | Warning |")]
        // Release with all 3 tables with borders
        [InlineData(DefaultShippedHeaderWithBorders + "| Id3 | Category1 | Warning | |" + BlankLine + "| Id2 | Category0 | Warning | |" + BlankLine +
                    DefaultShippedHeader2WithBorders + "| Id1 | Category1 | Warning | |" + BlankLine + DefaultChangedUnshippedHeaderWithBorders + "| Id3 | Category2 | Warning | Category1 | Warning | |" + BlankLine + DefaultRemovedUnshippedHeaderWithBorders + "| Id2 | Category1 | Warning | |",
                    DefaultRemovedUnshippedHeaderWithBorders + "| Id3 | Category2 | Warning | |")]
        [Theory]
        public async Task TestReleasesFileAlreadyHasEntryAsync(string shippedText, string unshippedText)
        {
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    // Enabled by default descriptor.
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor("Id1", "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) {{ }}
                }
                """, shippedText, unshippedText);
        }
 
        [Fact]
        public async Task TestRemoveUnshippedDeletedDiagnosticIdRuleAsync()
        {
            var unshippedText = DefaultUnshippedHeader + "Id1 | Category1 | Warning |";
 
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray<DiagnosticDescriptor>.Empty;
                    public override void Initialize(AnalysisContext context) {{ }} 
                }
                """, @"", unshippedText,
                new DiagnosticResult(DiagnosticDescriptorCreationAnalyzer.RemoveUnshippedDeletedDiagnosticIdRule).WithArguments("Id1"));
        }
 
        [Fact]
        public async Task TestRemoveShippedDeletedDiagnosticIdRuleAsync()
        {
            var shippedText = DefaultShippedHeader + "Id1 | Category1 | Warning |";
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray<DiagnosticDescriptor>.Empty;
                    public override void Initialize(AnalysisContext context) {{ }} 
                }
                """, shippedText, @"",
                new DiagnosticResult(DiagnosticDescriptorCreationAnalyzer.RemoveShippedDeletedDiagnosticIdRule).WithArguments("Id1", "1.0"));
        }
 
        [Fact]
        public async Task TestCodeFixToAddUnshippedEntriesAsync()
        {
            await VerifyCSharpAdditionalFileFixAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    // Enabled by default descriptor.
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor({|RS2000:"Id1"|}, "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    // Duplicate descriptor with different message.
                    private static readonly DiagnosticDescriptor descriptor1_dupe =
                        new DiagnosticDescriptor({|RS2000:"Id1"|}, "Title1", "DifferentMessage", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    // Disabled by default descriptor.
                    private static readonly DiagnosticDescriptor descriptor2 =
                        new DiagnosticDescriptor({|RS2000:"Id2"|}, "Title2", "Message2", "Category2", DiagnosticSeverity.Warning, isEnabledByDefault: false);
 
                    // Descriptor with help link.
                    private static readonly DiagnosticDescriptor descriptor3 =
                        new DiagnosticDescriptor({|RS2000:"Id3"|}, "Title3", "Message3", "Category3", DiagnosticSeverity.Warning, isEnabledByDefault: true, helpLinkUri: "Dummy");
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1, descriptor1_dupe, descriptor2, descriptor3);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, @"", @"", $"""
                {DefaultUnshippedHeader}Id1 | Category1 | Warning | MyAnalyzer
                Id2 | Category2 | Disabled | MyAnalyzer
                Id3 | Category3 | Warning | MyAnalyzer, [Documentation](Dummy)
                """);
        }
 
        [Fact]
        public async Task TestCodeFixToAddUnshippedEntries_DiagnosticDescriptorHelperAsync()
        {
            var source = """
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    // Enabled by default descriptor.
                    private static readonly DiagnosticDescriptor descriptor1 =
                        DiagnosticDescriptorHelper.Create({|RS2000:"Id1"|}, "Title1", "Message1", "Category1", RuleLevel.BuildWarning);
 
                    // Duplicate descriptor with different message.
                    private static readonly DiagnosticDescriptor descriptor1_dupe =
                        DiagnosticDescriptorHelper.Create({|RS2000:"Id1"|}, "Title1", "DifferentMessage", "Category1", RuleLevel.BuildWarning);
 
                    // Disabled by default descriptor.
                    private static readonly DiagnosticDescriptor descriptor2 =
                        DiagnosticDescriptorHelper.Create({|RS2000:"Id2"|}, "Title2", "Message2", "Category2", RuleLevel.Disabled);
 
                    // Descriptor with help link.
                    private static readonly DiagnosticDescriptor descriptor3 =
                        DiagnosticDescriptorHelper.Create({|RS2000:"Id3"|}, "Title3", "Message3", "Category3", RuleLevel.BuildWarning, helpLinkUri: "Dummy");
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1, descriptor1_dupe, descriptor2, descriptor3);
                    public override void Initialize(AnalysisContext context) { }
                }
                """ + CSharpDiagnosticDescriptorCreationHelper;
            await VerifyCSharpAdditionalFileFixAsync(source, @"", @"", $"""
                {DefaultUnshippedHeader}Id1 | Category1 | Warning | MyAnalyzer
                Id2 | Category2 | Disabled | MyAnalyzer
                Id3 | Category3 | Warning | MyAnalyzer, [Documentation](Dummy)
                """);
        }
 
        private const string BlankLine = """
 
 
            """;
 
        // Comments
        [InlineData(DefaultUnshippedHeader + @"; Comments are preserved" + BlankLine,
                    DefaultUnshippedHeader + @"; Comments are preserved" + BlankLine + @"Id1 | Category1 | Warning | MyAnalyzer" + BlankLine)]
        // Blank lines are preserved
        [InlineData(DefaultUnshippedHeader + BlankLine,
                    DefaultUnshippedHeader + @"Id1 | Category1 | Warning | MyAnalyzer" + BlankLine + BlankLine)]
        // Mixed
        [InlineData(DefaultUnshippedHeader + BlankLine + @"; Comments are preserved" + BlankLine,
                    DefaultUnshippedHeader + @"Id1 | Category1 | Warning | MyAnalyzer" + BlankLine + BlankLine + @"; Comments are preserved" + BlankLine)]
        [Theory]
        public async Task TestCodeFixToAddUnshippedEntries_TriviaIsPreservedAsync(string unshippedText, string fixedUnshippedText)
        {
            await VerifyCSharpAdditionalFileFixAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor({|RS2000:"Id1"|}, "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, @"", unshippedText, fixedUnshippedText);
        }
 
        // Added after current entry.
        [InlineData("Id0", DefaultUnshippedHeader + @"Id0 | DifferentCategory | Warning | MyAnalyzer",
                           DefaultUnshippedHeader + @"Id0 | DifferentCategory | Warning | MyAnalyzer" + BlankLine + @"Id1 | Category1 | Warning | MyAnalyzer")]
        // Added before current entry.
        [InlineData("Id2", DefaultUnshippedHeader + @"Id2 | DifferentCategory | Warning | MyAnalyzer",
                           DefaultUnshippedHeader + @"Id1 | Category1 | Warning | MyAnalyzer" + BlankLine + @"Id2 | DifferentCategory | Warning | MyAnalyzer")]
        // Has existing changed rules table.
        [InlineData("Id2", DefaultChangedUnshippedHeader + @"Id2 | DifferentCategory | Warning | Category | Warning | MyAnalyzer",
                           DefaultUnshippedHeader + @"Id1 | Category1 | Warning | MyAnalyzer" + BlankLine + BlankLine + DefaultChangedUnshippedHeader + @"Id2 | DifferentCategory | Warning | Category | Warning | MyAnalyzer",
                           DefaultShippedHeader + @"Id2 | Category | Warning | MyAnalyzer")]
        // Has existing removed rules table.
        [InlineData("Id2", DefaultRemovedUnshippedHeader + @"Id3 | Category | Warning | MyAnalyzer",
                           DefaultUnshippedHeader + @"Id1 | Category1 | Warning | MyAnalyzer" + BlankLine + BlankLine + DefaultRemovedUnshippedHeader + @"Id3 | Category | Warning | MyAnalyzer",
                           DefaultShippedHeader + @"Id2 | DifferentCategory | Warning | MyAnalyzer" + BlankLine + @"Id3 | Category | Warning | MyAnalyzer")]
        [Theory]
        public async Task TestCodeFixToAddUnshippedEntries_AlreadyHasDifferentUnshippedEntriesAsync(string differentRuleId, string unshippedText, string fixedUnshippedText, string shippedText = "")
        {
            await VerifyCSharpAdditionalFileFixAsync($$"""

                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor({|RS2000:"Id1"|}, "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    private static readonly DiagnosticDescriptor descriptor2 =
                        new DiagnosticDescriptor("{{differentRuleId}}", "DifferentTitle", "DifferentMessage", "DifferentCategory", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1, descriptor2);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText, fixedUnshippedText);
        }
 
        // Adds to existing new rules table and creates a new changed rules table.
        [InlineData(DefaultUnshippedHeader + @"Id0 | Category0 | Warning | MyAnalyzer",
                    DefaultUnshippedHeader + @"Id0 | Category0 | Warning | MyAnalyzer" + BlankLine + @"Id1 | Category1 | Warning | MyAnalyzer" + BlankLine + BlankLine + DefaultChangedUnshippedHeader + @"Id2 | DifferentCategory | Warning | Category | Warning | MyAnalyzer",
                    DefaultShippedHeader + @"Id2 | Category | Warning | MyAnalyzer")]
        // Adds to existing new rules table and changed rules table.
        [InlineData(DefaultChangedUnshippedHeader + @"Id0 | Category0 | Warning | Category | Warning | MyAnalyzer",
                    DefaultUnshippedHeader + @"Id1 | Category1 | Warning | MyAnalyzer" + BlankLine + BlankLine + DefaultChangedUnshippedHeader + @"Id0 | Category0 | Warning | Category | Warning | MyAnalyzer" + BlankLine + @"Id2 | DifferentCategory | Warning | Category | Warning | MyAnalyzer",
                    DefaultShippedHeader + @"Id0 | Category | Warning | MyAnalyzer" + BlankLine + @"Id2 | Category | Warning | MyAnalyzer")]
        [Theory]
        public async Task TestCodeFixToAddUnshippedEntriesToMultipleTablesAsync(string unshippedText, string fixedUnshippedText, string shippedText = "")
        {
            await VerifyCSharpAdditionalFileFixAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor0 =
                        new DiagnosticDescriptor("Id0", "Title0", "Message0", "Category0", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor({|RS2000:"Id1"|}, "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    private static readonly DiagnosticDescriptor descriptor2 =
                        new DiagnosticDescriptor({|RS2001:"Id2"|}, "DifferentTitle", "DifferentMessage", "DifferentCategory", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor0, descriptor1, descriptor2);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText, fixedUnshippedText);
        }
 
        [InlineData("",
                    DefaultUnshippedHeader + "Id1 | Category1 | Warning | MyAnalyzer",
                    "RS2000")]
        [InlineData(DefaultShippedHeader + "Id1 | DifferentCategory | Warning | MyAnalyzer",
                    DefaultChangedUnshippedHeader + "Id1 | Category1 | Warning | DifferentCategory | Warning | MyAnalyzer",
                    "RS2001")]
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Disabled | MyAnalyzer",
                    DefaultChangedUnshippedHeader + "Id1 | Category1 | Warning | Category1 | Disabled | MyAnalyzer",
                    "RS2001")]
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Info |" + BlankLine + DefaultRemovedShippedHeader2 + "Id1 | Category1 | Info | MyAnalyzer",
                    DefaultUnshippedHeader + "Id1 | Category1 | Warning | MyAnalyzer",
                    "RS2000")]
        [Theory]
        public async Task TestCodeFixToAddUnshippedEntries_AlreadyHasDifferentShippedEntryAsync(string shippedText, string fixedUnshippedText, string expectedDiagnosticId)
        {
            await VerifyCSharpAdditionalFileFixAsync($$"""

                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    // Enabled by default descriptor.
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor({|{{expectedDiagnosticId}}:"Id1"|}, "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, @"", fixedUnshippedText);
        }
 
        [Fact]
        public async Task TestCodeFixToUpdateMultipleUnshippedEntriesAsync()
        {
            await VerifyCSharpAdditionalFileFixAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    // Enabled by default descriptor.
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor({|RS2001:"Id1"|}, "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    // Disable by default descriptor.
                    private static readonly DiagnosticDescriptor descriptor2 =
                        new DiagnosticDescriptor({|RS2001:"Id2"|}, "Title2", "Message2", "Category2", DiagnosticSeverity.Warning, isEnabledByDefault: false);
 
                    // Descriptor with help - ensure that just adding a help link does not require a new analyzer release entry.
                    private static readonly DiagnosticDescriptor descriptor3 =
                        new DiagnosticDescriptor("Id3", "Title3", "Message3", "Category3", DiagnosticSeverity.Warning, isEnabledByDefault: true, helpLinkUri: "Dummy");
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1, descriptor2, descriptor3);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, @"", $"""
                {DefaultUnshippedHeader}Id1 | DifferentCategory | Warning | MyAnalyzer
                Id2 | Category2 | Warning | MyAnalyzer
                Id3 | Category3 | Warning | MyAnalyzer
                """, $"""
                {DefaultUnshippedHeader}Id1 | Category1 | Warning | MyAnalyzer
                Id2 | Category2 | Disabled | MyAnalyzer
                Id3 | Category3 | Warning | MyAnalyzer
                """);
        }
 
        [Fact]
        public async Task TestCodeFixToAddUnshippedEntries_UndetectedFieldsAsync()
        {
            var entry = $@"Id1 | {ReleaseTrackingHelper.UndetectedText} | {ReleaseTrackingHelper.UndetectedText} | MyAnalyzer";
            await VerifyCSharpAdditionalFileFixAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                public static class DiagnosticDescriptorHelper
                {
                    public static DiagnosticDescriptor Create(
                        string id,
                        LocalizableString title,
                        LocalizableString messageFormat)
                    => null;
                }
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        DiagnosticDescriptorHelper.Create({|RS2000:"Id1"|}, "Title1", "Message1");
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, @"", @"", $@"{DefaultUnshippedHeader}{entry}", additionalExpectedDiagnosticsInInput: ImmutableArray<DiagnosticResult>.Empty,
                additionalExpectedDiagnosticsInResult: ImmutableArray.Create(
                    GetAdditionalFileResultAt(5, 1,
                        ReleaseTrackingHelper.UnshippedFileName,
                        DiagnosticDescriptorCreationAnalyzer.InvalidUndetectedEntryInAnalyzerReleasesFileRule,
                        ReleaseTrackingHelper.UnshippedFileName,
                        entry)));
        }
 
        [Fact]
        public async Task TestNoCodeFixToAddUnshippedEntries_UndetectedFieldsAsync()
        {
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                public static class DiagnosticDescriptorHelper
                {
                    public static DiagnosticDescriptor Create(
                        string id,
                        LocalizableString title,
                        LocalizableString messageFormat)
                    => null;
                }
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        DiagnosticDescriptorHelper.Create("Id1", "Title1", "Message1");
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, @"", $@"{DefaultUnshippedHeader}Id1 | CustomCategory | Warning |");
        }
 
        // No header in unshipped
        [InlineData("", "Id1 | Category1 | Warning |")]
        // No header in shipped
        [InlineData("Id1 | Category1 | Warning |", "")]
        // Missing TableTitle in unshipped
        [InlineData("", ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine1 + BlankLine + "Id1 | Category1 | Warning |")]
        // Missing TableHeaderLine1 in unshipped
        [InlineData("", ReleaseTrackingHelper.TableTitleNewRules + BlankLine + ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine2 + BlankLine + "Id1 | Category1 | Warning |", 2)]
        // Missing TableHeaderLine1 with minimum markdown column hyphens in unshipped
        [InlineData("", ReleaseTrackingHelper.TableTitleNewRules + BlankLine + @"---|---|---|---" + BlankLine + "Id1 | Category1 | Warning |", 2)]
        // Missing TableHeaderLine2 in unshipped
        [InlineData("", ReleaseTrackingHelper.TableTitleNewRules + BlankLine + ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine1 + BlankLine + "Id1 | Category1 | Warning |", 3)]
        // Missing TableHeaderLine2 with extra column header text spaces in unshipped
        [InlineData("", ReleaseTrackingHelper.TableTitleNewRules + BlankLine + @"Rule ID  | Category  | Severity  | Notes " + BlankLine + "Id1 | Category1 | Warning |", 3)]
        // Missing Release Version line in shipped
        [InlineData(DefaultUnshippedHeader + "Id1 | Category1 | Warning |", "")]
        // Missing Release Version in shipped
        [InlineData(ReleaseTrackingHelper.ReleasePrefix + BlankLine + DefaultUnshippedHeader + "Id1 | Category1 | Warning |", "")]
        // Invalid Release Version in shipped
        [InlineData(ReleaseTrackingHelper.ReleasePrefix + " InvalidVersion" + BlankLine + DefaultUnshippedHeader + "Id1 | Category1 | Warning |", "")]
        // Missing TableTitle in shipped
        [InlineData(ReleaseTrackingHelper.ReleasePrefix + "1.0" + BlankLine + ReleaseTrackingHelper.TableHeaderChangedRulesLine1 + BlankLine + "Id1 | Category1 | Warning |", "", 2)]
        // Missing TableHeaderLine1 in shipped
        [InlineData(ReleaseTrackingHelper.ReleasePrefix + "1.0" + BlankLine + ReleaseTrackingHelper.TableTitleChangedRules + BlankLine + ReleaseTrackingHelper.TableHeaderChangedRulesLine2 + BlankLine + "Id1 | Category1 | Warning |", "", 3)]
        // Missing TableHeaderLine1 with minimum markdown column hyphens in shipped
        [InlineData(ReleaseTrackingHelper.ReleasePrefix + "1.0" + BlankLine + ReleaseTrackingHelper.TableTitleChangedRules + BlankLine + @"---|---|---|---|---|---" + BlankLine + "Id1 | Category1 | Warning |", "", 3)]
        // Missing TableHeaderLine2 in shipped
        [InlineData(ReleaseTrackingHelper.ReleasePrefix + " 1.0" + BlankLine + ReleaseTrackingHelper.TableTitleChangedRules + BlankLine + ReleaseTrackingHelper.TableHeaderChangedRulesLine1 + BlankLine + "Id1 | Category1 | Warning |", "", 4)]
        // Missing TableHeaderLine2 in with extra column header text spaces shipped
        [InlineData(ReleaseTrackingHelper.ReleasePrefix + " 1.0" + BlankLine + ReleaseTrackingHelper.TableTitleChangedRules + BlankLine + @"Rule ID  | New Category  | New Severity  | Old Category  | Old Severity  | Notes" + BlankLine + "Id1 | Category1 | Warning |", "", 4)]
        // Invalid Release Version line in unshipped
        [InlineData("", DefaultShippedHeader + "Id1 | Category1 | Warning |")]
        // Mismatch Table title and TableHeaderLine1 in unshipped
        [InlineData("", ReleaseTrackingHelper.TableTitleNewRules + BlankLine + ReleaseTrackingHelper.TableHeaderChangedRulesLine1 + BlankLine + ReleaseTrackingHelper.TableHeaderChangedRulesLine2 + BlankLine + "Id1 | Category1 | Warning |", 2)]
        // Mismatch Table title and TableHeaderLine2 in unshipped
        [InlineData("", ReleaseTrackingHelper.TableTitleChangedRules + BlankLine + ReleaseTrackingHelper.TableHeaderChangedRulesLine1 + BlankLine + ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine2 + BlankLine + "Id1 | Category1 | Warning |", 3)]
        [Theory]
        public async Task TestInvalidHeaderDiagnosticAsync(string shippedText, string unshippedText, int line = 1)
        {
            var fileWithDiagnostics = shippedText.Length > 0 ? ReleaseTrackingHelper.ShippedFileName : ReleaseTrackingHelper.UnshippedFileName;
            var diagnosticText = (shippedText.Length > 0 ? shippedText : unshippedText).Split([Environment.NewLine], StringSplitOptions.None).ElementAt(line - 1);
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor("Id1", "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText,
                    GetAdditionalFileResultAt(line, 1,
                        fileWithDiagnostics,
                        DiagnosticDescriptorCreationAnalyzer.InvalidHeaderInAnalyzerReleasesFileRule,
                        fileWithDiagnostics,
                        diagnosticText));
        }
 
        // Undetected category
        [InlineData("Id1 | " + ReleaseTrackingHelper.UndetectedText + " | Warning |", true)]
        // Undetected severity
        [InlineData("Id1 | Category1 | " + ReleaseTrackingHelper.UndetectedText + " |", true)]
        // Undetected category and severity
        [InlineData("Id1 | " + ReleaseTrackingHelper.UndetectedText + " | " + ReleaseTrackingHelper.UndetectedText + " |", true)]
        // Invalid severity
        [InlineData("Id1 | Category1 | Invalid |", false)]
        // Missing required fields - category + severity
        [InlineData("Id1", false)]
        // Missing required field - category
        [InlineData("Id1 | Warning ", false)]
        // Missing required field - severity
        [InlineData("Id1 | Category1 |", false)]
        // Extra fields
        [InlineData("Id1 | Category1 | Warning | Notes | InvalidField", false)]
        [Theory]
        public async Task TestInvalidEntryDiagnosticAsync(string entry, bool hasUndetectedField)
        {
            var rule = hasUndetectedField ?
                DiagnosticDescriptorCreationAnalyzer.InvalidUndetectedEntryInAnalyzerReleasesFileRule :
                DiagnosticDescriptorCreationAnalyzer.InvalidEntryInAnalyzerReleasesFileRule;
            var unshippedText = DefaultUnshippedHeader + entry;
 
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor("Id1", "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, @"", unshippedText,
                    GetAdditionalFileResultAt(5, 1,
                        ReleaseTrackingHelper.UnshippedFileName,
                        rule,
                        ReleaseTrackingHelper.UnshippedFileName,
                        entry));
        }
 
        // Undetected category
        [InlineData("Id1 | " + ReleaseTrackingHelper.UndetectedText + " | Warning | Category1 | Warning |", true)]
        // Undetected old category
        [InlineData("Id1 | Category1 | Warning | " + ReleaseTrackingHelper.UndetectedText + " | Warning |", true)]
        // Undetected severity
        [InlineData("Id1 | Category1 | " + ReleaseTrackingHelper.UndetectedText + " | Category1 | Warning |", true)]
        // Undetected old severity
        [InlineData("Id1 | Category1 | Warning | Category1 | " + ReleaseTrackingHelper.UndetectedText + " |", true)]
        // Undetected category and severity
        [InlineData("Id1 | " + ReleaseTrackingHelper.UndetectedText + " | " + ReleaseTrackingHelper.UndetectedText + " | Category1 | Warning |", true)]
        // Invalid severity
        [InlineData("Id1 | Category1 | Invalid | Category1 | Warning |", false)]
        // Invalid old severity
        [InlineData("Id1 | Category1 | Warning | Category1 | Invalid |", false)]
        // Unchanged old category and old severity
        [InlineData("Id1 | Category1 | Warning | Category1 | Warning |", false)]
        // Missing required fields - category + severity + old category + old severity
        [InlineData("Id1", false)]
        // Missing required fields - category + old category + old severity
        [InlineData("Id1 | Warning ", false)]
        // Missing required fields - old category + old severity
        [InlineData("Id1 | Category1 | Warning ", false)]
        // Missing required fields - old severity
        [InlineData("Id1 | Category1 | Warning | OldCategory ", false)]
        // Missing required field - severity
        [InlineData("Id1 | Category1 |", false)]
        // Extra fields
        [InlineData("Id1 | Category1 | Warning | Category2 | Warning | Notes | InvalidField", false)]
        [Theory]
        public async Task TestInvalidEntryDiagnostic_ChangedRulesAsync(string entry, bool hasUndetectedField)
        {
            var rule = hasUndetectedField ?
                DiagnosticDescriptorCreationAnalyzer.InvalidUndetectedEntryInAnalyzerReleasesFileRule :
                DiagnosticDescriptorCreationAnalyzer.InvalidEntryInAnalyzerReleasesFileRule;
            var unshippedText = DefaultChangedUnshippedHeader + entry;
 
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor("Id1", "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, @"", unshippedText,
                    GetAdditionalFileResultAt(5, 1,
                        ReleaseTrackingHelper.UnshippedFileName,
                        rule,
                        ReleaseTrackingHelper.UnshippedFileName,
                        entry));
        }
 
        // Duplicate entries in shipped.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + "{|RS2005:Id1 | Category1 | Warning ||}", "")]
        // Duplicate entries in unshipped.
        [InlineData("", DefaultUnshippedHeader + "Id1 | Category1 | Warning |" + BlankLine + "{|RS2005:Id1 | Category1 | Warning ||}")]
        // Duplicate changed entries in unshipped.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |", DefaultChangedUnshippedHeader + "Id1 | Category1 | Info | Category1 | Warning |" + BlankLine + DefaultChangedUnshippedHeader + "{|RS2005:Id1 | Category1 | Info | Category1 | Warning ||}")]
        // Duplicate entries with changed field in shipped.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + "{|RS2005:Id1 | Category2 | Warning ||}", "")]
        // Duplicate entries with changed field in unshipped.
        [InlineData("", DefaultUnshippedHeader + "Id1 | Category1 | Warning |" + BlankLine + "{|RS2005:Id1 | Category1 | Info ||}")]
        // Duplicate entries with in shipped with first removed entry.
        [InlineData(DefaultRemovedShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultUnshippedHeader + "{|RS2005:Id1 | Category2 | Warning ||}", "")]
        // Duplicate entries with in shipped with second removed entry.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultRemovedUnshippedHeader + "{|RS2005:Id1 | Category2 | Warning ||}", "")]
        [Theory]
        public async Task TestDuplicateEntryInReleaseDiagnosticAsync(string shippedText, string unshippedText)
        {
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor("Id1", "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText);
        }
 
        // Duplicate entries across shipped and unshipped.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |", DefaultUnshippedHeader + "{|RS2006:Id1 | Category1 | Warning ||}")]
        // Duplicate entries across consecutive shipped releases.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultShippedHeader2 + "{|RS2006:Id1 | Category1 | Warning ||}", "")]
        // Duplicate entries across non-consecutive shipped releases.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultShippedHeader2 + BlankLine + DefaultShippedHeader3 + "{|RS2006:Id1 | Category1 | Warning ||}", "")]
        // Duplicate entries across shipped and unshipped, but with an intermediate entry with changed category - no diagnostic expected.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultShippedHeader2 + "Id1 | Category2 | Warning |", DefaultUnshippedHeader + "Id1 | Category1 | Warning |")]
        // Duplicate entries across shipped and unshipped, but with an intermediate entry with changed severity - no diagnostic expected.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultShippedHeader2 + "Id1 | Category1 | Info |", DefaultUnshippedHeader + "Id1 | Category1 | Warning |")]
        // Duplicate entries across shipped and unshipped, but with an intermediate entry with removed prefix - no diagnostic expected.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultRemovedShippedHeader2 + "Id1 | Category1 | Warning |", DefaultUnshippedHeader + "Id1 | Category1 | Warning |")]
        // Duplicate changed entries across consecutive shipped releases.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultChangedShippedHeader2 + "Id1 | Category1 | Warning | Category1 | Info |" + BlankLine + DefaultChangedShippedHeader3 + "{|RS2006:Id1 | Category1 | Warning | Category1 | Info ||}", "")]
        [Theory]
        public async Task TestDuplicateEntryBetweenReleasesDiagnosticAsync(string shippedText, string unshippedText)
        {
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor("Id1", "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText);
        }
 
        // Remove entry in unshipped for already shipped release.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |", DefaultRemovedUnshippedHeader + "Id1 | Category1 | Warning |", "RS2004")]
        // Remove entry in shipped for a prior shipped release.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultRemovedShippedHeader2 + "Id1 | Category1 | Warning |", "", "RS2000")]
        // Remove entry with changed severity in shipped for a prior shipped release.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultRemovedShippedHeader2 + "Id1 | Category1 | Info |", "", "RS2000")]
        [Theory]
        public async Task TestRemoveEntryInReleaseFile_DiagnosticCasesAsync(string shippedText, string unshippedText, string expectedDiagnosticId)
        {
            await VerifyCSharpAsync($$"""

                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor({|{{expectedDiagnosticId}}:"Id1"|}, "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText);
        }
 
        // Invalid remove entry without prior shipped entry in shipped.
        [InlineData(DefaultRemovedShippedHeader + "Id1 | Category1 | Warning |", "")]
        // Invalid remove entry without prior shipped entry in unshipped.
        [InlineData("", DefaultRemovedUnshippedHeader + "Id1 | Category1 | Warning |")]
        [Theory]
        public async Task TestInvalidRemoveWithoutShippedEntryInReleaseFileAsync(string shippedText, string unshippedText)
        {
            var fileWithDiagnostics = shippedText.Length > 0 ? ReleaseTrackingHelper.ShippedFileName : ReleaseTrackingHelper.UnshippedFileName;
            var lineCount = (shippedText.Length > 0 ? shippedText : unshippedText).Split([Environment.NewLine], StringSplitOptions.None).Length;
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray<DiagnosticDescriptor>.Empty;
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText,
                    GetAdditionalFileResultAt(lineCount, 1,
                        fileWithDiagnostics,
                        DiagnosticDescriptorCreationAnalyzer.InvalidRemovedOrChangedWithoutPriorNewEntryInAnalyzerReleasesFileRule,
                        fileWithDiagnostics,
                        "Removed",
                        "Id1"));
        }
 
        // Invalid changed entry without prior shipped entry in shipped.
        [InlineData(DefaultChangedShippedHeader + "Id1 | Category1 | Hidden | Category1 | Warning |", "")]
        // Invalid changed entry without prior shipped entry in unshipped.
        [InlineData("", DefaultChangedUnshippedHeader + "Id1 | Category1 | Hidden | Category1 | Warning |")]
        [Theory]
        public async Task TestInvalidChangedWithoutShippedEntryInReleaseFileAsync(string shippedText, string unshippedText)
        {
            var fileWithDiagnostics = shippedText.Length > 0 ? ReleaseTrackingHelper.ShippedFileName : ReleaseTrackingHelper.UnshippedFileName;
            var lineCount = (shippedText.Length > 0 ? shippedText : unshippedText).Split([Environment.NewLine], StringSplitOptions.None).Length;
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor("Id1", "Title1", "Message1", "Category1", DiagnosticSeverity.Hidden, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText,
                    GetAdditionalFileResultAt(lineCount, 1,
                        fileWithDiagnostics,
                        DiagnosticDescriptorCreationAnalyzer.InvalidRemovedOrChangedWithoutPriorNewEntryInAnalyzerReleasesFileRule,
                        fileWithDiagnostics,
                        "Changed",
                        "Id1"));
        }
 
        // Invalid remove entry without prior shipped entry in shipped, followed by a shipped entry.
        [InlineData(DefaultRemovedShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultShippedHeader2 + "Id1 | Category1 | Warning |", "")]
        // Invalid remove entry without prior shipped entry in shipped, followed by an unshipped entry.
        [InlineData(DefaultRemovedShippedHeader + "Id1 | Category1 | Warning |", DefaultUnshippedHeader + "Id1 | Category1 | Warning |")]
        [Theory]
        public async Task TestInvalidRemoveWithoutShippedEntryInReleaseFile_02Async(string shippedText, string unshippedText)
        {
            var fileWithDiagnostics = shippedText.Length > 0 ? ReleaseTrackingHelper.ShippedFileName : ReleaseTrackingHelper.UnshippedFileName;
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new DiagnosticDescriptor("Id1", "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText,
                    GetAdditionalFileResultAt(7, 1,
                        fileWithDiagnostics,
                        DiagnosticDescriptorCreationAnalyzer.InvalidRemovedOrChangedWithoutPriorNewEntryInAnalyzerReleasesFileRule,
                        fileWithDiagnostics,
                        "Removed",
                        "Id1"));
        }
 
        // Remove entry in unshipped for already shipped release.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |", DefaultRemovedUnshippedHeader + "Id1 | Category1 | Warning |")]
        // Remove entry in shipped for a prior shipped release.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultRemovedShippedHeader2 + "Id1 | Category1 | Warning |", "")]
        // Remove entry with changed severity in shipped for a prior shipped release.
        [InlineData(DefaultShippedHeader + "Id1 | Category1 | Warning |" + BlankLine + DefaultRemovedShippedHeader2 + "Id1 | Category1 | Info |", "")]
        [Theory]
        public async Task TestRemoveEntryInReleaseFile_NoDiagnosticCasesAsync(string shippedText, string unshippedText)
        {
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray<DiagnosticDescriptor>.Empty;
                    public override void Initialize(AnalysisContext context) { }
                }
                """, shippedText, unshippedText);
        }
 
        [Fact, WorkItem(5828, "https://github.com/dotnet/roslyn-analyzers/issues/5828")]
        public async Task TestTargetTypedNew()
        {
            await VerifyCSharpAsync("""
 
                using System;
                using System.Collections.Immutable;
                using Microsoft.CodeAnalysis;
                using Microsoft.CodeAnalysis.Diagnostics;
 
                [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
                class MyAnalyzer : DiagnosticAnalyzer
                {
                    private static readonly DiagnosticDescriptor descriptor1 =
                        new("Id1", "Title1", "Message1", "Category1", DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
                    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor1);
                    public override void Initialize(AnalysisContext context) { }
                }
                """, @"", $@"{DefaultUnshippedHeader}Id1 | Category1 | Warning |");
        }
        #region Helpers
 
        private const string DefaultUnshippedHeader = ReleaseTrackingHelper.TableTitleNewRules + BlankLine + BlankLine +
            ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine1 + BlankLine +
            ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine2 + BlankLine;
 
        private const string DefaultRemovedUnshippedHeader = ReleaseTrackingHelper.TableTitleRemovedRules + BlankLine + BlankLine +
            ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine1 + BlankLine +
            ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine2 + BlankLine;
 
        private const string DefaultChangedUnshippedHeader = ReleaseTrackingHelper.TableTitleChangedRules + BlankLine + BlankLine +
            ReleaseTrackingHelper.TableHeaderChangedRulesLine1 + BlankLine +
            ReleaseTrackingHelper.TableHeaderChangedRulesLine2 + BlankLine;
 
        private const string DefaultShippedHeader = ReleaseTrackingHelper.ReleasePrefix + " 1.0" + BlankLine + BlankLine + DefaultUnshippedHeader;
        private const string DefaultRemovedShippedHeader = ReleaseTrackingHelper.ReleasePrefix + " 1.0" + BlankLine + BlankLine + DefaultRemovedUnshippedHeader;
        private const string DefaultChangedShippedHeader = ReleaseTrackingHelper.ReleasePrefix + " 1.0" + BlankLine + BlankLine + DefaultChangedUnshippedHeader;
 
        private const string DefaultShippedHeader2 = ReleaseTrackingHelper.ReleasePrefix + " 2.0" + BlankLine + BlankLine + DefaultUnshippedHeader;
        private const string DefaultRemovedShippedHeader2 = ReleaseTrackingHelper.ReleasePrefix + " 2.0" + BlankLine + BlankLine + DefaultRemovedUnshippedHeader;
        private const string DefaultChangedShippedHeader2 = ReleaseTrackingHelper.ReleasePrefix + " 2.0" + BlankLine + BlankLine + DefaultChangedUnshippedHeader;
 
        private const string DefaultShippedHeader3 = ReleaseTrackingHelper.ReleasePrefix + " 3.0" + BlankLine + BlankLine + DefaultUnshippedHeader;
        private const string DefaultChangedShippedHeader3 = ReleaseTrackingHelper.ReleasePrefix + " 3.0" + BlankLine + BlankLine + DefaultChangedUnshippedHeader;
 
        private const string DefaultUnshippedHeaderWithBorders = ReleaseTrackingHelper.TableTitleNewRules + BlankLine + BlankLine +
            "|" + ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine1 + "|" + BlankLine +
            "|" + ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine2 + "|" + BlankLine;
 
        private const string DefaultRemovedUnshippedHeaderWithBorders = ReleaseTrackingHelper.TableTitleRemovedRules + BlankLine + BlankLine +
            "|" + ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine1 + "|" + BlankLine +
            "|" + ReleaseTrackingHelper.TableHeaderNewOrRemovedRulesLine2 + "|" + BlankLine;
 
        private const string DefaultChangedUnshippedHeaderWithBorders = ReleaseTrackingHelper.TableTitleChangedRules + BlankLine + BlankLine +
            "|" + ReleaseTrackingHelper.TableHeaderChangedRulesLine1 + "|" + BlankLine +
            "|" + ReleaseTrackingHelper.TableHeaderChangedRulesLine2 + "|" + BlankLine;
 
        private const string DefaultShippedHeaderWithBorders = ReleaseTrackingHelper.ReleasePrefix + " 1.0" + BlankLine + BlankLine + DefaultUnshippedHeaderWithBorders;
 
        private const string DefaultShippedHeader2WithBorders = ReleaseTrackingHelper.ReleasePrefix + " 2.0" + BlankLine + BlankLine + DefaultUnshippedHeaderWithBorders;
        private const string DefaultRemovedShippedHeader2WithBorders = ReleaseTrackingHelper.ReleasePrefix + " 2.0" + BlankLine + BlankLine + DefaultRemovedUnshippedHeaderWithBorders;
        private const string DefaultChangedShippedHeader2WithBorders = ReleaseTrackingHelper.ReleasePrefix + " 2.0" + BlankLine + BlankLine + DefaultChangedUnshippedHeaderWithBorders;
 
        private static DiagnosticResult GetAdditionalFileResultAt(int line, int column, string path, DiagnosticDescriptor descriptor, params object[] arguments)
        {
#pragma warning disable RS0030 // Do not use banned APIs
            return new DiagnosticResult(descriptor)
                .WithLocation(path, line, column)
#pragma warning restore RS0030 // Do not use banned APIs
                .WithArguments(arguments);
        }
 
        private static readonly ImmutableDictionary<string, ReportDiagnostic> s_nonReleaseTrackingWarningsDisabled = ImmutableDictionary<string, ReportDiagnostic>.Empty
            .Add(DiagnosticDescriptorCreationAnalyzer.DiagnosticIdMustBeInSpecifiedFormatRule.Id, ReportDiagnostic.Suppress)
            .Add(DiagnosticDescriptorCreationAnalyzer.DoNotUseReservedDiagnosticIdRule.Id, ReportDiagnostic.Suppress)
            .Add(DiagnosticDescriptorCreationAnalyzer.ProvideCustomTagsInDescriptorRule.Id, ReportDiagnostic.Suppress)
            .Add(DiagnosticDescriptorCreationAnalyzer.ProvideHelpUriInDescriptorRule.Id, ReportDiagnostic.Suppress)
            .Add(DiagnosticDescriptorCreationAnalyzer.UseCategoriesFromSpecifiedRangeRule.Id, ReportDiagnostic.Suppress)
            .Add(DiagnosticDescriptorCreationAnalyzer.UseLocalizableStringsInDescriptorRule.Id, ReportDiagnostic.Suppress)
            .Add(DiagnosticDescriptorCreationAnalyzer.UseUniqueDiagnosticIdRule.Id, ReportDiagnostic.Suppress);
 
        private static Solution DisableNonReleaseTrackingWarnings(Solution solution, ProjectId projectId)
        {
            var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!;
            compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(s_nonReleaseTrackingWarningsDisabled));
            return solution.WithProjectCompilationOptions(projectId, compilationOptions);
        }
 
        private async Task VerifyCSharpAsync(string source, string shippedText, string unshippedText, params DiagnosticResult[] expected)
        {
            var test = new CSharpCodeFixTest<DiagnosticDescriptorCreationAnalyzer, AnalyzerReleaseTrackingFix, DefaultVerifier>
            {
                TestState =
                {
                    Sources = { source },
                    AdditionalFiles = { },
                },
                ReferenceAssemblies = AdditionalMetadataReferences.Default,
            };
 
            if (shippedText != null)
            {
                test.TestState.AdditionalFiles.Add((ReleaseTrackingHelper.ShippedFileName, shippedText));
            }
 
            if (unshippedText != null)
            {
                test.TestState.AdditionalFiles.Add((ReleaseTrackingHelper.UnshippedFileName, unshippedText));
            }
 
            test.SolutionTransforms.Add(DisableNonReleaseTrackingWarnings);
            test.ExpectedDiagnostics.AddRange(expected);
            await test.RunAsync();
        }
 
        private async Task VerifyCSharpAdditionalFileFixAsync(string source, string shippedText, string oldUnshippedText, string newUnshippedText)
        {
            await VerifyAdditionalFileFixAsync(LanguageNames.CSharp, source, shippedText, oldUnshippedText, newUnshippedText, ImmutableArray<DiagnosticResult>.Empty, ImmutableArray<DiagnosticResult>.Empty);
        }
 
        private async Task VerifyCSharpAdditionalFileFixAsync(string source, string shippedText, string oldUnshippedText, string newUnshippedText,
            ImmutableArray<DiagnosticResult> additionalExpectedDiagnosticsInInput, ImmutableArray<DiagnosticResult> additionalExpectedDiagnosticsInResult)
        {
            await VerifyAdditionalFileFixAsync(LanguageNames.CSharp, source, shippedText, oldUnshippedText, newUnshippedText, additionalExpectedDiagnosticsInInput, additionalExpectedDiagnosticsInResult);
        }
 
        private async Task VerifyAdditionalFileFixAsync(string language, string source, string shippedText, string oldUnshippedText, string newUnshippedText,
            ImmutableArray<DiagnosticResult> additionalExpectedDiagnosticsInInput, ImmutableArray<DiagnosticResult> additionalExpectedDiagnosticsInResult)
        {
            var test = language == LanguageNames.CSharp
                ? new CSharpCodeFixTest<DiagnosticDescriptorCreationAnalyzer, AnalyzerReleaseTrackingFix, DefaultVerifier>()
                : (CodeFixTest<DefaultVerifier>)new VisualBasicCodeFixTest<DiagnosticDescriptorCreationAnalyzer, AnalyzerReleaseTrackingFix, DefaultVerifier>();
 
            test.ReferenceAssemblies = AdditionalMetadataReferences.Default;
            test.SolutionTransforms.Add(DisableNonReleaseTrackingWarnings);
 
            test.TestState.Sources.Add(source);
            test.TestState.AdditionalFiles.Add((ReleaseTrackingHelper.ShippedFileName, shippedText));
            test.TestState.AdditionalFiles.Add((ReleaseTrackingHelper.UnshippedFileName, oldUnshippedText));
            test.TestState.ExpectedDiagnostics.AddRange(additionalExpectedDiagnosticsInInput);
 
            test.FixedState.AdditionalFiles.Add((ReleaseTrackingHelper.ShippedFileName, shippedText));
            test.FixedState.AdditionalFiles.Add((ReleaseTrackingHelper.UnshippedFileName, newUnshippedText));
            test.FixedState.ExpectedDiagnostics.AddRange(additionalExpectedDiagnosticsInResult);
 
            await test.RunAsync();
        }
 
        private const string CSharpDiagnosticDescriptorCreationHelper = """
 
            internal static class DiagnosticDescriptorHelper
            {
                // Dummy DiagnosticDescriptor creation helper.
                public static DiagnosticDescriptor Create(
                    string id,
                    LocalizableString title,
                    LocalizableString messageFormat,
                    string category,
                    RuleLevel ruleLevel,
                    string helpLinkUri = null)
                => null;
            }
 
            namespace Microsoft.CodeAnalysis
            {
                internal enum RuleLevel
                {
                    BuildWarning = 1,
                    IdeSuggestion = 2,
                    IdeHidden_BulkConfigurable = 3,
                    Disabled = 4,
                    CandidateForRemoval = 5,
                }
            }
            """;
        #endregion
    }
}