Skip to content

Conversation

anferbui
Copy link
Contributor

@anferbui anferbui commented Jul 18, 2025

Bug/issue #, if applicable: rdar://155521394

Summary

Currently DocC has some code in RenderIndex to be able to store whether a navigation item is beta or not, but the feature isn't supported yet:

// Currently Swift-DocC doesn't support marking a node as beta in the navigation index
// so we default to `false` here.
self.isBeta = false

This PR propagates beta information for both render nodes and external render nodes to the navigation index such that we can ultimately encode the information as part of the navigator (i.e. index.json file):

{
  "beta": true,
  "path": "/documentation/mykit/myclass",
  "title": "MyClass",
  "type": "class"
},

Same as in other parts of the codebase, an item is considered to be in beta if all of its platforms are in beta.

Navigator comparison (using swift-docc-render):

Before After
Screenshot 2025-07-18 at 10 23 50 Screenshot 2025-07-18 at 10 21 32

Dependencies

N/A

Testing

Previewed a custom bundle locally which contained both an external and local link which were both beta.

  • Verified that they showed the beta badge in the navigator when using swift-docc-render.
  • Verified that the resulting index.json looked as expected

Steps:

  1. Use example bundle: Example.docc.zip
  2. Set up a link resolver, e.g. using /path/to/swift-docc/bin/test-data-external-resolver and modifying the data as desired
  3. Run DOCC_LINK_RESOLVER_EXECUTABLE=/path/to/swift-docc/bin/test-data-external-resolver swift run docc preview --platform name=macOS,version=1.0.0,beta=true --platform name=watchOS,version=2.0.0,beta=true --platform name=tvOS,version=3.0.0,beta=true --platform name=iOS,version=4.0.0,beta=true --platform "name=Mac Catalyst,version=4.0.0,beta=true" --platform name=iPadOS,version=4.0.0,beta=true Example.docc
  4. Navigate to http://localhost:8080/documentation/mykit/myotherclass
  5. Verify that the index.json is showing the external reference as a node, and that the node has a property beta=true:
    {
      "beta": true,
      "path": "/documentation/mykit/myclass",
      "title": "MyClass",
      "type": "class"
    },
    [...]
    {
      "beta": true,
      "external": true,
      "path": "/resolved/path",
      "title": "Resolved Title",
      "type": "symbol"
   }

Checklist

Make sure you check off the following items. If they cannot be completed, provide a reason.

  • Added tests
  • Ran the ./bin/test script and it succeeded
  • Updated documentation if necessary

@anferbui
Copy link
Contributor Author

@swift-ci please test

@anferbui anferbui marked this pull request as ready for review July 18, 2025 09:49
@anferbui
Copy link
Contributor Author

@swift-ci please test

1 similar comment
@sofiaromorales
Copy link
Contributor

@swift-ci please test

Comment on lines +127 to +133
var isBeta: Bool {
guard let platforms, !platforms.isEmpty else {
return false
}

return platforms.allSatisfy { $0.isBeta == true }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code it's repeated in LinkDestinationSummary ResolvedInformation and now here. Might be better to unify these so it does not become a maintenance nightmare

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have an idea for how we can do this, by defining this extension:

extension Array where Array.Element == AvailabilityRenderItem {
    var isBeta: Bool {
        guard !self.isEmpty else {
            return false
        }
        
        return self.allSatisfy { $0.isBeta == true }
    }
}

I will open a separate PR with this proposal so that we can review/discuss separately :)

Comment on lines 195 to 216
public static func == (lhs: NavigatorItem, rhs: NavigatorItem) -> Bool {
return lhs.pageType == rhs.pageType &&
lhs.languageID == rhs.languageID &&
lhs.title == rhs.title &&
lhs.platformMask == rhs.platformMask &&
lhs.availabilityID == rhs.availabilityID &&
lhs.isBeta == rhs.isBeta &&
lhs.isExternal == rhs.isExternal
}

// MARK: - Hashable
// Needed because a Swift class's synthesized Hashable conformance doesn't take into account properties which have default values as part of the designated initializer.

