File: InternalReferencedInPublicDocAnalyzerTests.cs
Web Access
Project: src\test\Analyzers\Microsoft.Analyzers.Local.Tests\Microsoft.Analyzers.Local.Tests.csproj (Microsoft.Analyzers.Local.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.LocalAnalyzers.Resource.Test;
using Xunit;
 
namespace Microsoft.Extensions.LocalAnalyzers.Test;
 
public class InternalReferencedInPublicDocAnalyzerTests
{
    private static readonly string[] _members =
    {
        "void Method() {}", "int Property {get; set;}", "event System.EventHandler Event;", "int _field;", "string this[int i] { get => string.Empty; set {} }",
    };
    private static readonly string[] _membersReferenced = { "void Referenced() {}", "int Referenced {get; set;}", "event System.EventHandler Referenced;", "int Referenced;", };
    private static readonly string[] _typesReferenced = { "class Referenced {}", "struct Referenced {}", "interface Referenced {}", "delegate void Referenced(int value);", "enum Referenced {}", };
    private static readonly string[] _compositeTypeNames = { "class", "struct", "interface" };
 
    public static IEnumerable<Assembly> References => new Assembly[]
    {
        // add references here
    };
 
    public static IEnumerable<object[]> GetMemberAndTypePairs(string memberAccessModifier, string typeAccessModifier)
    {
        return MakePairs(memberAccessModifier, _members, typeAccessModifier, _typesReferenced);
    }
 
    public static IEnumerable<object[]> GetCompositeTypeAndMemberPairs(string typeAccessModifier, string memberAccessModifier)
    {
        return MakePairs(typeAccessModifier, _compositeTypeNames, memberAccessModifier, _membersReferenced);
    }
 
    private static IEnumerable<object[]> MakePairs(string firstPrefix, IReadOnlyList<string> firstList, string secondPrefix, IReadOnlyList<string> secondList)
    {
        var casesCnt = Math.Max(firstList.Count, secondList.Count);
        for (var i = 0; i < casesCnt; i++)
        {
            var first = $"{firstPrefix} {firstList[i % firstList.Count]}";
            var second = $"{secondPrefix} {secondList[i % secondList.Count]}";
            yield return new object[] { first, second };
        }
    }
 
    private static string MemberReferencesTopLevelTypeLine6(string classAccess, string member, string type)
    {
        return @"
            namespace Example;
        
            " + classAccess + @" class TestClass
            {
                /// <summary>
                /// Does something with <see cref=""Referenced""/>. This is line #6.
                /// </summary>
                " + member + @"
            }
 
            " + type + @"
            ";
    }
 
    private static string TopLevelTypeReferencesItsMemberLine4(string type, string member)
    {
        return @"
            namespace Example
            {
                /// <summary>
                /// Does something with <see cref=""Referenced""/>. This is line #4.
                /// </summary>
                " + type + @" Test
                {
                    " + member + @"
                }
            }
            ";
    }
 
    [Fact]
    public void CheckExceptionIsThrownWhenNullIsPassedToInitializeCall()
    {
        var a = new InternalReferencedInPublicDocAnalyzer();
        Assert.Throws<ArgumentNullException>(() => a.Initialize(null!));
    }
 
    [Theory]
    [MemberData(nameof(GetMemberAndTypePairs), "public", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "public", "")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected", "")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected internal", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected internal", "")]
    public async Task ShouldIndicateWhenExternallyVisibleMemberReferencesTopLevelInternalType(string member, string type)
    {
        var source = MemberReferencesTopLevelTypeLine6("public", member, type);
 
        var result = await Analyze(source);
 
        AssertDetected(result, source, 6, "Referenced");
    }
 
    [Theory]
    [MemberData(nameof(GetMemberAndTypePairs), "public", "public")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected", "public")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected internal", "public")]
    [MemberData(nameof(GetMemberAndTypePairs), "private", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "private", "")]
    [MemberData(nameof(GetMemberAndTypePairs), "private protected", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "private protected", "")]
    [MemberData(nameof(GetMemberAndTypePairs), "internal", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "internal", "")]
    public async Task ShouldNotIndicateWhenMemberReferencesTopLevelType(string member, string type)
    {
        var source = MemberReferencesTopLevelTypeLine6("public", member, type);
 
        var result = await Analyze(source);
 
        AssertNotDetected(result);
    }
 
    [Theory]
    [MemberData(nameof(GetMemberAndTypePairs), "public", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "public", "")]
    [MemberData(nameof(GetMemberAndTypePairs), "public", "public")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected", "")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected", "public")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected internal", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected internal", "")]
    [MemberData(nameof(GetMemberAndTypePairs), "protected internal", "public")]
    [MemberData(nameof(GetMemberAndTypePairs), "private", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "private", "")]
    [MemberData(nameof(GetMemberAndTypePairs), "private protected", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "private protected", "")]
    [MemberData(nameof(GetMemberAndTypePairs), "internal", "internal")]
    [MemberData(nameof(GetMemberAndTypePairs), "internal", "")]
    public async Task ShouldNotIndicateWhenInternalClassMemberReferencesTopLevelType(string member, string type)
    {
        var source = MemberReferencesTopLevelTypeLine6("internal", member, type);
 
        var result = await Analyze(source);
 
        AssertNotDetected(result);
    }
 
    [Theory]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "public", "internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "public", "private")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "public", "private protected")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected", "internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected", "private")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected", "private protected")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected internal", "internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected internal", "private")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected internal", "private protected")]
    public async Task ShouldIndicateWhenExternallyVisibleTopLevelTypeReferencesItsInvisibleMember(string type, string member)
    {
        var source = TopLevelTypeReferencesItsMemberLine4(type, member);
 
        var result = await Analyze(source);
 
        AssertDetected(result, source, 4, "Referenced");
    }
 
    [Theory]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "public", "public")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "public", "protected")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "public", "protected internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected", "public")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected", "protected")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected", "protected internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected internal", "public")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected internal", "protected")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "protected internal", "protected internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "internal", "public")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "internal", "internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "internal", "protected")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "internal", "protected internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "internal", "private")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "internal", "private protected")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "", "public")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "", "internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "", "protected")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "", "protected internal")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "", "private")]
    [MemberData(nameof(GetCompositeTypeAndMemberPairs), "", "private protected")]
    public async Task ShouldNotIndicateWhenTopLevelTypeReferencesItsMember(string type, string member)
    {
        var source = TopLevelTypeReferencesItsMemberLine4(type, member);
 
        var result = await Analyze(source);
 
        AssertNotDetected(result);
    }
 
    [Theory]
    [InlineData("public", false)]
    [InlineData("internal", true)]
    [InlineData("", true)]
    public async Task ShouldSupportReferencesToEnumMembers(string enumAccess, bool shouldIndicate)
    {
        var source = @"
            namespace Example
            {
                /// <summary>
                /// Use <see cref=""MyEnum.Member1""/>. This is line #4.
                /// </summary>
                public class TestClass
                {
                    public void Method X() {}
                }
 
                " + enumAccess + @" enum MyEnum
                {
                    Member1,
                    Member2,
                }
            }
            ";
 
        var result = await Analyze(source);
 
        if (shouldIndicate)
        {
            AssertDetected(result, source, 4, "MyEnum.Member1");
        }
        else
        {
            AssertNotDetected(result);
        }
    }
 
    [Theory]
    [InlineData("public", "public", false)]
    [InlineData("public", "internal", true)]
    [InlineData("internal", "public", false)]
    [InlineData("", "internal", false)]
    public async Task ShouldSupportCommentsOnEnumMembers(string enumAccess, string typeAccess, bool shouldIndicate)
    {
        var source = @"
            namespace Example
            {
                " + typeAccess + @" class Referenced
                {
                    public void Method X() {}
                }
 
                " + enumAccess + @" enum MyEnum
                {
                    Member1,
 
                    /// <summary>
                    /// Uses <see cref=""Referenced""/>. This is line #13.
                    /// </summary>
                    Member2,
                }
            }
            ";
 
        var result = await Analyze(source);
 
        if (shouldIndicate)
        {
            AssertDetected(result, source, 13, "Referenced");
        }
        else
        {
            AssertNotDetected(result);
        }
    }
 
    [Theory]
    [InlineData("public", "public", false)]
    [InlineData("public", "internal", true)]
    [InlineData("public", "private", true)]
    [InlineData("public", "protected", false)]
    [InlineData("public", "protected internal", false)]
    [InlineData("public", "private protected", true)]
    [InlineData("internal", "public", true)]
    [InlineData("internal", "internal", true)]
    [InlineData("internal", "private", true)]
    [InlineData("internal", "protected", true)]
    [InlineData("internal", "protected internal", true)]
    [InlineData("internal", "private protected", true)]
    [InlineData("", "public", true)]
    [InlineData("", "internal", true)]
    [InlineData("", "private", true)]
    [InlineData("", "protected", true)]
    [InlineData("", "protected internal", true)]
    [InlineData("", "private protected", true)]
    public async Task ShouldSupportCrefPointingToNestedType(string enclosingTypeAccess, string nestedTypeAccess, bool shouldIndicate)
    {
        var source = @"
            namespace Example
            {
                " + enclosingTypeAccess + @" class Enclosing
                {
                    " + nestedTypeAccess + @" class Referenced
                    {
                    }
                }
 
                /// <summary>
                /// Uses <see cref=""Enclosing.Referenced""/>. This is line #11.
                /// </summary>
                public interface ITest
                {
                }
            }
            ";
 
        var result = await Analyze(source);
 
        if (shouldIndicate)
        {
            AssertDetected(result, source, 11, "Enclosing.Referenced");
        }
        else
        {
            AssertNotDetected(result);
        }
    }
 
    [Theory]
    [InlineData("public", "public", true)]
    [InlineData("public", "internal", false)]
    [InlineData("public", "private", false)]
    [InlineData("public", "protected", true)]
    [InlineData("public", "protected internal", true)]
    [InlineData("public", "private protected", false)]
    [InlineData("internal", "public", false)]
    [InlineData("internal", "internal", false)]
    [InlineData("internal", "private", false)]
    [InlineData("internal", "protected", false)]
    [InlineData("internal", "protected internal", false)]
    [InlineData("internal", "private protected", false)]
    [InlineData("", "public", false)]
    [InlineData("", "internal", false)]
    [InlineData("", "private", false)]
    [InlineData("", "protected", false)]
    [InlineData("", "protected internal", false)]
    [InlineData("", "private protected", false)]
    public async Task ShouldSupportCrefOnNestedType(string enclosingTypeAccess, string nestedTypeAccess, bool shouldIndicate)
    {
        var source = @"
            namespace Example
            {
                " + enclosingTypeAccess + @" class Enclosing
                {
                    /// <summary>
                    /// Uses <see cref=""Referenced""/>. This is line #6.
                    /// </summary>
                    " + nestedTypeAccess + @" class Test
                    {
                    }
                }
 
                internal interface Referenced
                {
                }
            }
            ";
 
        var result = await Analyze(source);
 
        if (shouldIndicate)
        {
            AssertDetected(result, source, 6, "Referenced");
        }
        else
        {
            AssertNotDetected(result);
        }
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("NotExists")]
    [InlineData("this is not a valid reference")]
    [InlineData("$^&#@")]
    public async Task ShouldNotIndicateWhenCrefIsInvalid(string cref)
    {
        var source = @"
            namespace Example
            {
                public class TestClass
                {
                    /// <summary>
                    /// Use <see cref=""" + cref + @"""/>.
                    /// </summary>
                    public void Method X() {}
                }
            }
            ";
 
        var result = await Analyze(source);
 
        AssertNotDetected(result);
    }
 
    [Fact]
    public async Task ShouldNotIndicateWhenCommentIsOrphan()
    {
        var source = @"
            namespace Example
            {
                public class TestClass
                {
                    /// <summary>
                    /// Use <see cref=""Referenced""/>.
                    /// </summary>                    
                }
 
                internal class Referenced {}
            }
            ";
 
        var result = await Analyze(source);
 
        AssertNotDetected(result);
    }
 
    [Fact]
    public async Task ShouldNotIndicateWhenCRefDoesNotBelongToXmlDocumentation()
    {
        var source = @"
            namespace Example
            {
                /*
                Type <see cref=""Referenced""/> is referenced
                */
                public class TestClass
                {
                    // Use <see cref=""Referenced""/>
                    public void Method() {};
                }
 
                internal class Referenced {}
            }
            ";
 
        var result = await Analyze(source);
 
        AssertNotDetected(result);
    }
 
    private static void AssertDetected(IReadOnlyList<Diagnostic> result, string source, int lineNumber, string detectedText) =>
        AssertDetected(result, source, new[] { lineNumber }, new[] { detectedText });
 
    private static void AssertDetected(IReadOnlyList<Diagnostic> result, string source, int[] lineNumbers, string[] detectedTexts)
    {
        Debug.Assert(lineNumbers.Length == detectedTexts.Length, "Line numbers and texts should be the same length");
 
        var detected = result.Where(IsInternalReferencedInPublicDocDiagnostic).ToList();
 
        var expectedNumberOfWarnings = lineNumbers.Length;
        Assert.Equal(expectedNumberOfWarnings, detected.Count);
 
        for (int i = 0; i < detected.Count; i++)
        {
            var location = detected[i].Location;
            Assert.Equal(lineNumbers[i], location.GetLineSpan().StartLinePosition.Line);
 
            var text = source.Substring(location.SourceSpan.Start, location.SourceSpan.Length);
            Assert.Equal(detectedTexts[i], text, StringComparer.Ordinal);
        }
    }
 
    private static bool IsInternalReferencedInPublicDocDiagnostic(Diagnostic d) => ReferenceEquals(d.Descriptor, DiagDescriptors.InternalReferencedInPublicDoc);
 
    private static void AssertNotDetected(IReadOnlyList<Diagnostic> result)
    {
        var detected = result.Where(IsInternalReferencedInPublicDocDiagnostic);
        Assert.Empty(detected);
    }
 
    private static async Task<IReadOnlyList<Diagnostic>> Analyze(string source)
    {
        return await RoslynTestUtils.RunAnalyzer(
            new InternalReferencedInPublicDocAnalyzer(),
            References,
            new[] { source }).ConfigureAwait(false);
    }
}