Skip to content

Fix "missing technology root" warning for tutorials #812

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

Merged
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,29 @@ extension DefaultDiagnosticConsoleFormatter {

guard let url = diagnostic.source
else { return "" }

guard let diagnosticRange = diagnostic.range
else { return "\n--> \(formattedSourcePath(url))" }
else {
// If the replacement operation involves adding new files,
// emit the file content as an addition instead of a replacement.
//
// Example:
// --> /path/to/new/file.md
// Summary
// suggestion:
// 0 + Addition file and
// 1 + multiline file content.
var addition = ""
solutions.forEach { solution in
addition.append("\n" + solution.summary)
solution.replacements.forEach { replacement in
let solutionFragments = replacement.replacement.split(separator: "\n")
addition += "\nsuggestion:\n" + solutionFragments.enumerated().map {
"\($0.offset) + \($0.element)"
}.joined(separator: "\n")
}
}
return "\n--> \(formattedSourcePath(url))\(addition)"
}

let sourceLines = readSourceLines(url)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,15 +482,42 @@ public struct ConvertAction: Action, RecreatingContext {
}

var didEncounterError = analysisProblems.containsErrors || conversionProblems.containsErrors
if try context.renderRootModules.isEmpty {
let hasTutorial = context.knownPages.contains(where: {
guard let kind = try? context.entity(with: $0).kind else { return false }
return kind == .tutorial || kind == .tutorialArticle
})
// Warn the user if the catalog is a tutorial but does not contains a table of contents
// and provide template content to fix this problem.
if (
context.rootTechnologies.isEmpty &&
hasTutorial
) {
let tableOfContentsFilename = CatalogTemplateKind.tutorialTopLevelFilename
let source = rootURL?.appendingPathComponent(tableOfContentsFilename)
var replacements = [Replacement]()
if let tableOfContentsTemplate = CatalogTemplateKind.tutorialTemplateFiles(converter.firstAvailableBundle()?.displayName ?? "Tutorial Name")[tableOfContentsFilename] {
replacements.append(
Replacement(
range: .init(line: 1, column: 1, source: source) ..< .init(line: 1, column: 1, source: source),
replacement: tableOfContentsTemplate
)
)
}
postConversionProblems.append(
Problem(
diagnostic: Diagnostic(
source: source,
severity: .warning,
identifier: "org.swift.docc.MissingTechnologyRoot",
summary: "No TechnologyRoot to organize article-only documentation.",
explanation: "Article-only documentation needs a TechnologyRoot page (indicated by a `TechnologyRoot` directive within a `Metadata` directive) to define the root of the documentation hierarchy."
)
identifier: "org.swift.docc.MissingTableOfContents",
summary: "Missing tutorial table of contents page.",
explanation: "`@Tutorial` and `@Article` pages require a `@Tutorials` table of content page to define the documentation hierarchy."
),
possibleSolutions: [
Solution(
summary: "Create a `@Tutorials` table of content page.",
replacements: replacements
)
]
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ extension CatalogTemplateKind {
}

/// Content of the 'tutorial' template
static var tutorialTopLevelFilename: String { "table-of-contents.tutorial" }
static func tutorialTemplateFiles(_ title: String) -> [String: String] {
[
"table-of-contents.tutorial": """
tutorialTopLevelFilename: """
@Tutorials(name: "\(title)") {
@Intro(title: "Tutorial Introduction") {
Add one or more paragraphs that introduce your tutorial.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,4 +389,105 @@ class DiagnosticConsoleWriterDefaultFormattingTest: XCTestCase {
""")
}
}

func testEmitAdditionReplacementSolution() throws {
func problemsLoggerOutput(possibleSolutions: [Solution]) -> String {
let logger = Logger()
let consumer = DiagnosticConsoleWriter(logger, highlight: true)
let problem = Problem(diagnostic: Diagnostic(source: URL(fileURLWithPath: "/path/to/file.md"), severity: .warning, range: nil, identifier: "org.swift.docc.tests", summary: "Test diagnostic"), possibleSolutions: possibleSolutions)
consumer.receive([problem])
try? consumer.flush()
return logger.output
}
let sourcelocation = SourceLocation(line: 1, column: 1, source: nil)
let range = sourcelocation..<sourcelocation
XCTAssertEqual(
problemsLoggerOutput(possibleSolutions: [
Solution(summary: "Create a sloth.", replacements: [
Replacement(
range: range,
replacement: """
var slothName = "slothy"
var slothDiet = .vegetarian
"""
)
])
]),
"""
\u{1B}[1;33mwarning: Test diagnostic\u{1B}[0;0m
--> /path/to/file.md
Create a sloth.
suggestion:
0 + var slothName = \"slothy\"
1 + var slothDiet = .vegetarian
"""
)

XCTAssertEqual(
problemsLoggerOutput(possibleSolutions: [
Solution(summary: "Create a sloth.", replacements: [
Replacement(
range: range,
replacement: """
var slothName = "slothy"
var slothDiet = .vegetarian
"""
),
Replacement(
range: range,
replacement: """
var slothName = SlothGenerator().generateName()
var slothDiet = SlothGenerator().generateDiet()
"""
)
])
]),
"""
\u{1B}[1;33mwarning: Test diagnostic\u{1B}[0;0m
--> /path/to/file.md
Create a sloth.
suggestion:
0 + var slothName = "slothy"
1 + var slothDiet = .vegetarian
suggestion:
0 + var slothName = SlothGenerator().generateName()
1 + var slothDiet = SlothGenerator().generateDiet()
"""
)

XCTAssertEqual(
problemsLoggerOutput(possibleSolutions: [
Solution(summary: "Create a sloth.", replacements: [
Replacement(
range: range,
replacement: """
var slothName = "slothy"
var slothDiet = .vegetarian
"""
),
]),
Solution(summary: "Create a bee.", replacements: [
Replacement(
range: range,
replacement: """
var beeName = "Bee"
var beeDiet = .vegetarian
"""
)
])
]),
"""
\u{1B}[1;33mwarning: Test diagnostic\u{1B}[0;0m
--> /path/to/file.md
Create a sloth.
suggestion:
0 + var slothName = "slothy"
1 + var slothDiet = .vegetarian
Create a bee.
suggestion:
0 + var beeName = "Bee"
1 + var beeDiet = .vegetarian
"""
)
}
}
105 changes: 75 additions & 30 deletions Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3057,31 +3057,79 @@ class ConvertActionTests: XCTestCase {

// Tests that when converting a catalog with no technology root a warning is raised (r93371988)
func testConvertWithNoTechnologyRoot() throws {
let bundle = Folder(name: "unit-test.docc", content: [
func problemsFromConverting(_ catalogContent: [File]) throws -> [Problem] {
let catalog = Folder(name: "unit-test.docc", content: catalogContent)
let testDataProvider = try TestFileSystem(folders: [catalog, Folder.emptyHTMLTemplateDirectory])
let engine = DiagnosticEngine()
var action = try ConvertAction(
documentationBundleURL: catalog.absoluteURL,
outOfProcessResolver: nil,
analyze: false,
targetDirectory: URL(fileURLWithPath: "/output"),
htmlTemplateDirectory: Folder.emptyHTMLTemplateDirectory.absoluteURL,
emitDigest: false,
currentPlatforms: nil,
dataProvider: testDataProvider,
fileManager: testDataProvider,
temporaryDirectory: URL(fileURLWithPath: "/tmp"),
diagnosticEngine: engine
)
_ = try action.perform(logHandle: .none)
return engine.problems
}

let onlyTutorialArticleProblems = try problemsFromConverting([
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
TextFile(name: "Documentation.md", utf8Content: "")
TextFile(name: "Article.tutorial", utf8Content: """
@Article(time: 20) {
@Intro(title: "Slothy Tutorials") {
This is an abstract for the intro.
}
}
"""
),
])
let testDataProvider = try TestFileSystem(folders: [bundle, Folder.emptyHTMLTemplateDirectory])
let targetDirectory = URL(fileURLWithPath: testDataProvider.currentDirectoryPath)
.appendingPathComponent("target", isDirectory: true)
let engine = DiagnosticEngine()
var action = try ConvertAction(
documentationBundleURL: bundle.absoluteURL,
outOfProcessResolver: nil,
analyze: true,
targetDirectory: targetDirectory,
htmlTemplateDirectory: Folder.emptyHTMLTemplateDirectory.absoluteURL,
emitDigest: false,
currentPlatforms: nil,
dataProvider: testDataProvider,
fileManager: testDataProvider,
temporaryDirectory: createTemporaryDirectory(),
diagnosticEngine: engine
)
let _ = try action.perform(logHandle: .none)
XCTAssertEqual(engine.problems.count, 1)
XCTAssertEqual(engine.problems.map { $0.diagnostic.identifier }, ["org.swift.docc.MissingTechnologyRoot"])
XCTAssert(engine.problems.contains(where: { $0.diagnostic.severity == .warning }))
XCTAssert(onlyTutorialArticleProblems.contains(where: {
$0.diagnostic.identifier == "org.swift.docc.MissingTableOfContents"
}))

let tutorialTableOfContentProblem = try problemsFromConverting([
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
TextFile(name: "table-of-contents.tutorial", utf8Content: """
"""
),
TextFile(name: "article.tutorial", utf8Content: """
@Article(time: 20) {
@Intro(title: "Slothy Tutorials") {
This is an abstract for the intro.
}
}
"""
),
])
XCTAssert(tutorialTableOfContentProblem.contains(where: {
$0.diagnostic.identifier == "org.swift.docc.MissingTableOfContents"
}))

let incompleteTutorialFile = try problemsFromConverting([
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
TextFile(name: "article.tutorial", utf8Content: """
@Chapter(name: "SlothCreator Essentials") {
@Image(source: "chapter1-slothcreatorEssentials.png", alt: "A wireframe of an app interface that has an outline of a sloth and four buttons below the sloth. The buttons display the following symbols, from left to right: snowflake, fire, wind, and lightning.")

Create custom sloths and edit their attributes and powers using SlothCreator.

@TutorialReference(tutorial: "doc:Creating-Custom-Sloths")
}
"""
),
])
XCTAssert(incompleteTutorialFile.contains(where: {
$0.diagnostic.identifier == "org.swift.docc.missingTopLevelChild"
}))
XCTAssertFalse(incompleteTutorialFile.contains(where: {
$0.diagnostic.identifier == "org.swift.docc.MissingTableOfContents"
}))
}

func testWrittenDiagnosticsAfterConvert() throws {
Expand Down Expand Up @@ -3120,22 +3168,19 @@ class ConvertActionTests: XCTestCase {
)

let _ = try action.perform(logHandle: .none)
XCTAssertEqual(engine.problems.count, 2)
XCTAssertEqual(engine.problems.count, 1)

XCTAssert(FileManager.default.fileExists(atPath: diagnosticFile.path))

let diagnosticFileContent = try JSONDecoder().decode(DiagnosticFile.self, from: Data(contentsOf: diagnosticFile))
XCTAssertEqual(diagnosticFileContent.diagnostics.count, 2)
XCTAssertEqual(diagnosticFileContent.diagnostics.count, 1)

XCTAssertEqual(diagnosticFileContent.diagnostics.map(\.summary).sorted(), [
"No TechnologyRoot to organize article-only documentation.",
"No symbol matched 'ModuleThatDoesNotExist'. Can't resolve 'ModuleThatDoesNotExist'."
])
].sorted())

let logLines = logStorage.text.splitByNewlines
XCTAssertEqual(logLines.filter { ($0 as NSString).contains("warning:") }.count, 2, "There should be two warnings printed to the console")
XCTAssertEqual(logLines.filter { ($0 as NSString).contains("No TechnologyRoot to organize article-only documentation.") }.count, 1, "The root page warning shouldn't be repeated.")
XCTAssertEqual(logLines.filter { ($0 as NSString).contains("No symbol matched 'ModuleThatDoesNotExist'. Can't resolve 'ModuleThatDoesNotExist'.") }.count, 1, "The link warning shouldn't be repeated.")
XCTAssertEqual(logLines.filter { $0.hasPrefix("warning: No symbol matched 'ModuleThatDoesNotExist'. Can't resolve 'ModuleThatDoesNotExist'.") }.count, 1)
}

#endif
Expand Down