Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/Docfx.Dotnet/ManagedReference/Resolvers/ResolveReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ internal class ResolveReference : IResolverPipeline
{
public void Run(MetadataModel yaml, ResolverContext context)
{
var pages = new List<MetadataItem>();

TreeIterator.Preorder(yaml.TocYamlViewModel, null,
s => s.IsInvalid ? null : s.Items,
(current, parent) =>
Expand All @@ -21,6 +23,7 @@ public void Run(MetadataModel yaml, ResolverContext context)
{
page = current;
current.References = [];
pages.Add(page);
}
else
{
Expand All @@ -46,8 +49,42 @@ public void Run(MetadataModel yaml, ResolverContext context)
}
}
AddIndirectReference(context, page, addingReferences);

return true;
});

foreach (var page in pages)
{
ClearBrokenHrefs(page, context);
}
}

private static void ClearBrokenHrefs(MetadataItem page, ResolverContext context)
{
foreach (var reference in page.References.Values)
{
foreach (var parts in new[] { reference.NameParts, reference.NameWithTypeParts, reference.QualifiedNameParts })
{
if (parts is null)
{
continue;
}

foreach (var items in parts.Values)
{
foreach (var item in items)
{
if (item.Name is not null
&& item.Href is not null
&& !item.Href.StartsWith("http", StringComparison.Ordinal)
&& !context.Members.ContainsKey(item.Name))
{
item.Href = null;
}
}
}
}
}
}

private static void TryAddReference(ResolverContext context, MetadataItem page, List<ReferenceItem> addingReferences, string key)
Expand Down
82 changes: 82 additions & 0 deletions test/Docfx.Dotnet.Tests/GenerateMetadataFromCSUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3782,4 +3782,86 @@ public class Foo
Assert.Contains("TupleLibrary", output.References.Keys);
Assert.Contains("TupleLibrary.XmlTasks", output.References.Keys);
}

[Fact]
public void TestGenerateMetadataWithNestedNamespaceHrefs()
{
// https://github.com/dotnet/docfx/issues/10588
var codeA = """
namespace Foo.Bar
{
public class TypeFromA { }
}
""";

var codeB = """
namespace Foo.Bar.Baz
{
public class TypeFromB
{
public Foo.Bar.TypeFromA GetA() => null;
}
}
""";

var compilationA = CompilationHelper.CreateCompilationFromCSharpCode(codeA, EmptyMSBuildProperties, "AssemblyA.dll");
var compilationB = CompilationHelper.CreateCompilationFromCSharpCode(codeB, EmptyMSBuildProperties, "AssemblyB.dll", compilationA.ToMetadataReference());

var allAssemblies = new HashSet<IAssemblySymbol>([compilationA.Assembly, compilationB.Assembly], SymbolEqualityComparer.Default);

var filter = new SymbolFilter(new(), new());

var outputA = compilationA.Assembly.Accept(new SymbolVisitorAdapter(compilationA, new(compilationA, MemberLayout.SamePage, allAssemblies), new(), filter, []));
var outputB = compilationB.Assembly.Accept(new SymbolVisitorAdapter(compilationB, new(compilationB, MemberLayout.SamePage, allAssemblies), new(), filter, []));

var allMembers = new Dictionary<string, MetadataItem>();
var allReferences = new Dictionary<string, ReferenceItem>();

foreach (var output in new[] { outputA, outputB })
{
foreach (var ns in output.Items ?? [])
{
allMembers.TryAdd(ns.Name, ns);
foreach (var type in ns.Items ?? [])
{
allMembers.TryAdd(type.Name, type);
}
}
if (output.References is not null)
{
foreach (var (key, value) in output.References)
{
if (allReferences.TryGetValue(key, out var existing))
{
existing.Merge(value);
}
else
{
allReferences[key] = value;
}
}
}
}

var model = YamlMetadataResolver.ResolveMetadata(allMembers, allReferences, NamespaceLayout.Flattened);

// Verify cross-assembly reference: TypeFromB references TypeFromA from another assembly
var typeBPage = model.Members.Single(m => m.Name == "Foo.Bar.Baz.TypeFromB");
var bazParts = typeBPage.References["Foo.Bar.Baz"].NameParts[SyntaxLanguage.CSharp];
Assert.Equal(["Foo", ".", "Bar", ".", "Baz"], bazParts.Select(p => p.DisplayName));
Assert.Null(bazParts.Single(p => p.DisplayName == "Foo").Href);
Assert.NotNull(bazParts.Single(p => p.DisplayName == "Bar").Href);
Assert.NotNull(bazParts.Single(p => p.DisplayName == "Baz").Href);

var barParts = typeBPage.References["Foo.Bar"].NameParts[SyntaxLanguage.CSharp];
Assert.Equal(["Foo", ".", "Bar"], barParts.Select(p => p.DisplayName));
Assert.Null(barParts.Single(p => p.DisplayName == "Foo").Href);
Assert.NotNull(barParts.Single(p => p.DisplayName == "Bar").Href);

// Same-assembly reference
var typeAPage = model.Members.Single(m => m.Name == "Foo.Bar.TypeFromA");
var barPartsA = typeAPage.References["Foo.Bar"].NameParts[SyntaxLanguage.CSharp];
Assert.Null(barPartsA.Single(p => p.DisplayName == "Foo").Href);
Assert.NotNull(barPartsA.Single(p => p.DisplayName == "Bar").Href);
}
}