public func hash(into hasher: inout Hasher) {
hasher.combine(pageType)
hasher.combine(languageID)
hasher.combine(title)
hasher.combine(platformMask)
hasher.combine(availabilityID)
hasher.combine(isBeta)
hasher.combine(isExternal)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true? If so, that feels worthy of a bug on the compiler that we should reference in these comments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anferbui this code does not seem to be needed. I commented it out and all test pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these are needed, removed this logic from 7b266fa now


length = MemoryLayout<UInt8>.stride
// To ensure backwards compatibility, handle both when `isBeta` has been encoded and when it hasn't
if cursor < data.count {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check that cursor + length fits within the data? Currently, it's still possible that this code would index out of bounds. (same below)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to a few lines below I added an assertion to catch if this happens. d3e651d

}

// To ensure backwards compatibility, handle both when `isExternal` has been encoded and when it hasn't
if cursor < data.count {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these two ever be read/written individually or should they be checked together inside a single if-statement?
If they can be read/written individually, how is it possible to know which boolean a single value represents?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The have to either be both present or none. I made the change here d3e651d

XCTAssertEqual(item, fromData)
}

func testNavigatorItemRawDumpBackwardCompatibility() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice test 👍

@d-ronnqvist
Copy link
Contributor

minor: Some of the modified files should also update their license comments. After merging main you can use the utility in bin/update-license-comments/ to help with that.

sofiaromorales added a commit to anferbui/swift-docc that referenced this pull request Sep 11, 2025
…ew values.

Added an assertion that checks that the cursor plus the lenght of the memory
to be decoded is less or equal than the length of the entire memory for the
raw value that has been passed, avoiding a runtime crash.

Also both if statements were combined into a signgle one since, as pointed out
in [1], `isBeta` and `isExternal` have to either both exists or not.

---

[1] swiftlang#1249 (comment)
anferbui and others added 7 commits September 15, 2025 10:16
Adds a computed property to `NavigatorIndexableRenderMetadataRepresentation` which derives whether the navigator item `isBeta` or not. This uses the same logic used in other places in the codebase [1].

Fixes rdar://155521394.

[1]: https://github.com/swiftlang/swift-docc/blob/f968935b770b0011d7aa28f59eda22a8407282b7/Sources/SwiftDocC/Infrastructure/External%20Data/OutOfProcessReferenceResolver.swift#L592-L598
Propagates the `isBeta` property to the `NavigatorItem` type from `NavigatorIndexableRenderMetadataRepresentation`.  When we index a new node, whether the item is beta or not will now be captured as part of the navigator

This is preparatory work before this property is propagated to the `RenderIndex` [1].

Fixes rdar://155521394.

[1]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Indexing/RenderIndexJSON/RenderIndex.swift#L257-L259
The isBeta and isExternal properties weren't being serialised as part of `NavigatorItem`.

These properties are used to initialise to `RenderIndex.Node` during the conversion to `index.json` [1] and must be preserved when navigator indexes are written to disk [2] so that when they are read [3], we don't drop beta and external information.

This serialisation happens during the `finalize` step [4] and the deserialisation can be invoked via `NavigatorIndex.readNavigatorIndex` [5].

Otherwise, the values can be lost on serialisation roundtrip.

[1]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Indexing/RenderIndexJSON/RenderIndex.swift#L329
[2]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift#L195
[3]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Indexing/Navigator/NavigatorTree.swift#L266
[4]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift#L1193
[5]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift#L157
All that was left was that on initialisation of a `RenderIndex.Node`, we propagate the `isBeta` value from the `NavigatorItem`.

With this change, beta information is now available in the navigator, encoded as `"beta": true` in the resulting `index.json` file.

Fixes rdar://155521394.
…ew values.

Added an assertion that checks that the cursor plus the lenght of the memory
to be decoded is less or equal than the length of the entire memory for the
raw value that has been passed, avoiding a runtime crash.

Also both if statements were combined into a signgle one since, as pointed out
in [1], `isBeta` and `isExternal` have to either both exists or not.

---

[1] swiftlang#1249 (comment)
@anferbui anferbui force-pushed the beta-status-in-navigator branch from ee7ee0a to 0a6fbba Compare September 15, 2025 09:22
Copy link
Contributor

@sofiaromorales sofiaromorales left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

@anferbui
Copy link
Contributor Author

@swift-ci please test

@anferbui anferbui merged commit 1c54e10 into swiftlang:main Sep 15, 2025
2 checks passed
@anferbui anferbui deleted the beta-status-in-navigator branch September 15, 2025 09:40
@anferbui anferbui mentioned this pull request Sep 15, 2025
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants