Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 10 additions & 0 deletions docs/design/api-mark-dot-net/cref-target-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ a member declared in the indexed assembly.
- *Returns*: `true` when a member with this exact identifier was indexed;
otherwise `false`.

**CrefTargetResolver.DeclaresCrefType** (internal): Reports whether a cref names a type of the indexed
assembly, or a member (existing or not) whose declaring type is one.

- *Parameters*: `string crefId` — the raw `cref` attribute value, including its kind prefix.
- *Algorithm*: Strips the kind prefix and any parameter list, then tests the whole name as a `T:` key in
`_types` and progressively shorter dot-separated prefixes as the declaring type of a member.
- *Returns*: `true` when a prefix matches an indexed type; `false` otherwise, including for malformed input.
- *Purpose*: Lets `XmlDocReader` keep a cref of the documented assembly out of any external link mapping
even when it is filtered out or not indexed.

### Error Handling

CrefTargetResolver does not throw. An unresolvable `crefId` (external
Expand Down
2 changes: 1 addition & 1 deletion docs/design/api-mark-dot-net/dot-net-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ memory and returns a `DotNetEmitter` ready to emit.
filtered) never becomes a dictionary key and therefore never appears in any
generated index or page — the same mechanism that already applies to
obsolete-only namespaces when `IncludeObsolete` is false.
- *TypeLinkResolver scope*: `TypeLinkResolver` is constructed with `rootNamespaces` only (not all namespaces), plus `generateLinks: true` and the external link targets, which constrains which type references can produce intra-assembly Markdown links; types whose namespace does not map to a known root namespace path fall back to plain text.
- *TypeLinkResolver scope*: `TypeLinkResolver` is constructed with `rootNamespaces` only (not all namespaces), plus `generateLinks: true`, the external link targets, and `documentedModule: assembly.MainModule` (the identity of the module being documented, so that types of other modules are never treated as intra-assembly), which constrains which type references can produce intra-assembly Markdown links; types whose namespace does not map to a known root namespace path fall back to plain text.
- *`ComputeRootNamespaces`*: shared helper reducing a namespace list to its root namespaces; used for both the documented assembly and each mapped external assembly. The empty (global) namespace is never treated as a prefix, so it is always a root and never hides other roots.
- *`BuildExternalLinkTargets`*: validates each `ExternalLinks` entry (`IsValidExternalLinkPath`), locates the DLL via `FindReferenceAssembly`, reads its namespaces with Mono.Cecil, and returns `ExternalLinkTarget` values keyed by assembly name case-insensitively; failures produce warnings and skipped mappings.
- *`FindReferenceAssembly`*: resolves each reference path with `Path.GetFullPath` and `PathHelpers.NormalizeCase`, accepting a path naming the DLL, a file beside it, or a directory containing it; blank paths are skipped.
Expand Down
19 changes: 15 additions & 4 deletions docs/design/api-mark-dot-net/type-link-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ parameter.
referenced assembly's simple name to the location of its generated documentation.
Always a case-insensitive (`OrdinalIgnoreCase`) copy of the caller-supplied
dictionary, regardless of the comparer the caller's dictionary uses.
- *_documentedModule* (private `ModuleDefinition?`): The module of the assembly being documented, or
`null` when unknown. A base type resolved from another assembly carries that assembly's own
`ModuleDefinition` as its scope, so a module scope alone does not prove a type belongs to the documented
assembly; this identity is what distinguishes the two.
- *PrimitiveFullNames* (private static `HashSet<string>`): Full CLR names of
C# primitive types that are always rendered as their keyword alias and never
tracked as external dependencies.
Expand Down Expand Up @@ -83,7 +87,11 @@ reference collected during table cell generation.
- `IReadOnlyDictionary<string, ExternalLinkTarget>? externalLinks = null` —
map from referenced assembly name to documentation location. Copied into a
case-insensitive dictionary.
- *Postconditions*: The resolver is ready to call `Linkify`.
- `ModuleDefinition? documentedModule = null` — the module of the assembly being
documented. When supplied, only types scoped to exactly this module are intra-assembly;
a type scoped to any other module is external. When `null`, any `ModuleDefinition` scope
is intra-assembly (the previous behavior).
- *Postcondition*: The resolver is ready to call `Linkify`.

**TypeLinkResolver.Linkify**: Resolves a `TypeReference` to a Markdown string.

