From 40cfa4b8ff85eb0c37622fca8bdafb97521168f8 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 27 Sep 2021 20:25:49 +1000 Subject: [PATCH] Add documents to PDB for types that have no methods with IL (#56278) Co-authored-by: Charles Stoner Co-authored-by: samuelawuah <65207187+samaw7@users.noreply.github.com> --- .../Portable/Emitter/Model/PEModuleBuilder.cs | 115 ++++ .../Emit/PDB/TypeDefinitionDocumentTests.cs | 511 ++++++++++++++++++ .../Portable/Emit/CommonPEModuleBuilder.cs | 41 ++ .../PEWriter/MetadataWriter.PortablePdb.cs | 20 + .../Core/Portable/PEWriter/MetadataWriter.cs | 1 + .../Portable/Emit/PEModuleBuilder.vb | 95 ++++ .../Emit/PDB/TypeDefinitionDocumentTests.vb | 392 ++++++++++++++ .../PortableCustomDebugInfoKinds.cs | 1 + 8 files changed, 1176 insertions(+) create mode 100644 src/Compilers/CSharp/Test/Emit/PDB/TypeDefinitionDocumentTests.cs create mode 100644 src/Compilers/VisualBasic/Test/Emit/PDB/TypeDefinitionDocumentTests.vb diff --git a/src/Compilers/CSharp/Portable/Emitter/Model/PEModuleBuilder.cs b/src/Compilers/CSharp/Portable/Emitter/Model/PEModuleBuilder.cs index 622b37a11a41e..d86e69f73a2c3 100644 --- a/src/Compilers/CSharp/Portable/Emitter/Model/PEModuleBuilder.cs +++ b/src/Compilers/CSharp/Portable/Emitter/Model/PEModuleBuilder.cs @@ -208,6 +208,121 @@ private void ValidateReferencedAssembly(AssemblySymbol assembly, AssemblyReferen return null; } + public sealed override IEnumerable<(Cci.ITypeDefinition, ImmutableArray)> GetTypeToDebugDocumentMap(EmitContext context) + { + var typesToProcess = ArrayBuilder.GetInstance(); + var debugDocuments = ArrayBuilder.GetInstance(); + var methodDocumentList = PooledHashSet.GetInstance(); + + var namespacesAndTopLevelTypesToProcess = ArrayBuilder.GetInstance(); + namespacesAndTopLevelTypesToProcess.Push(SourceModule.GlobalNamespace); + while (namespacesAndTopLevelTypesToProcess.Count > 0) + { + var symbol = namespacesAndTopLevelTypesToProcess.Pop(); + + switch (symbol.Kind) + { + case SymbolKind.Namespace: + var location = GetSmallestSourceLocationOrNull(symbol); + + // filtering out synthesized symbols not having real source + // locations such as anonymous types, etc... + if (location != null) + { + foreach (var member in symbol.GetMembers()) + { + switch (member.Kind) + { + case SymbolKind.Namespace: + case SymbolKind.NamedType: + namespacesAndTopLevelTypesToProcess.Push((NamespaceOrTypeSymbol)member); + break; + default: + throw ExceptionUtilities.UnexpectedValue(member.Kind); + } + } + } + break; + case SymbolKind.NamedType: + Debug.Assert(debugDocuments.Count == 0); + Debug.Assert(methodDocumentList.Count == 0); + Debug.Assert(typesToProcess.Count == 0); + + var typeDefinition = (Cci.ITypeDefinition)symbol.GetCciAdapter(); + typesToProcess.Push(typeDefinition); + GetDocumentsForMethodsAndNestedTypes(methodDocumentList, typesToProcess, context); + + foreach (var loc in symbol.Locations) + { + if (!loc.IsInSource) + { + continue; + } + + var span = loc.GetLineSpan(); + var debugDocument = DebugDocumentsBuilder.TryGetDebugDocument(span.Path, basePath: null); + + // If we have a debug document that is already referenced by method debug info in this type, or a nested type, + // then we don't need to include it. Since its impossible to declare a nested type without also including + // a declaration for its containing type, we don't need to consider nested types in this method itself. + if (debugDocument is not null && !methodDocumentList.Contains(debugDocument)) + { + debugDocuments.Add(debugDocument); + } + } + + if (debugDocuments.Count > 0) + { + yield return (typeDefinition, debugDocuments.ToImmutable()); + } + + debugDocuments.Clear(); + methodDocumentList.Clear(); + break; + default: + throw ExceptionUtilities.UnexpectedValue(symbol.Kind); + } + } + + namespacesAndTopLevelTypesToProcess.Free(); + debugDocuments.Free(); + methodDocumentList.Free(); + typesToProcess.Free(); + } + + /// + /// Gets a list of documents from the method definitions in the types in or any + /// nested types of those types. + /// + private static void GetDocumentsForMethodsAndNestedTypes(PooledHashSet documentList, ArrayBuilder typesToProcess, EmitContext context) + { + while (typesToProcess.Count > 0) + { + var definition = typesToProcess.Pop(); + + var typeMethods = definition.GetMethods(context); + foreach (var method in typeMethods) + { + var body = method.GetBody(context); + if (body is null) + { + continue; + } + + foreach (var point in body.SequencePoints) + { + documentList.Add(point.Document); + } + } + + var nestedTypes = definition.GetNestedTypes(context); + foreach (var nestedTypeDefinition in nestedTypes) + { + typesToProcess.Push(nestedTypeDefinition); + } + } + } + public sealed override MultiDictionary GetSymbolToLocationMap() { var result = new MultiDictionary(); diff --git a/src/Compilers/CSharp/Test/Emit/PDB/TypeDefinitionDocumentTests.cs b/src/Compilers/CSharp/Test/Emit/PDB/TypeDefinitionDocumentTests.cs new file mode 100644 index 0000000000000..55cd246063e59 --- /dev/null +++ b/src/Compilers/CSharp/Test/Emit/PDB/TypeDefinitionDocumentTests.cs @@ -0,0 +1,511 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Text; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Debugging; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.PDB +{ + public class TypeDefinitionDocumentTests : CSharpTestBase + { + [Fact] + public void ClassWithMethod() + { + string source = @" +class M +{ + public static void A() + { + System.Console.WriteLine(); + } +} +"; + TestTypeDefinitionDocuments(new[] { source }); + } + + [Fact] + public void NestedClassWithMethod() + { + string source = @" +class C +{ + class N + { + public static void A() + { + System.Console.WriteLine(); + } + } +} +"; + TestTypeDefinitionDocuments(new[] { source }); + } + + [Fact] + public void MultiNestedClassWithMethod() + { + string source = @" +class C +{ + class N + { + class N2 + { + public static void A() + { + System.Console.WriteLine(); + } + } + } +} +"; + TestTypeDefinitionDocuments(new[] { source }); + } + + + [Fact] + public void PartialNestedClassWithMethod() + { + string source1 = @" +partial class C +{ + partial class N + { + public static void A() + { + System.Console.WriteLine(); + } + } +} +"; + string source2 = @" +partial class C +{ + partial class N + { + } +} +"; + TestTypeDefinitionDocuments(new[] { source1, source2 }, + ("C", "2.cs")); + } + + [Fact] + public void EmptyClass() + { + string source = @" +class O +{ +} +"; + TestTypeDefinitionDocuments(new[] { source }, + ("O", "1.cs")); + } + + [Fact] + public void EmptyNestedClass() + { + string source = @" +class O +{ + class N + { + } +} +"; + TestTypeDefinitionDocuments(new[] { source }, + ("O", "1.cs")); + } + + [Fact] + public void EmptyMultiNestedClass() + { + string source = @" +class O +{ + class N + { + class N2 + { + } + } +} +"; + TestTypeDefinitionDocuments(new[] { source }, + ("O", "1.cs")); + } + + [Fact] + public void MultipleClassesAndFiles() + { + string source1 = @" +class M +{ + public static void A() + { + System.Console.WriteLine(); + } +} + +class N +{ +} + +class O +{ +} +"; + + string source2 = @" +class C +{ +} + +class D +{ +} +"; + + TestTypeDefinitionDocuments(new[] { source1, source2 }, + ("N", "1.cs"), + ("O", "1.cs"), + ("C", "2.cs"), + ("D", "2.cs")); + } + + [Fact] + public void PartialClasses() + { + string source1 = @" +partial class C +{ +} +"; + string source2 = @" +partial class C +{ +} +"; + TestTypeDefinitionDocuments(new[] { source1, source2 }, + ("C", "1.cs, 2.cs")); + } + + [Fact] + public void PartialClasses2() + { + string source1 = @" +partial class C +{ +} +"; + string source2 = @" +partial class C +{ + int x = 1; + void M() { } +} +"; + TestTypeDefinitionDocuments(new[] { source1, source2 }, + ("C", "1.cs")); + } + + [Fact] + public void PartialClasses3() + { + string source1 = @" +partial class C +{ +} +"; + string source2 = @" +partial class C +{ + int x; + void M() { } +} +"; + TestTypeDefinitionDocuments(new[] { source1, source2 }, + ("C", "1.cs")); + } + + [Fact] + public void Property() + { + string source = @" +class C +{ + public int X { get; set; } +} +"; + + TestTypeDefinitionDocuments(new[] { source }); + } + + [Fact] + public void Fields() + { + string source = @" +class C +{ + int x; + const int z = 2; + int y; +} +"; + + TestTypeDefinitionDocuments(new[] { source }, + ("C", "1.cs")); + } + + [Fact] + public void Fields_WithInitializer() + { + string source = @" +class C +{ + int x; + const int z = 2; + int y = 1; +} +"; + + TestTypeDefinitionDocuments(new[] { source }); + } + + [Fact] + public void AbstractMethod() + { + string source = @" +abstract class C +{ + public abstract void M(); +} +"; + + TestTypeDefinitionDocuments(new[] { source }, + ("C", "1.cs")); + } + + [Fact] + public void ExternMethod() + { + string source = @" +class C +{ + public extern void M(); +} +"; + + TestTypeDefinitionDocuments(new[] { source }, + ("C", "1.cs")); + } + + [Fact] + public void Interfaces() + { + string source1 = @" +interface I1 +{ +} + +partial interface I2 +{ + public void F(); +} +"; + string source2 = @" +partial interface I2 +{ +} +"; + TestTypeDefinitionDocuments(new[] { source1, source2 }, + ("I1", "1.cs"), + ("I2", "1.cs, 2.cs")); + } + + [Fact] + public void Record() + { + string source = @" +record R(int X); +"; + + // The compiler synthesized methods have document info so we don't expect a type document + TestTypeDefinitionDocuments(new[] { source, IsExternalInitTypeDefinition }, + ("IsExternalInit", "2.cs")); + } + + [Fact] + public void Record_SynthesizedMember() + { + string source = @" +record R(int X) +{ + protected virtual bool PrintMembers(System.Text.StringBuilder builder) + { + return true; + } +} +"; + + TestTypeDefinitionDocuments(new[] { source, IsExternalInitTypeDefinition }, + ("IsExternalInit", "2.cs")); + } + + [Fact] + public void Enum() + { + string source = @" +enum E +{ +} + +enum E2 +{ + A, + B +} +"; + + TestTypeDefinitionDocuments(new[] { source }, + ("E", "1.cs"), + ("E2", "1.cs")); + } + + + [Fact] + public void Delegate() + { + string source = @" +delegate void D(int a); + +class C +{ + void M() + { + var x = (int a, ref int b) => a; + } +} +"; + + TestTypeDefinitionDocuments(new[] { source }, + ("D", "1.cs")); + } + + [Fact] + public void AnonymousTypes() + { + string source = @" +class C +{ + void M() + { + var x = new { Goo = 1, Bar = ""Hi"" }; + } +} +"; + + TestTypeDefinitionDocuments(new[] { source }); + } + + [Fact] + public void LineDirectives() + { + string source = @" +class C +{ +#line 1 ""C.cs"" + void M() + { + } +#line default +} + +class D +{ +#line 1 ""D.cs"" + private int _x = 1; + private int X { get; set; } = 1; +} + +#line 1 ""E.cs"" +class E +{ + void M() + { + } +} + +#line 1 ""F.cs"" +class F +{ +} +"; + + TestTypeDefinitionDocuments(new[] { source }, + ("C", "1.cs"), + ("D", "1.cs"), + ("E", "1.cs"), + ("F", "1.cs")); + } + + private static void TestTypeDefinitionDocuments(string[] sources, params (string typeName, string documentName)[] expected) + { + var trees = sources.Select((s, i) => SyntaxFactory.ParseSyntaxTree(s, path: $"{i + 1}.cs", encoding: Encoding.UTF8)).ToArray(); + var compilation = CreateCompilation(trees, options: TestOptions.DebugDll); + + var pdbStream = new MemoryStream(); + var pe = compilation.EmitToArray(EmitOptions.Default.WithDebugInformationFormat(DebugInformationFormat.PortablePdb), pdbStream: pdbStream); + pdbStream.Position = 0; + + var metadata = ModuleMetadata.CreateFromImage(pe); + var metadataReader = metadata.GetMetadataReader(); + + using var provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); + var pdbReader = provider.GetMetadataReader(); + + var actual = from handle in pdbReader.CustomDebugInformation + let entry = pdbReader.GetCustomDebugInformation(handle) + where pdbReader.GetGuid(entry.Kind).Equals(PortableCustomDebugInfoKinds.TypeDefinitionDocuments) + select (typeName: GetTypeName(entry.Parent), documentName: GetDocumentNames(entry.Value)); + + AssertEx.Equal(expected, actual, itemSeparator: ",\n", itemInspector: i => $"(\"{i.typeName}\", \"{i.documentName}\")"); + + string GetTypeName(EntityHandle handle) + { + var typeHandle = (TypeDefinitionHandle)handle; + var type = metadataReader.GetTypeDefinition(typeHandle); + return metadataReader.GetString(type.Name); + } + + string GetDocumentNames(BlobHandle value) + { + var result = new List(); + + var reader = pdbReader.GetBlobReader(value); + while (reader.RemainingBytes > 0) + { + var documentRow = reader.ReadCompressedInteger(); + if (documentRow > 0) + { + var doc = pdbReader.GetDocument(MetadataTokens.DocumentHandle(documentRow)); + result.Add(pdbReader.GetString(doc.Name)); + } + } + + return string.Join(", ", result); + } + } + } +} diff --git a/src/Compilers/Core/Portable/Emit/CommonPEModuleBuilder.cs b/src/Compilers/Core/Portable/Emit/CommonPEModuleBuilder.cs index 1baea9a9f973b..0d82279c5e7ea 100644 --- a/src/Compilers/Core/Portable/Emit/CommonPEModuleBuilder.cs +++ b/src/Compilers/Core/Portable/Emit/CommonPEModuleBuilder.cs @@ -214,6 +214,47 @@ public CommonPEModuleBuilder( /// public abstract MultiDictionary GetSymbolToLocationMap(); + /// + /// Builds a list of types, and their documents, that would otherwise not be referenced by any document info + /// of any methods in those types, or any nested types. This data is helpful for navigating to the source of + /// types that have no methods in one or more of the source files they are contained in. + /// + /// For example: + /// + /// First.cs: + /// + /// partial class Outer + /// { + /// partial class Inner + /// { + /// public void Method() + /// { + /// } + /// } + /// } + /// + /// + /// /// Second.cs: + /// + /// partial class Outer + /// { + /// partial class Inner + /// { + /// } + /// } + /// + /// + /// When navigating to the definition of "Outer" we know about First.cs because of the MethodDebugInfo for Outer.Inner.Method() + /// but there would be no document information for Second.cs so this method would return that information. + /// + /// When navigating to "Inner" we likewise know about First.cs because of the MethodDebugInfo, and we know about Second.cs because + /// of the document info for its containing type, so this method would not return information for Inner. In fact this method + /// will never return information for any nested type. + /// + /// + /// + public abstract IEnumerable<(Cci.ITypeDefinition, ImmutableArray)> GetTypeToDebugDocumentMap(EmitContext context); + /// /// Number of debug documents in the module. /// Used to determine capacities of lists and indices when emitting debug info. diff --git a/src/Compilers/Core/Portable/PEWriter/MetadataWriter.PortablePdb.cs b/src/Compilers/Core/Portable/PEWriter/MetadataWriter.PortablePdb.cs index 986d5dd7eb20f..bfd660f5e4b1f 100644 --- a/src/Compilers/Core/Portable/PEWriter/MetadataWriter.PortablePdb.cs +++ b/src/Compilers/Core/Portable/PEWriter/MetadataWriter.PortablePdb.cs @@ -987,5 +987,25 @@ private void EmbedMetadataReferenceInformation(CommonPEModuleBuilder module) kind: _debugMetadataOpt.GetOrAddGuid(PortableCustomDebugInfoKinds.CompilationMetadataReferences), value: _debugMetadataOpt.GetOrAddBlob(builder)); } + + private void EmbedTypeDefinitionDocumentInformation(CommonPEModuleBuilder module) + { + var builder = new BlobBuilder(); + + foreach (var (definition, documents) in module.GetTypeToDebugDocumentMap(Context)) + { + foreach (var document in documents) + { + var handle = GetOrAddDocument(document, _documentIndex); + builder.WriteCompressedInteger(MetadataTokens.GetRowNumber(handle)); + } + _debugMetadataOpt.AddCustomDebugInformation( + parent: GetTypeDefinitionHandle(definition), + kind: _debugMetadataOpt.GetOrAddGuid(PortableCustomDebugInfoKinds.TypeDefinitionDocuments), + value: _debugMetadataOpt.GetOrAddBlob(builder)); + builder.Clear(); + } + } + } } diff --git a/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs b/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs index 197c3e5788360..e099493058eeb 100644 --- a/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs +++ b/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs @@ -1805,6 +1805,7 @@ public void BuildMetadataAndIL( EmbedCompilationOptions(module); EmbedMetadataReferenceInformation(module); + EmbedTypeDefinitionDocumentInformation(module); } int[] methodBodyOffsets; diff --git a/src/Compilers/VisualBasic/Portable/Emit/PEModuleBuilder.vb b/src/Compilers/VisualBasic/Portable/Emit/PEModuleBuilder.vb index 006e015f25b76..fdf4abe8ad6d3 100644 --- a/src/Compilers/VisualBasic/Portable/Emit/PEModuleBuilder.vb +++ b/src/Compilers/VisualBasic/Portable/Emit/PEModuleBuilder.vb @@ -637,6 +637,101 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Emit Return container.GetSynthesizedNestedTypes() End Function + Public Overrides Iterator Function GetTypeToDebugDocumentMap(context As EmitContext) As IEnumerable(Of (Cci.ITypeDefinition, ImmutableArray(Of Cci.DebugSourceDocument))) + Dim typesToProcess = ArrayBuilder(Of Cci.ITypeDefinition).GetInstance() + Dim debugDocuments = ArrayBuilder(Of Cci.DebugSourceDocument).GetInstance() + Dim methodDocumentList = PooledHashSet(Of Cci.DebugSourceDocument).GetInstance() + + Dim namespacesAndTopLevelTypesToProcess = ArrayBuilder(Of NamespaceOrTypeSymbol).GetInstance() + namespacesAndTopLevelTypesToProcess.Push(SourceModule.GlobalNamespace) + + While namespacesAndTopLevelTypesToProcess.Count > 0 + Dim symbol = namespacesAndTopLevelTypesToProcess.Pop() + + If symbol.Locations.Length = 0 Then + Continue While + End If + + Select Case symbol.Kind + Case SymbolKind.Namespace + Dim location = GetSmallestSourceLocationOrNull(symbol) + + ' filtering out synthesized symbols not having real source + ' locations such as anonymous types, my types, etc... + If location IsNot Nothing Then + For Each member In symbol.GetMembers() + Select Case member.Kind + Case SymbolKind.Namespace, SymbolKind.NamedType + namespacesAndTopLevelTypesToProcess.Push(DirectCast(member, NamespaceOrTypeSymbol)) + Case Else + Throw ExceptionUtilities.UnexpectedValue(member.Kind) + End Select + Next + End If + + Case SymbolKind.NamedType + ' We only process top level types in this method, and only return documents for types if there are no + ' methods that would refer to the same document, either in the type or in any nested type. + Debug.Assert(debugDocuments.Count = 0) + Debug.Assert(methodDocumentList.Count = 0) + Debug.Assert(typesToProcess.Count = 0) + + Dim typeDefinition = DirectCast(symbol.GetCciAdapter(), Cci.ITypeDefinition) + typesToProcess.Push(typeDefinition) + GetDocumentsForMethodsAndNestedTypes(methodDocumentList, typesToProcess, context) + + For Each loc In symbol.Locations + If Not loc.IsInSource Then + Continue For + End If + + Dim span = loc.GetLineSpan() + Dim debugDocument = DebugDocumentsBuilder.TryGetDebugDocument(span.Path, basePath:=Nothing) + + If debugDocument IsNot Nothing AndAlso Not methodDocumentList.Contains(debugDocument) Then + debugDocuments.Add(debugDocument) + End If + Next + + If debugDocuments.Count > 0 Then + Yield (typeDefinition, debugDocuments.ToImmutable()) + End If + debugDocuments.Clear() + methodDocumentList.Clear() + Case Else + Throw ExceptionUtilities.UnexpectedValue(symbol.Kind) + End Select + End While + + namespacesAndTopLevelTypesToProcess.Free() + debugDocuments.Free() + methodDocumentList.Free() + typesToProcess.Free() + End Function + + Private Shared Sub GetDocumentsForMethodsAndNestedTypes(documentList As PooledHashSet(Of Cci.DebugSourceDocument), typesToProcess As ArrayBuilder(Of Cci.ITypeDefinition), context As EmitContext) + While typesToProcess.Count > 0 + Dim definition = typesToProcess.Pop() + + Dim typeMethods = definition.GetMethods(context) + For Each method In typeMethods + Dim body = method.GetBody(context) + If body Is Nothing Then + Continue For + End If + + For Each point In body.SequencePoints + documentList.Add(point.Document) + Next + Next + + Dim nestedTypes = definition.GetNestedTypes(context) + For Each nestedTypeDefinition In nestedTypes + typesToProcess.Push(nestedTypeDefinition) + Next + End While + End Sub + Public Sub SetDisableJITOptimization(methodSymbol As MethodSymbol) Debug.Assert(methodSymbol.ContainingModule Is Me.SourceModule AndAlso methodSymbol Is methodSymbol.OriginalDefinition) diff --git a/src/Compilers/VisualBasic/Test/Emit/PDB/TypeDefinitionDocumentTests.vb b/src/Compilers/VisualBasic/Test/Emit/PDB/TypeDefinitionDocumentTests.vb new file mode 100644 index 0000000000000..a40fca1e710ab --- /dev/null +++ b/src/Compilers/VisualBasic/Test/Emit/PDB/TypeDefinitionDocumentTests.vb @@ -0,0 +1,392 @@ +' 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. + +Imports System.IO +Imports System.Reflection.Metadata +Imports System.Reflection.Metadata.Ecma335 +Imports System.Text +Imports Microsoft.CodeAnalysis.Debugging +Imports Microsoft.CodeAnalysis.Emit +Imports Microsoft.CodeAnalysis.Test.Utilities +Imports Roslyn.Test.Utilities + +Namespace Microsoft.CodeAnalysis.VisualBasic.UnitTests.PDB + Public Class TypeDefinitionDocumentTests + Inherits BasicTestBase + + + Public Sub ClassWithMethod() + Dim source As String = " +Class M + Public Shared Sub A() + System.Console.WriteLine() + End Sub +End Class +" + TestTypeDefinitionDocuments({source}) + End Sub + + + Public Sub NestedClassWithMethod() + Dim source As String = " +Class C + Class N + Public Shared Sub A() + System.Console.WriteLine() + End Sub + End Class +End Class +" + TestTypeDefinitionDocuments({source}) + End Sub + + + Public Sub MultiNestedClassWithMethod() + Dim source As String = " +Class C + Class N + Class N2 + Public Shared Sub A() + System.Console.WriteLine() + End Sub + End Class + End Class +End Class +" + TestTypeDefinitionDocuments({source}) + End Sub + + + Public Sub PartialMultiNestedClassWithMethod() + Dim source1 As String = " +Partial Class C + Partial Class N + Public Shared Sub A() + System.Console.WriteLine() + End Sub + End Class +End Class +" + + Dim source2 As String = " +Partial Class C + Partial Class N + End Class +End Class +" + TestTypeDefinitionDocuments({source1, source2}, + ("C", "2.vb")) + End Sub + + + Public Sub EmptyClass() + Dim source As String = " +Class O +End Class +" + TestTypeDefinitionDocuments({source}, + ("O", "1.vb")) + End Sub + + + Public Sub EmptyNestedClass() + Dim source As String = " +Class O + Class N + End Class +End Class +" + TestTypeDefinitionDocuments({source}, + ("O", "1.vb")) + End Sub + + + Public Sub EmptyMultiNestedClass() + Dim source As String = " +Class O + Class N + Class N2 + End Class + End Class +End Class +" + TestTypeDefinitionDocuments({source}, + ("O", "1.vb")) + End Sub + + + Public Sub MultipleClassesAndFiles() + Dim source1 As String = " +Class M + Public Shared Sub A() + System.Console.WriteLine() + End Sub +End Class + +Class N +End Class + +Class O +End Class +" + Dim source2 As String = " +Class C +End Class + +Class D +End Class +" + TestTypeDefinitionDocuments({source1, source2}, + ("N", "1.vb"), + ("O", "1.vb"), + ("C", "2.vb"), + ("D", "2.vb")) + End Sub + + + Public Sub PartialClasses() + Dim source1 As String = " +Partial Class C +End Class +" + Dim source2 As String = " +Partial Class C +End Class +" + TestTypeDefinitionDocuments({source1, source2}, + ("C", "1.vb, 2.vb")) + End Sub + + + Public Sub PartialClasses2() + Dim source1 As String = " +Partial Class C +End Class +" + Dim source2 As String = " +Partial Class C + Private x As Integer + + Sub M() + End Sub +End Class +" + TestTypeDefinitionDocuments({source1, source2}, + ("C", "1.vb")) + End Sub + + + Public Sub PartialClasses3() + Dim source1 As String = " +Partial Class C +End Class +" + Dim source2 As String = " +Partial Class C + Private x As Integer = 1 + + Sub M() + End Sub +End Class +" + TestTypeDefinitionDocuments({source1, source2}, + ("C", "1.vb")) + End Sub + + + Public Sub [Property]() + Dim source As String = " +Class C + Public Property X As Integer +End Class + +Class D + Public Property Y As Integer = 4 +End Class +" + TestTypeDefinitionDocuments({source}, + ("C", "1.vb")) + End Sub + + + Public Sub Fields() + Dim source As String = " +Class C + Private x As Integer + Private Const y As Integer = 3 +End Class +" + TestTypeDefinitionDocuments({source}, + ("C", "1.vb")) + End Sub + + + Public Sub Fields_WithInitializer() + Dim source As String = " +Class C + Private x As Integer = 1 + Private Const y As Integer = 3 +End Class +" + TestTypeDefinitionDocuments({source}) + End Sub + + + + Public Sub AbstractMethod() + Dim source As String = " +MustInherit Class C + MustOverride Sub M() +End Class +" + TestTypeDefinitionDocuments({source}, + ("C", "1.vb")) + End Sub + + + Public Sub Interfaces() + Dim source1 As String = " +Interface I1 +End Interface + +Partial Interface I2 + Sub M() +End Interface +" + Dim source2 As String = " +Partial Interface I2 +End Interface +" + TestTypeDefinitionDocuments({source1, source2}, + ("I1", "1.vb"), + ("I2", "1.vb, 2.vb")) + End Sub + + + Public Sub [Enum]() + Dim source As String = " +Enum E + A + B +End Enum +" + TestTypeDefinitionDocuments({source}, + ("E", "1.vb")) + End Sub + + + Public Sub [Delegate]() + Dim source As String = " +Delegate Sub D(a As Integer) + +Class C + Public Sub M() + Dim y = ( + Function(ByRef a As Integer) + Return a + End Function + ) + End Sub +End Class +" + TestTypeDefinitionDocuments({source}, + ("D", "1.vb")) + End Sub + + + Public Sub AnonymousType() + Dim source As String = " +Public Class C + Public Sub M() + Dim x = New With { .Goo = 1, .Bar = ""Hi"" } + End Sub +End Class +" + TestTypeDefinitionDocuments({source}) + End Sub + + + Public Sub ExternalSourceDirectives() + Dim source As String = " +Class C +#ExternalSource (""C.vb"", 1) + Public Sub M() + End Sub +#End ExternalSource +End Class + +Class D +#ExternalSource (""D.vb"", 1) + Private _x As Integer = 1 + Private Property Y As Integer = 1 +#End ExternalSource +End Class + +#ExternalSource (""E.vb"", 1) +Class E + Public Sub M() + End Sub +End Class +#End ExternalSource + +#ExternalSource (""F.vb"", 1) +Class F +End Class +#End ExternalSource +" + TestTypeDefinitionDocuments({source}, + ("C", "1.vb"), + ("D", "1.vb"), + ("E", "1.vb"), + ("F", "1.vb")) + End Sub + + Public Shared Sub TestTypeDefinitionDocuments(sources As String(), ParamArray expected As (String, String)()) + Dim trees = sources.Select(Function(s, i) SyntaxFactory.ParseSyntaxTree(s, path:=$"{i + 1}.vb", encoding:=Encoding.UTF8)).ToArray() + Dim compilation = CreateCompilation(trees, options:=TestOptions.DebugDll) + + Dim pdbStream = New MemoryStream() + Dim pe = compilation.EmitToArray(EmitOptions.[Default].WithDebugInformationFormat(DebugInformationFormat.PortablePdb), pdbStream:=pdbStream) + pdbStream.Position = 0 + + Dim metadata = ModuleMetadata.CreateFromImage(pe) + Dim metadataReader = metadata.GetMetadataReader() + + Using provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream) + Dim pdbReader = provider.GetMetadataReader() + + Dim actual = From handle In pdbReader.CustomDebugInformation + Let entry = pdbReader.GetCustomDebugInformation(handle) + Where pdbReader.GetGuid(entry.Kind).Equals(PortableCustomDebugInfoKinds.TypeDefinitionDocuments) + Select (GetTypeName(metadataReader, entry.Parent), GetDocumentNames(pdbReader, entry.Value)) + + AssertEx.Equal(expected, actual, itemSeparator:=", ", itemInspector:=Function(i) $"(""{i.Item1}"", ""{i.Item2}"")") + End Using + End Sub + + Private Shared Function GetTypeName(metadataReader As MetadataReader, handle As EntityHandle) As String + Dim typeHandle = CType(handle, TypeDefinitionHandle) + Dim type = metadataReader.GetTypeDefinition(typeHandle) + Return metadataReader.GetString(type.Name) + End Function + + Private Shared Function GetDocumentNames(pdbReader As MetadataReader, value As BlobHandle) As String + Dim result = New List(Of String)() + Dim reader = pdbReader.GetBlobReader(value) + + While reader.RemainingBytes > 0 + Dim documentRow = reader.ReadCompressedInteger() + + If documentRow > 0 Then + Dim doc = pdbReader.GetDocument(MetadataTokens.DocumentHandle(documentRow)) + result.Add(pdbReader.GetString(doc.Name)) + End If + End While + + ' Order can be different in net472 vs net5 :( + result.Sort() + Return String.Join(", ", result) + End Function + End Class +End Namespace diff --git a/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs b/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs index 331d772b21188..d05cb8526a91b 100644 --- a/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs +++ b/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs @@ -21,5 +21,6 @@ internal static class PortableCustomDebugInfoKinds public static readonly Guid EmbeddedSource = new("0E8A571B-6926-466E-B4AD-8AB04611F5FE"); public static readonly Guid CompilationMetadataReferences = new("7E4D4708-096E-4C5C-AEDA-CB10BA6A740D"); public static readonly Guid CompilationOptions = new("B5FEEC05-8CD0-4A83-96DA-466284BB4BD8"); + public static readonly Guid TypeDefinitionDocuments = new("932E74BC-DBA9-4478-8D46-0F32A7BAB3D3"); } }