Skip to content

Commit e353808

Browse files
Fix "missing technology root" warning for tutorials (#812)
Removes the "missing technology root" warning and adds a more specific one for tutorials without table of contents . rdar://117866037 * - `formattedDiagnosticSource` allows addition of new content file into the diagnostic message. - Replaced `org.swift.docc.MissingTechnologyRoot` diagnostic for `org.swift.docc.MissingTableOfContents` * Make sure table of contents template file exists instead of force-unwrapping. * Changed tutorial template root filename to a constant. Fixed tests.
1 parent bad0aaa commit e353808

File tree

5 files changed

+232
-38
lines changed

5 files changed

+232
-38
lines changed

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticConsoleWriter.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,29 @@ extension DefaultDiagnosticConsoleFormatter {
321321

322322
guard let url = diagnostic.source
323323
else { return "" }
324-
325324
guard let diagnosticRange = diagnostic.range
326-
else { return "\n--> \(formattedSourcePath(url))" }
325+
else {
326+
// If the replacement operation involves adding new files,
327+
// emit the file content as an addition instead of a replacement.
328+
//
329+
// Example:
330+
// --> /path/to/new/file.md
331+
// Summary
332+
// suggestion:
333+
// 0 + Addition file and
334+
// 1 + multiline file content.
335+
var addition = ""
336+
solutions.forEach { solution in
337+
addition.append("\n" + solution.summary)
338+
solution.replacements.forEach { replacement in
339+
let solutionFragments = replacement.replacement.split(separator: "\n")
340+
addition += "\nsuggestion:\n" + solutionFragments.enumerated().map {
341+
"\($0.offset) + \($0.element)"
342+
}.joined(separator: "\n")
343+
}
344+
}
345+
return "\n--> \(formattedSourcePath(url))\(addition)"
346+
}
327347

328348
let sourceLines = readSourceLines(url)
329349

Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -482,15 +482,42 @@ public struct ConvertAction: Action, RecreatingContext {
482482
}
483483

484484
var didEncounterError = analysisProblems.containsErrors || conversionProblems.containsErrors
485-
if try context.renderRootModules.isEmpty {
485+
let hasTutorial = context.knownPages.contains(where: {
486+
guard let kind = try? context.entity(with: $0).kind else { return false }
487+
return kind == .tutorial || kind == .tutorialArticle
488+
})
489+
// Warn the user if the catalog is a tutorial but does not contains a table of contents
490+
// and provide template content to fix this problem.
491+
if (
492+
context.rootTechnologies.isEmpty &&
493+
hasTutorial
494+
) {
495+
let tableOfContentsFilename = CatalogTemplateKind.tutorialTopLevelFilename
496+
let source = rootURL?.appendingPathComponent(tableOfContentsFilename)
497+
var replacements = [Replacement]()
498+
if let tableOfContentsTemplate = CatalogTemplateKind.tutorialTemplateFiles(converter.firstAvailableBundle()?.displayName ?? "Tutorial Name")[tableOfContentsFilename] {
499+
replacements.append(
500+
Replacement(
501+
range: .init(line: 1, column: 1, source: source) ..< .init(line: 1, column: 1, source: source),
502+
replacement: tableOfContentsTemplate
503+
)
504+
)
505+
}
486506
postConversionProblems.append(
487507
Problem(
488508
diagnostic: Diagnostic(
509+
source: source,
489510
severity: .warning,
490-
identifier: "org.swift.docc.MissingTechnologyRoot",
491-
summary: "No TechnologyRoot to organize article-only documentation.",
492-
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."
493-
)
511+
identifier: "org.swift.docc.MissingTableOfContents",
512+
summary: "Missing tutorial table of contents page.",
513+
explanation: "`@Tutorial` and `@Article` pages require a `@Tutorials` table of content page to define the documentation hierarchy."
514+
),
515+
possibleSolutions: [
516+
Solution(
517+
summary: "Create a `@Tutorials` table of content page.",
518+
replacements: replacements
519+
)
520+
]
494521
)
495522
)
496523
}

Sources/SwiftDocCUtilities/Action/Actions/Init/CatalogTemplateKind.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ extension CatalogTemplateKind {
4848
}
4949

5050
/// Content of the 'tutorial' template
51+
static var tutorialTopLevelFilename: String { "table-of-contents.tutorial" }
5152
static func tutorialTemplateFiles(_ title: String) -> [String: String] {
5253
[
53-
"table-of-contents.tutorial": """
54+
tutorialTopLevelFilename: """
5455
@Tutorials(name: "\(title)") {
5556
@Intro(title: "Tutorial Introduction") {
5657
Add one or more paragraphs that introduce your tutorial.

Tests/SwiftDocCTests/Diagnostics/DiagnosticConsoleWriterDefaultFormattingTest.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,4 +389,105 @@ class DiagnosticConsoleWriterDefaultFormattingTest: XCTestCase {
389389
""")
390390
}
391391
}
392+
393+
func testEmitAdditionReplacementSolution() throws {
394+
func problemsLoggerOutput(possibleSolutions: [Solution]) -> String {
395+
let logger = Logger()
396+
let consumer = DiagnosticConsoleWriter(logger, highlight: true)
397+
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)
398+
consumer.receive([problem])
399+
try? consumer.flush()
400+
return logger.output
401+
}
402+
let sourcelocation = SourceLocation(line: 1, column: 1, source: nil)
403+
let range = sourcelocation..<sourcelocation
404+
XCTAssertEqual(
405+
problemsLoggerOutput(possibleSolutions: [
406+
Solution(summary: "Create a sloth.", replacements: [
407+
Replacement(
408+
range: range,
409+
replacement: """
410+
var slothName = "slothy"
411+
var slothDiet = .vegetarian
412+
"""
413+
)
414+
])
415+
]),
416+
"""
417+
\u{1B}[1;33mwarning: Test diagnostic\u{1B}[0;0m
418+
--> /path/to/file.md
419+
Create a sloth.
420+
suggestion:
421+
0 + var slothName = \"slothy\"
422+
1 + var slothDiet = .vegetarian
423+
"""
424+
)
425+
426+
XCTAssertEqual(
427+
problemsLoggerOutput(possibleSolutions: [
428+
Solution(summary: "Create a sloth.", replacements: [
429+
Replacement(
430+
range: range,
431+
replacement: """
432+
var slothName = "slothy"
433+
var slothDiet = .vegetarian
434+
"""
435+
),
436+
Replacement(
437+
range: range,
438+
replacement: """
439+
var slothName = SlothGenerator().generateName()
440+
var slothDiet = SlothGenerator().generateDiet()
441+
"""
442+
)
443+
])
444+
]),
445+
"""
446+
\u{1B}[1;33mwarning: Test diagnostic\u{1B}[0;0m
447+
--> /path/to/file.md
448+
Create a sloth.
449+
suggestion:
450+
0 + var slothName = "slothy"
451+
1 + var slothDiet = .vegetarian
452+
suggestion:
453+
0 + var slothName = SlothGenerator().generateName()
454+
1 + var slothDiet = SlothGenerator().generateDiet()
455+
"""
456+
)
457+
458+
XCTAssertEqual(
459+
problemsLoggerOutput(possibleSolutions: [
460+
Solution(summary: "Create a sloth.", replacements: [
461+
Replacement(
462+
range: range,
463+
replacement: """
464+
var slothName = "slothy"
465+
var slothDiet = .vegetarian
466+
"""
467+
),
468+
]),
469+
Solution(summary: "Create a bee.", replacements: [
470+
Replacement(
471+
range: range,
472+
replacement: """
473+
var beeName = "Bee"
474+
var beeDiet = .vegetarian
475+
"""
476+
)
477+
])
478+
]),
479+
"""
480+
\u{1B}[1;33mwarning: Test diagnostic\u{1B}[0;0m
481+
--> /path/to/file.md
482+
Create a sloth.
483+
suggestion:
484+
0 + var slothName = "slothy"
485+
1 + var slothDiet = .vegetarian
486+
Create a bee.
487+
suggestion:
488+
0 + var beeName = "Bee"
489+
1 + var beeDiet = .vegetarian
490+
"""
491+
)
492+
}
392493
}

Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3057,31 +3057,79 @@ class ConvertActionTests: XCTestCase {
30573057

30583058
// Tests that when converting a catalog with no technology root a warning is raised (r93371988)
30593059
func testConvertWithNoTechnologyRoot() throws {
3060-
let bundle = Folder(name: "unit-test.docc", content: [
3060+
func problemsFromConverting(_ catalogContent: [File]) throws -> [Problem] {
3061+
let catalog = Folder(name: "unit-test.docc", content: catalogContent)
3062+
let testDataProvider = try TestFileSystem(folders: [catalog, Folder.emptyHTMLTemplateDirectory])
3063+
let engine = DiagnosticEngine()
3064+
var action = try ConvertAction(
3065+
documentationBundleURL: catalog.absoluteURL,
3066+
outOfProcessResolver: nil,
3067+
analyze: false,
3068+
targetDirectory: URL(fileURLWithPath: "/output"),
3069+
htmlTemplateDirectory: Folder.emptyHTMLTemplateDirectory.absoluteURL,
3070+
emitDigest: false,
3071+
currentPlatforms: nil,
3072+
dataProvider: testDataProvider,
3073+
fileManager: testDataProvider,
3074+
temporaryDirectory: URL(fileURLWithPath: "/tmp"),
3075+
diagnosticEngine: engine
3076+
)
3077+
_ = try action.perform(logHandle: .none)
3078+
return engine.problems
3079+
}
3080+
3081+
let onlyTutorialArticleProblems = try problemsFromConverting([
30613082
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
3062-
TextFile(name: "Documentation.md", utf8Content: "")
3083+
TextFile(name: "Article.tutorial", utf8Content: """
3084+
@Article(time: 20) {
3085+
@Intro(title: "Slothy Tutorials") {
3086+
This is an abstract for the intro.
3087+
}
3088+
}
3089+
"""
3090+
),
30633091
])
3064-
let testDataProvider = try TestFileSystem(folders: [bundle, Folder.emptyHTMLTemplateDirectory])
3065-
let targetDirectory = URL(fileURLWithPath: testDataProvider.currentDirectoryPath)
3066-
.appendingPathComponent("target", isDirectory: true)
3067-
let engine = DiagnosticEngine()
3068-
var action = try ConvertAction(
3069-
documentationBundleURL: bundle.absoluteURL,
3070-
outOfProcessResolver: nil,
3071-
analyze: true,
3072-
targetDirectory: targetDirectory,
3073-
htmlTemplateDirectory: Folder.emptyHTMLTemplateDirectory.absoluteURL,
3074-
emitDigest: false,
3075-
currentPlatforms: nil,
3076-
dataProvider: testDataProvider,
3077-
fileManager: testDataProvider,
3078-
temporaryDirectory: createTemporaryDirectory(),
3079-
diagnosticEngine: engine
3080-
)
3081-
let _ = try action.perform(logHandle: .none)
3082-
XCTAssertEqual(engine.problems.count, 1)
3083-
XCTAssertEqual(engine.problems.map { $0.diagnostic.identifier }, ["org.swift.docc.MissingTechnologyRoot"])
3084-
XCTAssert(engine.problems.contains(where: { $0.diagnostic.severity == .warning }))
3092+
XCTAssert(onlyTutorialArticleProblems.contains(where: {
3093+
$0.diagnostic.identifier == "org.swift.docc.MissingTableOfContents"
3094+
}))
3095+
3096+
let tutorialTableOfContentProblem = try problemsFromConverting([
3097+
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
3098+
TextFile(name: "table-of-contents.tutorial", utf8Content: """
3099+
"""
3100+
),
3101+
TextFile(name: "article.tutorial", utf8Content: """
3102+
@Article(time: 20) {
3103+
@Intro(title: "Slothy Tutorials") {
3104+
This is an abstract for the intro.
3105+
}
3106+
}
3107+
"""
3108+
),
3109+
])
3110+
XCTAssert(tutorialTableOfContentProblem.contains(where: {
3111+
$0.diagnostic.identifier == "org.swift.docc.MissingTableOfContents"
3112+
}))
3113+
3114+
let incompleteTutorialFile = try problemsFromConverting([
3115+
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
3116+
TextFile(name: "article.tutorial", utf8Content: """
3117+
@Chapter(name: "SlothCreator Essentials") {
3118+
@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.")
3119+
3120+
Create custom sloths and edit their attributes and powers using SlothCreator.
3121+
3122+
@TutorialReference(tutorial: "doc:Creating-Custom-Sloths")
3123+
}
3124+
"""
3125+
),
3126+
])
3127+
XCTAssert(incompleteTutorialFile.contains(where: {
3128+
$0.diagnostic.identifier == "org.swift.docc.missingTopLevelChild"
3129+
}))
3130+
XCTAssertFalse(incompleteTutorialFile.contains(where: {
3131+
$0.diagnostic.identifier == "org.swift.docc.MissingTableOfContents"
3132+
}))
30853133
}
30863134

30873135
func testWrittenDiagnosticsAfterConvert() throws {
@@ -3120,22 +3168,19 @@ class ConvertActionTests: XCTestCase {
31203168
)
31213169

31223170
let _ = try action.perform(logHandle: .none)
3123-
XCTAssertEqual(engine.problems.count, 2)
3171+
XCTAssertEqual(engine.problems.count, 1)
31243172

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

31273175
let diagnosticFileContent = try JSONDecoder().decode(DiagnosticFile.self, from: Data(contentsOf: diagnosticFile))
3128-
XCTAssertEqual(diagnosticFileContent.diagnostics.count, 2)
3176+
XCTAssertEqual(diagnosticFileContent.diagnostics.count, 1)
31293177

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

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

31413186
#endif

0 commit comments

Comments
 (0)