diff --git a/src/EditorFeatures/CSharpTest/PdbSourceDocument/ImplementationAssemblyLookupServiceTests.cs b/src/EditorFeatures/CSharpTest/PdbSourceDocument/ImplementationAssemblyLookupServiceTests.cs index 80bd80ba40c7f..52e8f4a35a791 100644 --- a/src/EditorFeatures/CSharpTest/PdbSourceDocument/ImplementationAssemblyLookupServiceTests.cs +++ b/src/EditorFeatures/CSharpTest/PdbSourceDocument/ImplementationAssemblyLookupServiceTests.cs @@ -188,6 +188,126 @@ public class C }); } + [Fact] + public async Task FollowTypeForwards_Namespace() + { + var source = @" +namespace A +{ + namespace B + { + public class C + { + public class D + { + // A change + public event System.EventHandler [|E|] { add { } remove { } } + } + } + } +}"; + var typeForwardSource = @" +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(A.B.C))] +"; + + await RunTestAsync(async path => + { + MarkupTestFile.GetSpan(source, out var metadataSource, out var expectedSpan); + + // Compile reference assembly + var sourceText = SourceText.From(metadataSource, encoding: Encoding.UTF8); + var (project, symbol) = await CompileAndFindSymbolAsync(path, Location.Embedded, Location.Embedded, sourceText, c => c.GetMember("A.B.C.D.E"), buildReferenceAssembly: true); + + // Compile implementation assembly to a different DLL + var dllFilePath = Path.Combine(path, "implementation.dll"); + var sourceCodePath = Path.Combine(path, "implementation.cs"); + var pdbFilePath = Path.Combine(path, "implementation.pdb"); + var assemblyName = "implementation"; + + var workspace = TestWorkspace.Create(@$" + + + +", composition: GetTestComposition()); + + var implProject = workspace.CurrentSolution.Projects.First(); + CompileTestSource(dllFilePath, sourceCodePath, pdbFilePath, assemblyName, sourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); + + // Compile type forwarding implementation DLL + var typeForwardDllFilePath = Path.Combine(path, "typeforward.dll"); + assemblyName = "typeforward"; + + implProject = implProject.AddMetadataReference(MetadataReference.CreateFromFile(dllFilePath)); + sourceText = SourceText.From(typeForwardSource, Encoding.UTF8); + CompileTestSource(typeForwardDllFilePath, sourceCodePath, pdbFilePath, assemblyName, sourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); + + var service = workspace.GetService(); + + var foundImplementationFilePath = service.FollowTypeForwards(symbol, typeForwardDllFilePath, new NoDuplicatesLogger()); + Assert.Equal(dllFilePath, foundImplementationFilePath); + }); + } + + [Fact] + public async Task FollowTypeForwards_Generics() + { + var source = @" +namespace A +{ + namespace B + { + public class C + { + public class D + { + // A change + public event System.EventHandler [|E|] { add { } remove { } } + } + } + } +}"; + var typeForwardSource = @" +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(A.B.C<>))] +"; + + await RunTestAsync(async path => + { + MarkupTestFile.GetSpan(source, out var metadataSource, out var expectedSpan); + + // Compile reference assembly + var sourceText = SourceText.From(metadataSource, encoding: Encoding.UTF8); + var (project, symbol) = await CompileAndFindSymbolAsync(path, Location.Embedded, Location.Embedded, sourceText, c => c.GetMember("A.B.C.D.E"), buildReferenceAssembly: true); + + // Compile implementation assembly to a different DLL + var dllFilePath = Path.Combine(path, "implementation.dll"); + var sourceCodePath = Path.Combine(path, "implementation.cs"); + var pdbFilePath = Path.Combine(path, "implementation.pdb"); + var assemblyName = "implementation"; + + var workspace = TestWorkspace.Create(@$" + + + +", composition: GetTestComposition()); + + var implProject = workspace.CurrentSolution.Projects.First(); + CompileTestSource(dllFilePath, sourceCodePath, pdbFilePath, assemblyName, sourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); + + // Compile type forwarding implementation DLL + var typeForwardDllFilePath = Path.Combine(path, "typeforward.dll"); + assemblyName = "typeforward"; + + implProject = implProject.AddMetadataReference(MetadataReference.CreateFromFile(dllFilePath)); + sourceText = SourceText.From(typeForwardSource, Encoding.UTF8); + CompileTestSource(typeForwardDllFilePath, sourceCodePath, pdbFilePath, assemblyName, sourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); + + var service = workspace.GetService(); + + var foundImplementationFilePath = service.FollowTypeForwards(symbol, typeForwardDllFilePath, new NoDuplicatesLogger()); + Assert.Equal(dllFilePath, foundImplementationFilePath); + }); + } + [Fact] public async Task FollowTypeForwards_NestedType() { @@ -402,28 +522,36 @@ public class C var typeForwardDllFilePath = Path.Combine(path, "typeforward.dll"); assemblyName = "typeforward"; - implProject = implProject.AddMetadataReference(MetadataReference.CreateFromFile(dllFilePath)); - sourceText = SourceText.From(typeForwardSource, Encoding.UTF8); - CompileTestSource(typeForwardDllFilePath, sourceCodePath, pdbFilePath, assemblyName, sourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); + implProject = workspace.CurrentSolution.Projects.First().AddMetadataReference(MetadataReference.CreateFromFile(dllFilePath)); + var typeForwardSourceText = SourceText.From(typeForwardSource, Encoding.UTF8); + CompileTestSource(typeForwardDllFilePath, sourceCodePath, pdbFilePath, assemblyName, typeForwardSourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); - var typeForward2DllFilePath = Path.Combine(path, "typeforward2.dll"); - assemblyName = "typeforward2"; + // Now compile a new implementation in realimplementation.dll + var realImplementationDllFilePath = Path.Combine(path, "realimplementation.dll"); + assemblyName = "realimplementation"; - implProject = implProject.AddMetadataReference(MetadataReference.CreateFromFile(typeForwardDllFilePath)); - sourceText = SourceText.From(typeForwardSource, Encoding.UTF8); - CompileTestSource(typeForward2DllFilePath, sourceCodePath, pdbFilePath, assemblyName, sourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); + implProject = workspace.CurrentSolution.Projects.First(); + CompileTestSource(realImplementationDllFilePath, sourceCodePath, pdbFilePath, assemblyName, sourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); + + // Now compile a new implementation.dll that typeforwards to realimplementation.dll + assemblyName = "implementation"; + + implProject = workspace.CurrentSolution.Projects.First().AddMetadataReference(MetadataReference.CreateFromFile(realImplementationDllFilePath)); + CompileTestSource(dllFilePath, sourceCodePath, pdbFilePath, assemblyName, typeForwardSourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); var service = workspace.GetService(); - Assert.Equal(dllFilePath, service.FollowTypeForwards(symbol, typeForward2DllFilePath, new NoDuplicatesLogger())); + var foundImplementationFilePath = service.FollowTypeForwards(symbol, typeForwardDllFilePath, new NoDuplicatesLogger()); + Assert.Equal(realImplementationDllFilePath, foundImplementationFilePath); // We need the DLLs to exist, in order for some checks to pass correct, but to ensure // that the file isn't read, we just zero it out. File.WriteAllBytes(typeForwardDllFilePath, Array.Empty()); - File.WriteAllBytes(typeForward2DllFilePath, Array.Empty()); + File.WriteAllBytes(realImplementationDllFilePath, Array.Empty()); File.WriteAllBytes(dllFilePath, Array.Empty()); - Assert.Equal(dllFilePath, service.FollowTypeForwards(symbol, typeForward2DllFilePath, new NoDuplicatesLogger())); + foundImplementationFilePath = service.FollowTypeForwards(symbol, typeForwardDllFilePath, new NoDuplicatesLogger()); + Assert.Equal(realImplementationDllFilePath, foundImplementationFilePath); }); } diff --git a/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.cs b/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.cs index 59fc5c3a401ca..51c4f0a787903 100644 --- a/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.cs +++ b/src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.cs @@ -457,6 +457,50 @@ public class C { } } +"; + + await RunTestAsync(async path => + { + MarkupTestFile.GetSpan(source, out var metadataSource, out var expectedSpan); + + var packDir = Directory.CreateDirectory(Path.Combine(path, "packs", "MyPack.Ref", "1.0", "ref", "net6.0")).FullName; + var dataDir = Directory.CreateDirectory(Path.Combine(path, "packs", "MyPack.Ref", "1.0", "data")).FullName; + var sharedDir = Directory.CreateDirectory(Path.Combine(path, "shared", "MyPack", "1.0")).FullName; + + var sourceText = SourceText.From(metadataSource, Encoding.UTF8); + var (project, symbol) = await CompileAndFindSymbolAsync(packDir, Location.Embedded, Location.Embedded, sourceText, c => c.GetMember("C.M"), buildReferenceAssembly: true); + + var workspace = TestWorkspace.Create(@$" + + + +", composition: GetTestComposition()); + + var implProject = workspace.CurrentSolution.Projects.First(); + + // Compile implementation assembly + CompileTestSource(sharedDir, sourceText, project, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); + + // Create FrameworkList.xml + File.WriteAllText(Path.Combine(dataDir, "FrameworkList.xml"), """ + + + """); + + await GenerateFileAndVerifyAsync(project, symbol, Location.Embedded, metadataSource.ToString(), expectedSpan, expectNullResult: false); + }); + } + + [Fact] + public async Task Net6SdkLayout_TypeForward() + { + var source = @" +public class [|C|] +{ + public void M(string d) + { + } +} "; var typeForwardSource = @" [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(C))] @@ -471,7 +515,7 @@ public class C var sharedDir = Directory.CreateDirectory(Path.Combine(path, "shared", "MyPack", "1.0")).FullName; var sourceText = SourceText.From(metadataSource, Encoding.UTF8); - var (project, symbol) = await CompileAndFindSymbolAsync(packDir, Location.Embedded, Location.Embedded, sourceText, c => c.GetMember("C.M"), buildReferenceAssembly: true); + var (project, symbol) = await CompileAndFindSymbolAsync(packDir, Location.Embedded, Location.Embedded, sourceText, c => c.GetMember("C"), buildReferenceAssembly: true); var workspace = TestWorkspace.Create(@$" @@ -482,15 +526,20 @@ public class C var implProject = workspace.CurrentSolution.Projects.First(); // Compile implementation assembly - CompileTestSource(sharedDir, sourceText, project, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); + var implementationDllFilePath = Path.Combine(sharedDir, "implementation.dll"); + var sourceCodePath = Path.Combine(sharedDir, "implementation.cs"); + var pdbFilePath = Path.Combine(sharedDir, "implementation.pdb"); + var assemblyName = "implementation"; + + CompileTestSource(implementationDllFilePath, sourceCodePath, pdbFilePath, assemblyName, sourceText, project, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); - // Compile type forwarding implementation DLL - var typeForwardDllFilePath = Path.Combine(sharedDir, "typeforward.dll"); - var sourceCodePath = Path.Combine(sharedDir, "typeforward.cs"); - var pdbFilePath = Path.Combine(sharedDir, "typeforward.pdb"); - var assemblyName = "typeforward"; + // Compile type forwarding implementation DLL, that looks like reference.dll + var typeForwardDllFilePath = Path.Combine(sharedDir, "reference.dll"); + sourceCodePath = Path.Combine(sharedDir, "reference.cs"); + pdbFilePath = Path.Combine(sharedDir, "reference.pdb"); + assemblyName = "reference"; - implProject = implProject.AddMetadataReference(MetadataReference.CreateFromFile(GetDllPath(sharedDir))); + implProject = implProject.AddMetadataReference(MetadataReference.CreateFromFile(implementationDllFilePath)); sourceText = SourceText.From(typeForwardSource, Encoding.UTF8); CompileTestSource(typeForwardDllFilePath, sourceCodePath, pdbFilePath, assemblyName, sourceText, implProject, Location.Embedded, Location.Embedded, buildReferenceAssembly: false, windowsPdb: false); diff --git a/src/Features/Core/Portable/PdbSourceDocument/ImplementationAssemblyLookupService.cs b/src/Features/Core/Portable/PdbSourceDocument/ImplementationAssemblyLookupService.cs index e06c69826cd20..f6fa8cbf50d7d 100644 --- a/src/Features/Core/Portable/PdbSourceDocument/ImplementationAssemblyLookupService.cs +++ b/src/Features/Core/Portable/PdbSourceDocument/ImplementationAssemblyLookupService.cs @@ -20,6 +20,12 @@ namespace Microsoft.CodeAnalysis.PdbSourceDocument [Export(typeof(IImplementationAssemblyLookupService)), Shared] internal class ImplementationAssemblyLookupService : IImplementationAssemblyLookupService { + // We need to generate the namespace name in the same format that is used in metadata, which + // is SymbolDisplayFormat.QualifiedNameOnlyFormat, which this is a copy of. + private static readonly SymbolDisplayFormat s_metadataSymbolDisplayFormat = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); + private static readonly string PathSeparatorString = Path.DirectorySeparatorChar.ToString(); // Cache for any type forwards. Key is the dll being inspected. Value is a dictionary @@ -56,6 +62,7 @@ public bool TryFindImplementationAssemblyPath(string referencedDllPath, [NotNull // Only the top most containing type in the ExportedType table actually points to an assembly // so no point looking for nested types. var typeSymbol = MetadataAsSourceHelpers.GetTopLevelContainingNamedType(symbol); + var namespaceName = typeSymbol.ContainingNamespace.ToDisplayString(s_metadataSymbolDisplayFormat); try { @@ -66,7 +73,7 @@ public bool TryFindImplementationAssemblyPath(string referencedDllPath, [NotNull { // If there are no type forwards in this DLL, or not one for this type, then it means // we've found the right DLL - if (typeForwards?.TryGetValue((typeSymbol.ContainingNamespace.MetadataName, typeSymbol.MetadataName), out var assemblyName) != true) + if (typeForwards?.TryGetValue((namespaceName, typeSymbol.MetadataName), out var assemblyName) != true) { return dllPath; } diff --git a/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentMetadataAsSourceFileProvider.cs b/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentMetadataAsSourceFileProvider.cs index f9d75c28ed977..fa410592ec2a7 100644 --- a/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentMetadataAsSourceFileProvider.cs +++ b/src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentMetadataAsSourceFileProvider.cs @@ -117,11 +117,11 @@ internal sealed class PdbSourceDocumentMetadataAsSourceFileProvider : IMetadataA var tmpCompilation = compilationFactory .CreateCompilation("tmp", compilationFactory.GetDefaultCompilationOptions()) - .AddReferences(project.MetadataReferences) .AddReferences(dllReference); var key = SymbolKey.Create(symbolToFind, cancellationToken); - var newSymbol = key.Resolve(tmpCompilation, ignoreAssemblyKey: true, cancellationToken).Symbol; + var resolution = key.Resolve(tmpCompilation, ignoreAssemblyKey: true, cancellationToken); + var newSymbol = resolution.Symbol; if (newSymbol is null) { _logger?.Log(FeaturesResources.Could_not_find_implementation_of_symbol_0, symbolToFind.MetadataName);