Document Extensions to External Types Using DocC

Hi everyone, this is the updated proposal, based on my initial pitch and the discussion in this thread:

(Introduction and Motivation didn't change, but I still included them so it is a more concise read and also because the example is referenced later.)

Introduction

DocC does not include extensions to a type defined in an external module in the documentation catalogue, even though the extension and its contents are declared in the documented module.

Consider the following potential addition to the SlothCreator package:

/// A type that generates sloths.
public protocol SlothGenerator {
    /// Generates a sloth in the specified habitat.
    func generateSloth(in habitat: Habitat) throws -> Sloth
}

public extension Collection where Element == Habitat {
    /// Generates one ``Sloth`` per ``Habitat`` in the collection.
    ///
    /// - Note: Unfortunately, neither this comment nor the function itself is included in
    /// the documentation catalogue yet.
    func mapToSloth(using generator: SlothGenerator) throws -> [Sloth] {
        try self.map(generator.generateSloth(in:))
    }
}

/// A type that generates names for sloths.
public protocol NameGenerator {
    /// Generates a name for a sloth.
    ///
    /// - parameter seed: A value that influences randomness.
    func generateName(seed: Int) -> String
}

/// An array of strings conforms to ``NameGenerator``. Each time
/// ``generateName(seed:)`` is called, it returns the element identified
/// by the given seed.
extension Array: NameGenerator where Element == String {
    public func generateName(seed: Int) -> String {
        self[seed % self.count]
    }
}

Neither of the two extensions is added to the documentation catalogue. That is, both Collection/mapToSloth(using:) and Array/generateName(seed:) are not listed. Furthermore, they cannot be referenced using their identifiers and therefore it is also not possible to include them in the documentation catalogue using manual curation. Finally, Array (with Element == String) is also not listed among the Conforming Types of NameGenerator.

Motivation

Swift encourages us to use extensions on external types and therefore we should also be able to document such extensions accordingly. As I already mentioned, this capability has also been requested and discussed on the Swift forums before.

While there are possibly infinite use cases, I think that especially the growing ecosystem of SwiftUI packages could benefit hugely from this addition. A large part of their public API surface may be made up of view modifiers, which are usually exposed as function-extensions on SwiftUI's View type.

Proposed Solution

We assume a hosting environment which hosts all documentation catalogues relevant to our project at hostpath/MODULE_NAME. For example, the documentation page for SlothGenerator (in the SlothCreator module) would be located at hostpath/slothcreator/slothgenerator and the standard library's Array could be found at hostpath/swift/array.

New Documentation Pages

Extended modules are documented within the extending module's documentation catalogue, where all content that is added to an external module via extensions is prefixed with the extended module's name. That is, the base path for all external extended contents is hostpath/EXTENDING_MODULE_NAME/EXTENDED_MODULE_NAME. To be more precise, hostpath/EXTENDING_MODULE_NAME/EXTENDED_MODULE_NAME would host a page that lists all types belonging to EXTENDED_MODULE that were (publicly) extended in EXTENDING_MODULE. For each of these types there exists a page at hostpath/EXTENDING_MODULE_NAME/EXTENDED_MODULE_NAME/EXTENDED_TYPE_NAME, which lists all the members and e.g. default implementations added to this type in EXTENDING_MODULE. Of course, all of these also have their respective documentation pages at hostpath/EXTENDING_MODULE_NAME/EXTENDED_MODULE_NAME/EXTENDED_TYPE_NAME/MEMBER_NAME.

We'd get the following new pages for the SlothCreator example above:

  • hostpath/slothcreator/swift (extended module page)
  • hostpath/slothcreator/swift/collection (extended type page)
  • hostpath/slothcreator/swift/collection/maptosloth(using:) (added member page)
  • hostpath/slothcreator/swift/array (extended type page)
  • hostpath/slothcreator/swift/array/namegenerator-implementations (added default implementation page)
  • hostpath/slothcreator/swift/array/generatename(seed:) (added member page)

Page Contents

The following snippets describe the outline of the newly introduced pages with examples from the SlothCreator framework.

Extended Module Page
<!-- hostpath/slothcreator/swift -->

# ``Swift``

## Topics

### Extended Protocols

- ``Collection``

### Extended Structures

- ``Array``
Extended Type Page
<!-- hostpath/slothcreator/swift/array -->

Extended Structure
# ``Array``

[`Array`](hostpath/swift/array) was originally declared in the [Swift](hostpath/swift) Framework.

## Declaration

\```swift
extension Array
\```

## Topics

### Default Implementations

- [NameGenerator Implementations](hostpath/slothcreator/swift/array/namegenerator-implementations)

## Relationships

### Conforms To

- ``NameGenerator``

The Added Member Page and Added Default Implementation Page look and behave exactly as they do for normal (locally defined) symbols.

Modified Documentation Pages

This proposal also adds content to some existing pages:

Module Page

The module page is extended by a segment called "Extended Modules" listing all extended module pages.

<!-- hostpath/slothcreator -->

# Sloth Creator

## Topics

### Structures

...

### Protocols

...

### Extended Modules

- ``Swift``
- ...

Type Page

The structure of normal type pages is not altered, however, they may receive additional entries in their "Relationships" section, e.g. for NameGenerator:

<!-- hostpath/slothcreator/namegenerator -->

Protocol
# ``NameGenerator``

A type that generates names for sloths.

## Declaration

\```swift
protocol NameGenerator
\```

## Topics

### Instance Methods

- ``generateName(seed:)``

## Relationships

### Conforming Types

- ``Array``
    Conforms when `Element` is `String`.

Manual Curation

Manual curation is permitted via the usual methods, including the possibility to reference any of the newly introduced pages outside of their respective extended module path (e.g. outside of hostpath/slothcreator/swift).

Navigation Sidebar

The navigation sidebar essentially lists the same content as the module page. That is, the new section header (i.e. the one on the same level as e.g. "Structures") would be called "Extended Modules". This section only lists the extended module pages, not the extended symbol pages. This list's items can be expanded to reveal items for the extended type pages, which in turn can be expanded to reveal the member pages, just as with regular pages.

Resolving URL Collisions

There exists one possible collision with the proposed URL scheme for extended content: A module defines a type with the same name as one of its public dependencies (i.e. extended modules). This scenario should be very very rare. However, we still propose to resolve such conflicts by applying the standard disambiguation suffix strategy. The EXTENDED_MODULE_NAME would get the suffix -swift.module.extension.

For example if SlothCreator contained the following enum declaration:

public enum Swift {
    case foo
}

Then we would have urls hostpath/slothcreator/swift-swift.enum and hostpath/slothcreator/swift-swift.module.extension. These disambiguation suffixes remain in place even for members that could be referenced unambiguously without the suffixes. For example, we'd get hostpath/slothcreator/swift-swift.enum/foo and hostpath/slothcreator/swift-swift.module.extension/array/generatename(seed:).

Identifier Syntax

Identifiers used in DocC code are suffixes of the urls used above. References to any of the newly introduced pages (i.e. those that list content from extensions to external types) must always contain the EXTENDED_MODULE_NAME. I.e. even if there is no local type Array, just ``Array`` is no valid identifier for hostpath/slothcreator/swift/array. Instead, ``Swift/Array`` would be the correct syntax.

Once [SR-15431] Support DocC references to symbols defined in another module · Issue #208 · apple/swift-docc · GitHub is implemented, we always follow a local first strategy. I.e. if a module shadows a symbol, the simple EXTENDED_MODULE_NAME/SYMBOL_PATH identifier links to the local page of the extension (hostpath/EXTENDING_MODULE_NAME/EXTENDED_MODULE_NAME/SYMBOL_PATH), not to the original page in the extended module's documentation catalogue (hostpath/EXTENDED_MODULE_NAME/SYMBOL_PATH).

In order to reference shadowed symbols, one must use absolute identifiers. Absolute identifiers are basically the URL without the leading hostpath. That is, shadowed symbols can be referenced unambiguously using /EXTENDED_MODULE_NAME/SYMBOL_PATH (note the leading slash).

Firstly, note that absolute identifiers are always an option, i.e. one could also refer to SlothCreator's NameGenerator in the Sloth Creator documentation catalogue using ``/SlothCreator/NameGenerator``.

Secondly, relative identifiers can be used for referencing external symbols, if they are not shadowed. That is, ``Swift/Array/count`` is a valid identifier in the SlothCreator module, even though there is no local extension that defines a count on Swift.Array.

Finally, identifiers use the same disambiguation syntax as the URL does. This applies to both, relative and absolute identifiers. In the edge-case where the -swift.module.extension suffix is required, usage of a relative path that includes a colliding name, but does not feature the disambiguation suffix is not permitted even if it does not collide in its entirety. (Usually, this identifier could link to a symbol in another documentation catalogue.) I.e. ``Swift/Array/count`` cannot be used in the example from "Resolving URL Collisions" in order to link to the Array.count definition in the standard library. Instead the absolute identifier ``/Swift/Array/count`` must be used.

5 Likes