Skip to content

Commit d7720d9

Browse files
- formattedDiagnosticSource allows addition of new content file into the diagnostic message.
- Replaced `org.swift.docc.MissingTechnologyRoot` diagnostic for `org.swift.docc.MissingTableOfContents`
1 parent 6516bb1 commit d7720d9

File tree

4 files changed

+169
-30
lines changed

4 files changed

+169
-30
lines changed

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticConsoleWriter.swift

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

295295
guard let url = diagnostic.source
296296
else { return "" }
297-
298297
guard let diagnosticRange = diagnostic.range
299-
else { return "\n--> \(formattedSourcePath(url))" }
298+
else {
299+
// If the replacement operation involves adding new files,
300+
// emit the file content as an addition instead of a replacement.
301+
//
302+
// Example:
303+
// --> /path/to/new/file.md
304+
// Summary
305+
// suggestion:
306+
// 0 + Addition file and
307+
// 1 + multiline file content.
308+
var addition = ""
309+
solutions.forEach { solution in
310+
addition.append("\n" + solution.summary)
311+
solution.replacements.forEach { replacement in
312+
let solutionFragments = replacement.replacement.split(separator: "\n")
313+
addition += "\nsuggestion:\n" + solutionFragments.enumerated().map {
314+
"\($0.offset) + \($0.element)"
315+
}.joined(separator: "\n")
316+
}
317+
}
318+
return "\n--> \(formattedSourcePath(url))\(addition)"
319+
}
300320

301321
let sourceLines = readSourceLines(url)
302322

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

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -469,23 +469,39 @@ public struct ConvertAction: Action, RecreatingContext {
469469
}
470470

471471
var didEncounterError = analysisProblems.containsErrors || conversionProblems.containsErrors
472-
if (try context.renderRootModules + context.rootTechnologies).isEmpty {
473-
// Check if the converted documentation is a tutorial to provide
474-
// a specific error message.
475-
let hasTutorial = context.knownPages.contains(where: {
476-
guard let kind = try? context.entity(with: $0).kind else { return false }
477-
return kind == .tutorial || kind == .tutorialArticle
478-
})
472+
let hasTutorial = context.knownPages.contains(where: {
473+
guard let kind = try? context.entity(with: $0).kind else { return false }
474+
return kind == .tutorial || kind == .tutorialArticle
475+
})
476+
// Warn the user if the catalog is a tutorial but does not contains a table of contents
477+
// and provide template content to fix this problem.
478+
if (
479+
context.rootTechnologies.isEmpty &&
480+
hasTutorial &&
481+
!analysisProblems.contains(where: {$0.diagnostic.identifier == "org.swift.docc.HasExactlyOne<Tutorials, Intro>.Missing"})
482+
) {
483+
let tableOfContentsFilename = "table-of-contents.tutorial"
484+
let source = rootURL?.appendingPathComponent(tableOfContentsFilename)
479485
postConversionProblems.append(
480486
Problem(
481487
diagnostic: Diagnostic(
488+
source: source,
482489
severity: .warning,
483-
identifier: "org.swift.docc.MissingTechnologyRoot",
484-
summary: "There was no root found for this documentation catalog.",
485-
explanation: hasTutorial ? """
486-
For Tutorials please add a table of contents page (indicated by a `Tutorials` directive with the corresponding `Intro` and `Chapters` directive) to define the root of the documentation hierarchy. \n
487-
""" : ""
488-
)
490+
identifier: "org.swift.docc.MissingTableOfContents",
491+
summary: "Missing tutorial table of contents page.",
492+
explanation: "`@Tutorial` and `@Article` pages require a `@Tutorials` table of content page to define the documentation hierarchy."
493+
),
494+
possibleSolutions: [
495+
Solution(
496+
summary: "Create a `@Tutorials` table of content page.",
497+
replacements: [
498+
Replacement(
499+
range: .init(line: 1, column: 1, source: source) ..< .init(line: 1, column: 1, source: source),
500+
replacement: CatalogTemplateKind.tutorialTemplateFiles(converter.firstAvailableBundle()?.displayName ?? "Project Name")[tableOfContentsFilename]! // this file name is known to exist in the dictionary
501+
)
502+
]
503+
)
504+
]
489505
)
490506
)
491507
}

Tests/SwiftDocCTests/Diagnostics/DiagnosticConsoleWriterDefaultFormattingTest.swift

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

Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3092,13 +3092,6 @@ class ConvertActionTests: XCTestCase {
30923092
return engine.problems
30933093
}
30943094

3095-
3096-
let TutorialArticleWithNoContentProblems = try problemsFromConverting([
3097-
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
3098-
TextFile(name: "Article.tutorial", utf8Content: "")
3099-
])
3100-
XCTAssert(TutorialArticleWithNoContentProblems.contains(where: { $0.diagnostic.identifier == "org.swift.docc.MissingTechnologyRoot" }))
3101-
31023095
let onlyTutorialArticleProblems = try problemsFromConverting([
31033096
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
31043097
TextFile(name: "Article.tutorial", utf8Content: """
@@ -3110,17 +3103,27 @@ class ConvertActionTests: XCTestCase {
31103103
"""
31113104
),
31123105
])
3113-
XCTAssert(onlyTutorialArticleProblems.contains(where: { $0.diagnostic.identifier == "org.swift.docc.MissingTechnologyRoot" }))
3106+
XCTAssert(onlyTutorialArticleProblems.contains(where: {
3107+
$0.diagnostic.identifier == "org.swift.docc.MissingTableOfContents"
3108+
}))
31143109

31153110
let tutorialTableOfContentProblem = try problemsFromConverting([
31163111
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
31173112
TextFile(name: "table-of-contents.tutorial", utf8Content: """
3118-
@Tutorials(name: "Tutorial") {
3113+
"""
3114+
),
3115+
TextFile(name: "article.tutorial", utf8Content: """
3116+
@Article(time: 20) {
3117+
@Intro(title: "Slothy Tutorials") {
3118+
This is an abstract for the intro.
3119+
}
31193120
}
31203121
"""
31213122
),
31223123
])
3123-
XCTAssert(tutorialTableOfContentProblem.contains(where: { $0.diagnostic.identifier == "org.swift.docc.MissingTechnologyRoot" }))
3124+
XCTAssert(tutorialTableOfContentProblem.contains(where: {
3125+
$0.diagnostic.identifier == "org.swift.docc.MissingTableOfContents"
3126+
}))
31243127
}
31253128

31263129
func testWrittenDiagnosticsAfterConvert() throws {
@@ -3159,21 +3162,20 @@ class ConvertActionTests: XCTestCase {
31593162
)
31603163

31613164
let _ = try action.perform(logHandle: .standardOutput)
3162-
XCTAssertEqual(engine.problems.count, 2)
3165+
XCTAssertEqual(engine.problems.count, 1)
31633166

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

31663169
let diagnosticFileContent = try JSONDecoder().decode(DiagnosticFile.self, from: Data(contentsOf: diagnosticFile))
3167-
XCTAssertEqual(diagnosticFileContent.diagnostics.count, 2)
3170+
XCTAssertEqual(diagnosticFileContent.diagnostics.count, 1)
31683171

31693172
XCTAssertEqual(diagnosticFileContent.diagnostics.map(\.summary).sorted(), [
3170-
"There was no root found for this documentation catalog.",
31713173
"No symbol matched 'ModuleThatDoesNotExist'. Can't resolve 'ModuleThatDoesNotExist'."
31723174
].sorted())
31733175

31743176
let logLines = logStorage.text.splitByNewlines
3175-
XCTAssertEqual(logLines.filter { ($0 as NSString).contains("warning:") }.count, 2, "There should be two warnings printed to the console")
3176-
XCTAssertEqual(logLines.filter { ($0 as NSString).contains("There was no root found for this documentation catalog.") }.count, 1, "The root page warning shouldn't be repeated.")
3177+
XCTAssertEqual(logLines.filter { ($0 as NSString).contains("warning:") }.count, 1, "There should be two warnings printed to the console")
3178+
XCTAssertEqual(logLines.filter { ($0 as NSString).contains("There was no root found for this documentation catalog.") }.count, 0, "The root page warning shouldn't be repeated.")
31773179
XCTAssertEqual(logLines.filter { ($0 as NSString).contains("No symbol matched 'ModuleThatDoesNotExist'. Can't resolve 'ModuleThatDoesNotExist'.") }.count, 1, "The link warning shouldn't be repeated.")
31783180
}
31793181

0 commit comments

Comments
 (0)