Skip to content

Add warnings for multiple root pages #1276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

agisilaos
Copy link

Fixes: #1170

Summary

This PR implements warnings when DocC documentation contains more than one root page, addressing unexpected input configurations that may lead to unexpected behaviors.

Expected User Experience:
When developers call docc convert directly with custom inputs that contain multiple root pages (either multiple main modules from symbol graphs or multiple @TechnologyRoot directives), DocC will now emit helpful warnings to alert them about the unexpected configuration.

Implementation Overview:

  • Added warning logic in DocumentationContext.swift to detect multiple main modules during symbol graph processing
  • Added warning logic to detect multiple @TechnologyRoot directives after article processing
  • Implemented proper diagnostic messages with source location association for TechnologyRoot warnings
  • Added automatic fix suggestions for TechnologyRoot warnings
  • Created comprehensive test coverage for both warning scenarios

Dependencies

No external dependencies. This is a self-contained enhancement to the existing DocC codebase.

Testing

Steps:

  1. Setup Instructions:

    • Check out the feature/multiple-root-page-warnings branch
    • Run swift test --filter DocumentationContext_RootPageTests to verify the new tests pass
  2. Testing Multiple TechnologyRoot Directives:

    • Create a .docc catalog with multiple articles containing @TechnologyRoot directives
    • Run docc convert on the catalog
    • Verify warnings are emitted for each extra @TechnologyRoot directive with source location and fix suggestions
  3. Testing Multiple Main Modules:

    • Create a .docc catalog with multiple symbol graph files for different modules
    • Run docc convert on the catalog
    • Verify a warning is emitted about multiple main modules

Test Content:
The test files in Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift provide examples of both scenarios:

  • testWarnsAboutMultipleTechnologyRootDirectives() - Tests multiple @TechnologyRoot directives
  • testWarnsAboutMultipleMainModules() - Tests multiple main modules from symbol graphs

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

Copy link
Contributor

@d-ronnqvist d-ronnqvist left a comment

Choose a reason for hiding this comment

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

Thanks for opening this PR.

The code generally looks good but there's a third case that's also worth warning about, because it's likely going to be the least intuitive to people;
if the documentation contains any symbols (has one module) but also has any @TechnologyRoot pages it results in the same unsupported setup with multiple roots of the documentation hierarchy. The Solution to suggest here is to remove each @TechnologyRoot so that those pages are treated as articles under the module.

Comment on lines +251 to +272
"symbols": [
{
"identifier": {
"precise": "ModuleA",
"interfaceLanguage": "swift"
},
"names": {
"title": "ModuleA",
"navigator": null,
"subHeading": null,
"prose": null
},
"pathComponents": ["ModuleA"],
"docComment": null,
"accessLevel": "public",
"kind": {
"identifier": "module",
"displayName": "Module"
},
"mixins": {}
}
],
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI: The module isn't a defined symbol in the file. Instead DocC creates the module symbol when it processes the rest of the symbol graph data.

The correct way do define a minimal (empty) symbol graph file would be "symbols": [],