Expand Down Expand Up @@ -130,15 +138,18 @@ and generic arguments. This is the overload the emitters call.
angle-bracketed list and any `?` are appended.
8. Primitive or `System.Nullable<>` open type → `TypeNameSimplifier.Simplify`,
append `?` if annotated.
9. Intra-assembly (scope is `ModuleDefinition`) → relative Markdown link when
9. Intra-assembly (scope is the documented `ModuleDefinition`) → relative Markdown link when
`_generateLinks` is `true`; plain-text type name when `false`.
10. Non-System external → `TryLinkExternalType`: when the type's assembly is mapped
and links are enabled, return a link into that documentation and do not track
the type; otherwise track it in `externalTypes` and return the plain name.
- *Intra-assembly detection*: `TypeReference.Scope is ModuleDefinition`.
- *Intra-assembly detection*: `TypeReference.Scope is ModuleDefinition` and, when a documented module was
supplied, that module is the documented module. A type scoped to another module (for example a base type of
a referenced assembly, resolved from that assembly) is external, never a page of the documented tree.

**TypeLinkResolver.TryLinkExternalType** (private): Builds a link for a type whose
scope is an `AssemblyNameReference` present in `_externalLinks`. Returns `null` when
scope is an `AssemblyNameReference`, or the `ModuleDefinition` of another assembly (its
owning assembly's name is used), present in `_externalLinks`. Returns `null` when
links are disabled or the assembly is not mapped. See External Assembly Links.

**TypeLinkResolver.BuildTypePageKey** (private static; type file names come from `DotNetEmitter.GetTypeFileName`, which reads the approved names from the symbol-name registry): Computes a type's page key
Expand Down
5 changes: 4 additions & 1 deletion docs/design/api-mark-dot-net/xml-doc-reader.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,10 @@ link's label; otherwise returns the code-span text unchanged.
`GetEffectiveSummary` and `GetEffectiveSummaryMarkdown` return "Inherits documentation from base member."
when the summary is empty and `HasInheritDoc` is true; `GetSummary` itself is unchanged. An external-assembly
`cref` that is not resolved locally falls back to `TypeLinkResolver.TryLinkifyExternalCref` before the
plain code span.
plain code span. A `cref` that `CrefTargetResolver.DeclaresCrefType` reports as belonging to the documented
assembly (a type of it, or a member of one of its types, whether or not that member is indexed or emitted) is
never passed to `TryLinkifyExternalCref`: it stays a plain code span, because a mapped assembly's namespaces
may be a prefix of the documented assembly's own.

### Error Handling

Expand Down
12 changes: 12 additions & 0 deletions docs/reqstream/api-mark-dot-net/cref-target-resolver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,15 @@ sections:
- CrefTargetResolver_TryResolveType_ExternalAssemblyType_ReturnsFalse
- CrefTargetResolver_TryResolveMember_UnknownIdentifier_ReturnsFalse
- CrefTargetResolver_TryResolveType_MalformedCref_ReturnsFalse
- id: ApiMarkDotNet-CrefTargetResolver-DeclareOwnAssemblyCrefTypes
title: CrefTargetResolver shall report, through DeclaresCrefType, whether a cref
names a type of the indexed assembly or a member (existing or not) whose
declaring type is a type of the indexed assembly.
justification: |
A cref that belongs to the documented assembly but is not linked locally (filtered
out, or a member the index does not hold) must never be routed to an external
documentation mapping whose namespaces happen to be a prefix of the documented
assembly's own namespaces. XmlDocReader uses this answer to keep such a cref a
plain code span.
tests:
- DotNetEmitterGradualDisclosure_Emit_OwnAssemblyCref_NotEmitted_NeverLinksExternally
14 changes: 14 additions & 0 deletions docs/reqstream/api-mark-dot-net/type-link-resolver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,17 @@ sections:
- DotNetEmitterGradualDisclosure_Emit_ExternalNestedTypeCrefs_LinkToNestedPages
- DotNetEmitterGradualDisclosure_Emit_ExternalNestedPropertyTypes_LinkToNestedPages
- DotNetEmitterGradualDisclosure_Emit_ExternalLinks_MatchTargetAssemblyRegistryNames
- id: ApiMarkDotNet-TypeLinkResolver-ForeignModuleTypesAreExternal
title: TypeLinkResolver shall, when given the documented module, treat only types
scoped to exactly that module as intra-assembly, and shall treat a type scoped to
any other module (such as a base type resolved from another assembly) as external,
linking it into the mapped documentation or rendering it as plain text.
justification: |
Resolving a base type from a referenced assembly yields a type whose scope is that
assembly's own module, which is indistinguishable by scope kind from a type of the
documented assembly. Without comparing the module identity such a type would be
rendered as a link to a page that does not exist in the documented tree.
tests:
- DotNetEmitterGradualDisclosure_Emit_ExternalBaseChain_Unmapped_RendersPlainTextHeadings
- DotNetEmitterGradualDisclosure_Emit_ExternalBaseChain_Mapped_LinksEveryHeadingIntoExternalDocs
- DotNetEmitterGradualDisclosure_Emit_WithReferences_AllRelativeLinksResolveToEmittedPages
1 change: 1 addition & 0 deletions docs/reqstream/api-mark-dot-net/xml-doc-reader.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ sections:
- XmlDocReader_GetSummary_ExplicitLabelUnresolvedCref_RendersLabelOnlyNoLink
- XmlDocReader_GetSummary_ExplicitLabelConstructorCref_RendersLabelOnlyNoLink
- CrefLinking_SeeCrefToVisibleType_RendersAsMarkdownLink
- DotNetEmitterGradualDisclosure_Emit_OwnAssemblyCref_NotEmitted_NeverLinksExternally
- CrefLinking_SeeCrefToVisibleMember_RendersAsMarkdownLink
- CrefLinking_SeeCrefToFilteredMember_RendersFallbackWithNoLink
- CrefLinking_SeeCrefToExternalType_RendersFallbackWithNoLink
Expand Down
3 changes: 3 additions & 0 deletions docs/verification/api-mark-dot-net/cref-target-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ or writable output location is needed.
- A field's XML-doc identifier resolves via `TryResolveMember`.
- An indexer's XML-doc identifier includes its index parameter type list and resolves via `TryResolveMember`.
- A generic method's own type parameter used as an array element type (e.g. `T[]`) resolves to XML-doc positional notation, not the raw source name.
- `DeclaresCrefType` reports a cref of a type, or of a member (existing or not) of a type, of the indexed assembly, so such a cref is never routed to an external link mapping.

### Test Scenarios

Expand Down Expand Up @@ -112,3 +113,5 @@ type rather than falling back to the raw Cecil source name `T[]` — matching
real compiler-emitted XML doc IDs, and resolves back to the method via
`TryResolveMember`. This scenario is tested by
`CrefTargetResolver_TryResolveMember_GenericMethodWithArrayParameter_ResolvesPositionalNotation`.

**Own-assembly crefs never link externally**: Arrange the fixture assembly mapped onto itself in `ExternalLinks` (forcing a namespace-prefix overlap) and a cref to a filtered-out private member. Expected: the cref stays a plain code span and no external link is emitted. Tested by `DotNetEmitterGradualDisclosure_Emit_OwnAssemblyCref_NotEmitted_NeverLinksExternally`.
3 changes: 3 additions & 0 deletions docs/verification/api-mark-dot-net/type-link-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ output location is needed.
- A type from an unmapped assembly, or any external type when `generateLinks` is `false`, stays plain text and is tracked as external.
- Nullable flags are applied per position in pre-order (generic arguments, array elements); value types never receive `?`; exhausted flags fall back to the default.
- A by-reference type resolves to its element type without a trailing `&` and consumes no nullable flag.
- A type scoped to a module other than the documented module (a base type of a referenced assembly) is treated as external: plain text and tracked when unmapped, linked into the mapped documentation when mapped, and never rendered as a link into the documented tree.

### Test Scenarios

Expand Down Expand Up @@ -313,3 +314,5 @@ and `NestedGenericHost<T>.Inner<U>` link to `.../NestedHost/Inner.md`, `.../Nest
`DotNetEmitterGradualDisclosure_Emit_ExternalNestedPropertyTypes_LinkToNestedPages`. External link names equal
the names the target assembly's own registry produces; tested by
`DotNetEmitterGradualDisclosure_Emit_ExternalLinks_MatchTargetAssemblyRegistryNames`.

**Foreign-module types are external**: Arrange `InheritedFromExternalChain`, whose base `ExternalChainMiddle` derives from `ExternalChainTop` in the same external assembly, and emit with reference paths, first unmapped and then with `ExternalLinks` mapping the external fixture assembly. Expected: unmapped, both inherited headings are plain text; mapped, both link into the external documentation; and every relative link on every page resolves to an emitted page. Tested by `DotNetEmitterGradualDisclosure_Emit_ExternalBaseChain_Unmapped_RendersPlainTextHeadings`, `DotNetEmitterGradualDisclosure_Emit_ExternalBaseChain_Mapped_LinksEveryHeadingIntoExternalDocs`, and `DotNetEmitterGradualDisclosure_Emit_WithReferences_AllRelativeLinksResolveToEmittedPages`.
1 change: 1 addition & 0 deletions docs/verification/api-mark-dot-net/xml-doc-reader.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ assembly is needed.
- A `<see cref>` to a constructor falls back to its existing unwrapped-text rendering even with a non-null `linkContext` — constructors are never linked.
- A `<seealso cref>` to a resolved, emitted intra-assembly type (nested inside `<remarks>`) produces a code span nested inside a Markdown link, confirming `<seealso>` shares the same `TryLinkifyCref` rendering path as `<see>`.
- A `<see cref>` carrying an explicit display label (e.g. `<see cref="...">custom label</see>`) that resolves to an emitted intra-assembly type is rendered with that label wrapped in a Markdown link, instead of being left unlinked merely because it has a custom label.
- A `<see cref>` into the documented assembly that is not linked locally (filtered out) stays a plain code span even when an external mapping's namespaces overlap the documented assembly's; tested by `DotNetEmitterGradualDisclosure_Emit_OwnAssemblyCref_NotEmitted_NeverLinksExternally`.
- A `<see cref>` with an explicit display label and no `linkContext`, or whose `cref` does not resolve, falls back to the plain label text with no link, preserving the existing fallback exactly.
- A `<see cref>` carrying an explicit display label that targets a constructor is never linked, even when the constructor's declaring type is a resolvable, emitted intra-assembly type — constructors are never linked regardless of whether the reference has an explicit label.

Expand Down
39 changes: 39 additions & 0 deletions src/ApiMark.DotNet/CrefTargetResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,45 @@ internal bool TryResolveType(string crefId, out TypeDefinition type) =>
internal bool TryResolveMember(string crefId, out IMemberDefinition member) =>
_members.TryGetValue(crefId, out member!);

/// <summary>
/// Returns <see langword="true"/> when <paramref name="crefId"/> names a type of the indexed
/// assembly, or a member (existing or not) whose declaring type is a type of the indexed assembly.
/// </summary>
/// <remarks>
/// Used to keep a cref that belongs to the documented assembly out of any external link mapping,
/// whatever the reason it is not linked locally (filtered out, or a member the index does not hold).
/// </remarks>
/// <param name="crefId">The raw <c>cref</c> attribute value, including its kind prefix.</param>
/// <returns><see langword="true"/> when the cref's type is declared in the indexed assembly.</returns>
internal bool DeclaresCrefType(string crefId)
{
if (crefId.Length < 3 || crefId[1] != ':')
{
return false;
}

var name = crefId.Substring(2);
var parenthesis = name.IndexOf('(', StringComparison.Ordinal);
if (parenthesis >= 0)
{
name = name.Substring(0, parenthesis);
}

// Try the whole name as a type, then progressively shorter prefixes as the declaring type of a member
var end = name.Length;
while (end > 0)
{
if (_types.ContainsKey("T:" + name.Substring(0, end)))
{
return true;
}

end = name.LastIndexOf('.', end - 1);
}

return false;
}

/// <summary>
/// Enumerates every method, property, field, and event declared directly on
/// <paramref name="type"/>, for indexing purposes only (no visibility filtering).
Expand Down
3 changes: 2 additions & 1 deletion src/ApiMark.DotNet/DotNetGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ public IApiEmitter Parse(IContext context)
var resolver = new TypeLinkResolver(
rootNamespaces,
generateLinks: true,
externalLinks: BuildExternalLinkTargets(context));
externalLinks: BuildExternalLinkTargets(context),
documentedModule: assembly.MainModule);

// Cache the parsed assembly and XML doc index so CheckDocumentationCoverage can be
// called after Parse returns and before Emit disposes the assembly.
Expand Down
Loading
Loading