File: SymbolKey\SymbolKeyTests.cs
Web Access
Project: src\src\EditorFeatures\CSharpTest\Microsoft.CodeAnalysis.CSharp.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.EditorFeatures.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.SymbolId;
 
[UseExportProvider]
public class SymbolKeyTests
{
    [Fact]
    public async Task FileType_01()
    {
        var typeSource = """
            file class C1
            {
                public static void M() { }
            }
            """;
 
        var workspaceXml = @$"
<Workspace>
    <Project Language=""C#"">
        <CompilationOptions Nullable=""Enable""/>
        <Document FilePath=""C.cs"">
{typeSource}
        </Document>
    </Project>
</Workspace>
";
        using var workspace = EditorTestWorkspace.Create(workspaceXml);
 
        var solution = workspace.CurrentSolution;
        var project = solution.Projects.Single();
 
        var compilation = await project.GetCompilationAsync();
        var type = compilation.GlobalNamespace.GetMembers("C1").Single();
        Assert.NotNull(type);
        var symbolKey = SymbolKey.Create(type);
        var resolved = symbolKey.Resolve(compilation).Symbol;
        Assert.Same(type, resolved);
    }
 
    [Fact]
    public async Task FileType_02()
    {
        var workspaceXml = $$"""
<Workspace>
    <Project Language="C#">
        <CompilationOptions Nullable="Enable"/>
        <Document FilePath="File0.cs">
file class C
{
    public static void M() { }
}
        </Document>
        <Document FilePath="File1.cs">
file class C
{
    public static void M() { }
}
        </Document>
    </Project>
</Workspace>
""";
        using var workspace = EditorTestWorkspace.Create(workspaceXml);
 
        var solution = workspace.CurrentSolution;
        var project = solution.Projects.Single();
 
        var compilation = await project.GetCompilationAsync();
 
        var members = compilation.GlobalNamespace.GetMembers("C").ToArray();
        Assert.Equal(2, members.Length);
 
        var type = members[0];
        Assert.NotNull(type);
        var symbolKey = SymbolKey.Create(type);
        var resolved = symbolKey.Resolve(compilation).Symbol;
        Assert.Same(type, resolved);
 
        type = members[1];
        Assert.NotNull(type);
        symbolKey = SymbolKey.Create(type);
        resolved = symbolKey.Resolve(compilation).Symbol;
        Assert.Same(type, resolved);
    }
 
    [Fact]
    public async Task FileType_03()
    {
        var workspaceXml = $$"""
<Workspace>
    <Project Language="C#">
        <CompilationOptions Nullable="Enable"/>
        <Document FilePath="File0.cs">
file class C
{
    public class Inner { }
}
        </Document>
    </Project>
</Workspace>
""";
        using var workspace = EditorTestWorkspace.Create(workspaceXml);
 
        var solution = workspace.CurrentSolution;
        var project = solution.Projects.Single();
 
        var compilation = await project.GetCompilationAsync();
 
        var type = compilation.GlobalNamespace.GetMembers("C").Single().GetMembers("Inner").Single();
        Assert.NotNull(type);
        var symbolKey = SymbolKey.Create(type);
        var resolved = symbolKey.Resolve(compilation).Symbol;
        Assert.Same(type, resolved);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/45437")]
    public async Task TestGenericsAndNullability()
    {
        var typeSource = """
            #nullable enable
 
                public sealed class ConditionalWeakTableTest<TKey, TValue> /*: IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable*/
                    where TKey : class
                    where TValue : class
                {
                    public ConditionalWeakTable() { }
                    public void Add(TKey key, TValue value) { }
                    public void AddOrUpdate(TKey key, TValue value) { }
                    public void Clear() { }
                    public TValue GetOrCreateValue(TKey key) => default;
                    public TValue GetValue(TKey key, ConditionalWeakTableTest<TKey, TValue>.CreateValueCallback createValueCallback) => default;
                    public bool Remove(TKey key) => false;
 
                    public delegate TValue CreateValueCallback(TKey key);
                }
            """.Replace("<", "&lt;").Replace(">", "&gt;");
 
        var workspaceXml = @$"
<Workspace>
    <Project Language=""C#"">
        <CompilationOptions Nullable=""Enable""/>
        <Document FilePath=""C.cs"">
{typeSource}
        </Document>
    </Project>
</Workspace>
";
        using var workspace = EditorTestWorkspace.Create(workspaceXml);
 
        var solution = workspace.CurrentSolution;
        var project = solution.Projects.Single();
 
        var compilation = await project.GetCompilationAsync();
 
        var type = compilation.GetTypeByMetadataName("ConditionalWeakTableTest`2");
        var method = type.GetMembers("GetValue").OfType<IMethodSymbol>().Single();
        var callbackParamater = method.Parameters[1];
        var parameterType = callbackParamater.Type;
        Assert.Equal("global::ConditionalWeakTableTest<TKey!, TValue!>.CreateValueCallback!", parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNotNullableReferenceTypeModifier)));
 
        var symbolKey = SymbolKey.Create(method);
        var resolved = symbolKey.Resolve(compilation).Symbol;
 
        Assert.Equal(method, resolved);
    }
 
    [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1178861")]
    [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1192188")]
    [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1192486")]
    public async Task ResolveBodySymbolsInMultiProjectReferencesToOriginalProjectAsync()
    {
        var random = new Random(Seed: 0);
 
        // try to trigger race caused by ability to find reference in multiple potential projects depending on what
        // order things are in in internal collections.  This test was always able to hit the issue prior to the fix
        // going in, but does not hit it with the fix.  While this doesn't prove the race is gone, it strongly
        // implies it.
        for (var i = 0; i < 100; i++)
        {
            using var workspace = GetWorkspace();
            var solution = workspace.CurrentSolution;
 
            var bodyProject = solution.Projects.Single(p => p.AssemblyName == "BodyProject");
            var referenceProject = solution.Projects.Single(p => p.AssemblyName == "ReferenceProject");
 
            var (bodyCompilation, referenceCompilation) = await GetCompilationsAsync(bodyProject, referenceProject);
            var (bodyLocalSymbol, referenceAssemblySymbol) = await GetSymbolsAsync(bodyCompilation, referenceCompilation);
 
            var (bodyLocalProjectId, referenceAssemblyProjectId) = GetOriginatingProjectIds(solution, bodyLocalSymbol, referenceAssemblySymbol);
 
            Assert.True(bodyProject.Id == bodyLocalProjectId, $"Expected {bodyProject.Id} == {bodyLocalProjectId}. {i}");
            Assert.Equal(referenceProject.Id, referenceAssemblyProjectId);
        }
 
        return;
 
        TestWorkspace GetWorkspace()
        {
            var bodyProject = """
                    <Project Language="C#" AssemblyName="BodyProject" CommonReferences="true">
                        <Document>
                class Program
                {
                    void M()
                    {
                        int local;
                    }
                }
                        </Document>
                    </Project>
                """;
            var referenceProject = """
                <Project Language="C#" AssemblyName="ReferenceProject" CommonReferences="true">
                    <ProjectReference>BodyProject</ProjectReference>
                    <Document>
                    </Document>
                </Project>
                """;
 
            // Randomize the order of the projects in the workspace.
            if (random.Next() % 2 == 0)
            {
                return TestWorkspace.CreateWorkspace(XElement.Parse($@"
<Workspace>
    {bodyProject}
    {referenceProject}
</Workspace>
"));
            }
            else
            {
                return TestWorkspace.CreateWorkspace(XElement.Parse($@"
<Workspace>
    {referenceProject}
    {bodyProject}
</Workspace>
"));
            }
        }
 
        async Task<(Compilation bodyCompilation, Compilation referenceCompilation)> GetCompilationsAsync(Project bodyProject, Project referenceProject)
        {
            // Randomize the order that we get compilations (and thus populate our internal caches).
            Compilation bodyCompilation, referenceCompilation;
            if (random.Next() % 2 == 0)
            {
                bodyCompilation = await bodyProject.GetCompilationAsync();
                referenceCompilation = await referenceProject.GetCompilationAsync();
            }
            else
            {
                referenceCompilation = await referenceProject.GetCompilationAsync();
                bodyCompilation = await bodyProject.GetCompilationAsync();
            }
 
            return (bodyCompilation, referenceCompilation);
        }
 
        async Task<(ISymbol bodyLocalSymbol, ISymbol referenceAssemblySymbol)> GetSymbolsAsync(Compilation bodyCompilation, Compilation referenceCompilation)
        {
            // Randomize the order that we get symbols from each project.
            ISymbol bodyLocalSymbol, referenceAssemblySymbol;
            if (random.Next() % 2 == 0)
            {
                bodyLocalSymbol = await GetBodyLocalSymbol(bodyCompilation);
                referenceAssemblySymbol = referenceCompilation.Assembly;
            }
            else
            {
                referenceAssemblySymbol = referenceCompilation.Assembly;
                bodyLocalSymbol = await GetBodyLocalSymbol(bodyCompilation);
            }
 
            return (bodyLocalSymbol, referenceAssemblySymbol);
        }
 
        async Task<ILocalSymbol> GetBodyLocalSymbol(Compilation bodyCompilation)
        {
            var tree = bodyCompilation.SyntaxTrees.Single();
            var semanticModel = bodyCompilation.GetSemanticModel(tree);
 
            var root = await tree.GetRootAsync();
            var varDecl = root.DescendantNodesAndSelf().OfType<VariableDeclaratorSyntax>().Single();
 
            var local = (ILocalSymbol)semanticModel.GetDeclaredSymbol(varDecl);
            Assert.NotNull(local);
 
            return local;
        }
 
        (ProjectId bodyLocalProjectId, ProjectId referenceAssemblyProjectId) GetOriginatingProjectIds(Solution solution, ISymbol bodyLocalSymbol, ISymbol referenceAssemblySymbol)
        {
            // Randomize the order that we get try to get the originating project for the symbol.
            ProjectId bodyLocalProjectId, referenceAssemblyProjectId;
            if (random.Next() % 2 == 0)
            {
                bodyLocalProjectId = solution.GetOriginatingProjectId(bodyLocalSymbol);
                referenceAssemblyProjectId = solution.GetOriginatingProjectId(referenceAssemblySymbol);
            }
            else
            {
                referenceAssemblyProjectId = solution.GetOriginatingProjectId(referenceAssemblySymbol);
                bodyLocalProjectId = solution.GetOriginatingProjectId(bodyLocalSymbol);
            }
 
            return (bodyLocalProjectId, referenceAssemblyProjectId);
        }
    }
}