Comment on lines +226 to +327
"metadata": {
"formatVersion": {
"major": 1,
"minor": 0,
"patch": 0
},
"generator": "unit-test"
},
"module": {
"name": "ModuleA",
"platform": {
"architecture": "x86_64",
"vendor": "apple",
"operatingSystem": {
"name": "macosx",
"minimumVersion": {
"major": 10,
"minor": 15
}
},
"environment": null
}
},
"symbols": [
{
"identifier": {
"precise": "ModuleA",
"interfaceLanguage": "swift"
},
"names": {
"title": "ModuleA",
"navigator": null,
"subHeading": null,
"prose": null
},
"pathComponents": ["ModuleA"],
"docComment": null,
"accessLevel": "public",
"kind": {
"identifier": "module",
"displayName": "Module"
},
"mixins": {}
}
],
"relationships": []
}
"""),

// Second module symbol graph
TextFile(name: "ModuleB.symbols.json", utf8Content: """
{
"metadata": {
"formatVersion": {
"major": 1,
"minor": 0,
"patch": 0
},
"generator": "unit-test"
},
"module": {
"name": "ModuleB",
"platform": {
"architecture": "x86_64",
"vendor": "apple",
"operatingSystem": {
"name": "macosx",
"minimumVersion": {
"major": 10,
"minor": 15
}
},
"environment": null
}
},
"symbols": [
{
"identifier": {
"precise": "ModuleB",
"interfaceLanguage": "swift"
},
"names": {
"title": "ModuleB",
"navigator": null,
"subHeading": null,
"prose": null
},
"pathComponents": ["ModuleB"],
"docComment": null,
"accessLevel": "public",
"kind": {
"identifier": "module",
"displayName": "Module"
},
"mixins": {}
}
],
"relationships": []
}
"""),
Copy link
Contributor

Choose a reason for hiding this comment

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

We have a makeSymbolGraph(moduleName:platform:symbols:relationships:) helper that mosts tests use. It makes the test significantly shorter and easier to read and helps with data correctness as well compared to spelling out the raw JSON string.

Suggested change
TextFile(name: "ModuleA.symbols.json", utf8Content: """
{
"metadata": {
"formatVersion": {
"major": 1,
"minor": 0,
"patch": 0
},
"generator": "unit-test"
},
"module": {
"name": "ModuleA",
"platform": {
"architecture": "x86_64",
"vendor": "apple",
"operatingSystem": {
"name": "macosx",
"minimumVersion": {
"major": 10,
"minor": 15
}
},
"environment": null
}
},
"symbols": [
{
"identifier": {
"precise": "ModuleA",
"interfaceLanguage": "swift"
},
"names": {
"title": "ModuleA",
"navigator": null,
"subHeading": null,
"prose": null
},
"pathComponents": ["ModuleA"],
"docComment": null,
"accessLevel": "public",
"kind": {
"identifier": "module",
"displayName": "Module"
},
"mixins": {}
}
],
"relationships": []
}
"""),
// Second module symbol graph
TextFile(name: "ModuleB.symbols.json", utf8Content: """
{
"metadata": {
"formatVersion": {
"major": 1,
"minor": 0,
"patch": 0
},
"generator": "unit-test"
},
"module": {
"name": "ModuleB",
"platform": {
"architecture": "x86_64",
"vendor": "apple",
"operatingSystem": {
"name": "macosx",
"minimumVersion": {
"major": 10,
"minor": 15
}
},
"environment": null
}
},
"symbols": [
{
"identifier": {
"precise": "ModuleB",
"interfaceLanguage": "swift"
},
"names": {
"title": "ModuleB",
"navigator": null,
"subHeading": null,
"prose": null
},
"pathComponents": ["ModuleB"],
"docComment": null,
"accessLevel": "public",
"kind": {
"identifier": "module",
"displayName": "Module"
},
"mixins": {}
}
],
"relationships": []
}
"""),
JSONFile(name: "ModuleA.symbols.json", content: makeSymbolGraph(moduleName: "ModuleA")),
JSONFile(name: "ModuleB.symbols.json", content: makeSymbolGraph(moduleName: "ModuleB")),

// Verify each warning has a solution to remove the TechnologyRoot directive
for problem in multipleRootsProblems {
XCTAssertEqual(problem.possibleSolutions.count, 1)
let solution = problem.possibleSolutions.first!
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: If this is nil it will trap and stop running the remainder of the tests. A more defensive solution would be to use XCTUnwrap to handle the nil value. This will gracefully fail the tests by reporting a test failure without interrupting other tests.

Suggested change
let solution = problem.possibleSolutions.first!
let solution = try XCTUnwrap(problem.possibleSolutions.first)

This is the third root page.
"""),

InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI (non-blocking): This Info.plist file hasn't been needed for some time. I understand that you looked at other tests in this file and followed what they were doing (which they likely did for historical reasons) but this test (and the other test) would work the same without this file because the test doesn't verify any of the information that the Info.plist file configures.

XCTAssertTrue(multipleModulesProblem.diagnostic.explanation?.contains("ModuleA, ModuleB") == true)

// Verify the warning doesn't have a source location since it's about the overall input structure
XCTAssertNil(multipleModulesProblem.diagnostic.source)
Copy link
Contributor

Choose a reason for hiding this comment

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

minor (non-blocking): We could refer to one of the symbol graph files here and use DiagnosticNote values to help the developer find the other symbol graph files so that this diagnostic becomes a bit more actionable.

See for example DocumentationContext.emitWarningsForSymbolsMatchedInMultipleDocumentationExtensions(with:)

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.

2 participants