Skip to content

Commit b54cb7a

Browse files
authored
Lower severity of source-symbol-not-found diagnostic (#966)
* Elaborate on diagnostic message when the source of a symbol graph relationship is missing * Lower severity of the diagnostic when the "source" of a symbol graph relationship is missing. Since the solution is for the tool that created the symbol graph file to not emit that relationship, DocC can safely skip it and proceed with the build. This issue isn't severe enough to fail the build. * Further reduce the severity to a debug-only assertion.
1 parent e54de2e commit b54cb7a

File tree

2 files changed

+51
-152
lines changed

2 files changed

+51
-152
lines changed

Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,37 @@ import SymbolKit
1212

1313
/// A set of functions that add relationship information to a topic graph.
1414
struct SymbolGraphRelationshipsBuilder {
15-
/// A namespace for creation of topic-graph related problems.
16-
enum NodeProblem {
17-
/// Returns a problem about a node with the given precise identifier not being found.
18-
static func notFound(_ identifier: String) -> Problem {
19-
return Problem(diagnostic: Diagnostic(source: nil, severity: .error, range: nil, identifier: "org.swift.docc.SymbolNodeNotFound", summary: "Symbol with identifier \(identifier.singleQuoted) couldn't be found"), possibleSolutions: [])
15+
/// A namespace for debug assert messages for data-correctness issues in the symbol graph data.
16+
private enum AssertionMessages {
17+
static func sourceNotFound(_ relationship: SymbolGraph.Relationship) -> String {
18+
"""
19+
Source symbol \(relationship.source.singleQuoted) not found locally, from \(relationship.kind.rawValue.singleQuoted) relationship to \(relationship.target.singleQuoted).
20+
21+
The "source" of a symbol graph relationship should always refer to a symbol in the same symbol graph file.
22+
If it doesn't, then the tool that created the symbol graph file should move the relationship to the symbol graph file that defines the "source" symbol \
23+
or remove the relationship if none of the created symbol graph file defines the "source" symbol.
24+
25+
The "target" may refer to a symbol in another module.
26+
For example, if local symbol conforms to a protocol from another module, \
27+
there will be a "{ source: local-symbol-ID, kind: conformsTo, target: protocol-in-other-module-ID }" relationship.
28+
29+
A symbol graph relationship with a non-local "source" symbol is a bug in the tool that created the symbol graph file.
30+
"""
2031
}
21-
/// Returns a problem about a node with the given reference not being found.
22-
static func invalidReference(_ reference: String) -> Problem {
23-
return Problem(diagnostic: Diagnostic(source: nil, severity: .error, range: nil, identifier: "org.swift.docc.InvalidSymbolIdentifier", summary: "Relationship symbol path \(reference.singleQuoted) isn't valid"), possibleSolutions: [])
32+
33+
static func overloadGroupNotFound(_ relationship: SymbolGraph.Relationship) -> String {
34+
"""
35+
Overload group \(relationship.source.singleQuoted) not found locally, from \(relationship.kind.rawValue.singleQuoted) relationship of \(relationship.source.singleQuoted).
36+
37+
Both the "source" and "target" of an \(relationship.kind.rawValue.singleQuoted) symbol graph relationships with should always refer to symbols in the same symbol graph file.
38+
A \(relationship.kind.rawValue.singleQuoted) symbol graph relationship with a non-local "target" symbol is a bug in the tool that created the symbol graph file.
39+
"""
40+
}
41+
42+
static func invalidSymbolReference(_ reference: SymbolReference) -> String {
43+
return """
44+
Failed to create an unresolved reference for \(reference.path.singleQuoted). It contains characters that are not allowed in the "path" of a RFC 3986 URL.
45+
"""
2446
}
2547
}
2648

@@ -47,7 +69,7 @@ struct SymbolGraphRelationshipsBuilder {
4769
let implementorSymbol = implementorNode.semantic as? Symbol
4870
else {
4971
// The source node for implementation relationship not found.
50-
engine.emit(NodeProblem.notFound(edge.source))
72+
assertionFailure(AssertionMessages.sourceNotFound(edge))
5173
return
5274
}
5375

@@ -63,7 +85,7 @@ struct SymbolGraphRelationshipsBuilder {
6385
let symbolReference = SymbolReference(edge.target, interfaceLanguage: language, symbol: localCache[edge.target]?.symbol)
6486
guard let unresolved = UnresolvedTopicReference(symbolReference: symbolReference, bundle: bundle) else {
6587
// The symbol reference format is invalid.
66-
engine.emit(NodeProblem.invalidReference(symbolReference.path))
88+
assertionFailure(AssertionMessages.invalidSymbolReference(symbolReference))
6789
return
6890
}
6991

@@ -97,7 +119,7 @@ struct SymbolGraphRelationshipsBuilder {
97119
// Make the implementation a child of the requirement
98120
guard let childReference = localCache.reference(symbolID: edge.source) else {
99121
// The child wasn't found, invalid reference in relationship.
100-
engine.emit(SymbolGraphRelationshipsBuilder.NodeProblem.notFound(edge.source))
122+
assertionFailure(SymbolGraphRelationshipsBuilder.AssertionMessages.sourceNotFound(edge))
101123
return
102124
}
103125

@@ -132,7 +154,7 @@ struct SymbolGraphRelationshipsBuilder {
132154
let conformingSymbol = conformingNode.semantic as? Symbol
133155
else {
134156
// The source node for conformance relationship not found.
135-
engine.emit(NodeProblem.notFound(edge.source))
157+
assertionFailure(AssertionMessages.sourceNotFound(edge))
136158
return
137159
}
138160

@@ -153,7 +175,7 @@ struct SymbolGraphRelationshipsBuilder {
153175
let symbolReference = SymbolReference(edge.target, interfaceLanguage: language, symbol: localCache[edge.target]?.symbol)
154176
guard let unresolved = UnresolvedTopicReference(symbolReference: symbolReference, bundle: bundle) else {
155177
// The symbol reference format is invalid.
156-
engine.emit(NodeProblem.invalidReference(symbolReference.path))
178+
assertionFailure(AssertionMessages.invalidSymbolReference(symbolReference))
157179
return
158180
}
159181
conformanceNodeReference = .unresolved(unresolved)
@@ -221,7 +243,7 @@ struct SymbolGraphRelationshipsBuilder {
221243
let childSymbol = childNode.semantic as? Symbol
222244
else {
223245
// The source node for inheritance relationship not found.
224-
engine.emit(NodeProblem.notFound(edge.source))
246+
assertionFailure(AssertionMessages.sourceNotFound(edge))
225247
return
226248
}
227249

@@ -240,7 +262,7 @@ struct SymbolGraphRelationshipsBuilder {
240262
let symbolReference = SymbolReference(edge.target, interfaceLanguage: language, symbol: nil)
241263
guard let unresolved = UnresolvedTopicReference(symbolReference: symbolReference, bundle: bundle) else {
242264
// The symbol reference format is invalid.
243-
engine.emit(NodeProblem.invalidReference(symbolReference.path))
265+
assertionFailure(AssertionMessages.invalidSymbolReference(symbolReference))
244266
return
245267
}
246268
parentNodeReference = .unresolved(unresolved)
@@ -321,7 +343,7 @@ struct SymbolGraphRelationshipsBuilder {
321343
let requiredSymbol = requiredNode.semantic as? Symbol
322344
else {
323345
// The source node for requirement relationship not found.
324-
engine.emit(NodeProblem.notFound(edge.source))
346+
assertionFailure(AssertionMessages.sourceNotFound(edge))
325347
return
326348
}
327349
requiredSymbol.isRequired = required
@@ -438,11 +460,11 @@ struct SymbolGraphRelationshipsBuilder {
438460
engine: DiagnosticEngine
439461
) {
440462
guard let overloadNode = localCache[edge.source] else {
441-
engine.emit(NodeProblem.notFound(edge.source))
463+
assertionFailure(AssertionMessages.sourceNotFound(edge))
442464
return
443465
}
444466
guard let overloadGroupNode = localCache[edge.target] else {
445-
engine.emit(NodeProblem.notFound(edge.target))
467+
assertionFailure(AssertionMessages.overloadGroupNotFound(edge))
446468
return
447469
}
448470

Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift

Lines changed: 11 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -36,42 +36,6 @@ class ConvertActionTests: XCTestCase {
3636
forResource: "TestBundle", withExtension: "docc", subdirectory: "Test Bundles")!
3737
.appendingPathComponent("project.zip")
3838

39-
/// A symbol graph file that has missing symbols.
40-
let incompleteSymbolGraphFile = TextFile(name: "TechnologyX.symbols.json", utf8Content: """
41-
{
42-
"metadata": {
43-
"formatVersion" : {
44-
"major" : 1
45-
},
46-
"generator" : "app/1.0"
47-
},
48-
"module" : {
49-
"name" : "MyKit",
50-
"platform" : {
51-
"architecture" : "x86_64",
52-
"vendor" : "apple",
53-
"operatingSystem" : {
54-
"name" : "ios",
55-
"minimumVersion" : {
56-
"major" : 13,
57-
"minor" : 0,
58-
"patch" : 0
59-
}
60-
}
61-
}
62-
},
63-
"symbols" : [],
64-
"relationships" : [
65-
{
66-
"source" : "s:5MyKit0A5ProtocolP",
67-
"target" : "s:5Foundation0A5EarhartP",
68-
"kind" : "conformsTo"
69-
}
70-
]
71-
}
72-
"""
73-
)
74-
7539
func testCopyingImageAssets() throws {
7640
XCTAssert(FileManager.default.fileExists(atPath: imageFile.path))
7741
let testImageName = "TestImage.png"
@@ -667,7 +631,6 @@ class ConvertActionTests: XCTestCase {
667631
expectedOutput.assertExist(at: result.outputs[0], fileManager: testDataProvider)
668632
}
669633

670-
/// Ensures that we always generate diagnosticJSON when there are errors. (rdar://72345373)
671634
func testOutputFolderContainsDiagnosticJSONWhenThereAreErrorsAndNoTemplate() throws {
672635
// Documentation bundle that contains an image
673636
let bundle = Folder(name: "unit-test.docc", content: [
@@ -809,7 +772,6 @@ class ConvertActionTests: XCTestCase {
809772
expectedOutput.assertExist(at: result.outputs[0], fileManager: testDataProvider)
810773
}
811774

812-
/// Ensures we never delete an existing build folder if conversion fails (rdar://72339050)
813775
func testOutputFolderIsNotRemovedWhenThereAreErrors() throws {
814776
let tutorialsFile = TextFile(name: "TechnologyX.tutorial", utf8Content: """
815777
@Tutorials(name: "Technology Z") {
@@ -847,11 +809,6 @@ class ConvertActionTests: XCTestCase {
847809
bundleInfoPlist,
848810
])
849811

850-
let badBundle = Folder(name: "unit-test.docc", content: [
851-
incompleteSymbolGraphFile,
852-
bundleInfoPlist,
853-
])
854-
855812
let testDataProvider = try TestFileSystem(folders: [goodBundle, Folder.emptyHTMLTemplateDirectory])
856813
let targetDirectory = URL(fileURLWithPath: testDataProvider.currentDirectoryPath)
857814
.appendingPathComponent("target", isDirectory: true)
@@ -883,76 +840,6 @@ class ConvertActionTests: XCTestCase {
883840
])
884841
expectedOutput.assertExist(at: targetDirectory, fileManager: testDataProvider)
885842
}
886-
887-
try testDataProvider.updateDocumentationBundles(withFolders: [badBundle])
888-
889-
do {
890-
var action = try ConvertAction(
891-
documentationBundleURL: badBundle.absoluteURL,
892-
outOfProcessResolver: nil,
893-
analyze: false,
894-
targetDirectory: targetDirectory,
895-
htmlTemplateDirectory: Folder.emptyHTMLTemplateDirectory.absoluteURL,
896-
emitDigest: false,
897-
currentPlatforms: nil,
898-
dataProvider: testDataProvider,
899-
fileManager: testDataProvider,
900-
temporaryDirectory: testDataProvider.uniqueTemporaryDirectory())
901-
let result = try action.perform(logHandle: .none)
902-
903-
XCTAssert(
904-
result.didEncounterError,
905-
"We expect errors to occur during during conversion of the bad test bundle."
906-
)
907-
908-
// Verify that the build output folder from the former successful conversion
909-
// still exists after this failure.
910-
let expectedOutput = Folder(name: ".docc-build", content: [
911-
Folder(name: "data", content: [
912-
]),
913-
])
914-
expectedOutput.assertExist(at: targetDirectory, fileManager: testDataProvider)
915-
}
916-
}
917-
918-
func testOutputFolderContainsDiagnosticJSONWhenThereAreErrors() throws {
919-
// Documentation bundle that contains an image
920-
let bundle = Folder(name: "unit-test.docc", content: [
921-
incompleteSymbolGraphFile,
922-
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
923-
])
924-
925-
let testDataProvider = try TestFileSystem(folders: [bundle, Folder.emptyHTMLTemplateDirectory])
926-
let targetDirectory = URL(fileURLWithPath: testDataProvider.currentDirectoryPath)
927-
.appendingPathComponent("target", isDirectory: true)
928-
929-
var action = try ConvertAction(
930-
documentationBundleURL: bundle.absoluteURL,
931-
outOfProcessResolver: nil,
932-
analyze: false,
933-
targetDirectory: targetDirectory,
934-
htmlTemplateDirectory: Folder.emptyHTMLTemplateDirectory.absoluteURL,
935-
emitDigest: true,
936-
currentPlatforms: nil,
937-
dataProvider: testDataProvider,
938-
fileManager: testDataProvider,
939-
temporaryDirectory: testDataProvider.uniqueTemporaryDirectory())
940-
let result = try action.perform(logHandle: .none)
941-
942-
// Verify that the following files and folder exist at the output location
943-
let expectedOutput = Folder(name: ".docc-build", content: [
944-
JSONFile(name: "diagnostics.json", content: [
945-
Digest.Diagnostic(
946-
start: nil,
947-
source: nil,
948-
severity: .error,
949-
summary: "Symbol with identifier 's:5MyKit0A5ProtocolP' couldn't be found",
950-
explanation: nil,
951-
notes: []
952-
),
953-
]),
954-
])
955-
expectedOutput.assertExist(at: result.outputs[0], fileManager: testDataProvider)
956843
}
957844

958845
/// Verifies that digest is correctly emitted for API documentation topics
@@ -1274,7 +1161,7 @@ class ConvertActionTests: XCTestCase {
12741161
])
12751162
}
12761163

1277-
func testDownloadMetadataIsWritenToOutputFolder() throws {
1164+
func testDownloadMetadataIsWrittenToOutputFolder() throws {
12781165
let bundle = Folder(name: "unit-test.docc", content: [
12791166
CopyOfFile(original: projectZipFile),
12801167
CopyOfFile(original: imageFile, newName: "referenced-tutorials-image.png"),
@@ -2387,7 +2274,6 @@ class ConvertActionTests: XCTestCase {
23872274
This article has a malformed title and can't be analyzed, so it
23882275
produces one warning.
23892276
"""),
2390-
incompleteSymbolGraphFile,
23912277
])
23922278

23932279
let testDataProvider = try TestFileSystem(folders: [bundle, Folder.emptyHTMLTemplateDirectory])
@@ -2411,8 +2297,8 @@ class ConvertActionTests: XCTestCase {
24112297
)
24122298
let result = try action.perform(logHandle: .none)
24132299

2414-
XCTAssertEqual(engine.problems.count, 1, "\(ConvertAction.self) didn't filter out diagnostics above the 'error' level.")
2415-
XCTAssert(result.didEncounterError)
2300+
XCTAssertEqual(engine.problems.count, 0, "\(ConvertAction.self) didn't filter out diagnostics at-or-above the 'error' level.")
2301+
XCTAssertFalse(result.didEncounterError, "The issues with this test bundle are not severe enough to fail the build.")
24162302
}
24172303

24182304
func testDiagnosticLevelIgnoredWhenAnalyzeIsPresent() throws {
@@ -2425,7 +2311,6 @@ class ConvertActionTests: XCTestCase {
24252311
This article has a malformed title and can't be analyzed, so it
24262312
produces one warning.
24272313
"""),
2428-
incompleteSymbolGraphFile,
24292314
])
24302315

24312316
let testDataProvider = try TestFileSystem(folders: [bundle, Folder.emptyHTMLTemplateDirectory])
@@ -2449,9 +2334,9 @@ class ConvertActionTests: XCTestCase {
24492334
)
24502335
let result = try action.perform(logHandle: .none)
24512336

2452-
XCTAssertEqual(engine.problems.count, 2, "\(ConvertAction.self) shouldn't filter out diagnostics when the '--analyze' flag is passed")
2453-
XCTAssertEqual(engine.problems.map { $0.diagnostic.identifier }, ["org.swift.docc.Article.Title.NotFound", "org.swift.docc.SymbolNodeNotFound"])
2454-
XCTAssert(result.didEncounterError)
2337+
XCTAssertEqual(engine.problems.count, 1, "\(ConvertAction.self) shouldn't filter out diagnostics when the '--analyze' flag is passed")
2338+
XCTAssertEqual(engine.problems.map { $0.diagnostic.identifier }, ["org.swift.docc.Article.Title.NotFound"])
2339+
XCTAssertFalse(result.didEncounterError, "The issues with this test bundle are not severe enough to fail the build.")
24552340
XCTAssert(engine.problems.contains(where: { $0.diagnostic.severity == .warning }))
24562341
}
24572342

@@ -2465,7 +2350,6 @@ class ConvertActionTests: XCTestCase {
24652350
This article has a malformed title and can't be analyzed, so it
24662351
produces one warning.
24672352
"""),
2468-
incompleteSymbolGraphFile,
24692353
])
24702354

24712355
let testDataProvider = try TestFileSystem(folders: [bundle, Folder.emptyHTMLTemplateDirectory])
@@ -2485,9 +2369,7 @@ class ConvertActionTests: XCTestCase {
24852369
temporaryDirectory: testDataProvider.uniqueTemporaryDirectory(),
24862370
diagnosticLevel: "error"
24872371
)
2488-
XCTAssertThrowsError(try action.performAndHandleResult(logHandle: .none)) { error in
2489-
XCTAssert(error is ErrorsEncountered, "Unexpected error type thrown by \(ConvertAction.self)")
2490-
}
2372+
XCTAssertNoThrow(try action.performAndHandleResult(logHandle: .none))
24912373
}
24922374

24932375
func testWritesDiagnosticFileWhenThrowingError() throws {
@@ -2500,7 +2382,6 @@ class ConvertActionTests: XCTestCase {
25002382
This article has a malformed title and can't be analyzed, so it
25012383
produces one warning.
25022384
"""),
2503-
incompleteSymbolGraphFile,
25042385
])
25052386

25062387
let testDataProvider = try TestFileSystem(folders: [bundle, Folder.emptyHTMLTemplateDirectory])
@@ -2527,9 +2408,7 @@ class ConvertActionTests: XCTestCase {
25272408

25282409
// TODO: Support TestFileSystem in DiagnosticFileWriter
25292410
XCTAssertFalse(FileManager.default.fileExists(atPath: diagnosticFile.path), "Diagnostic file doesn't exist before")
2530-
XCTAssertThrowsError(try action.performAndHandleResult(logHandle: .none)) { error in
2531-
XCTAssert(error is ErrorsEncountered, "Unexpected error type thrown by \(ConvertAction.self)")
2532-
}
2411+
XCTAssertNoThrow(try action.performAndHandleResult(logHandle: .none))
25332412
XCTAssertTrue(FileManager.default.fileExists(atPath: diagnosticFile.path), "Diagnostic file exist after")
25342413
}
25352414

@@ -2580,7 +2459,6 @@ class ConvertActionTests: XCTestCase {
25802459
let bundle = Folder(name: "unit-test.docc", content: [
25812460
InfoPlist(displayName: "TestBundle", identifier: "com.test.example"),
25822461
CopyOfFile(original: symbolGraphFile, newName: "MyKit.symbols.json"),
2583-
incompleteSymbolGraphFile,
25842462
])
25852463

25862464
let testDataProvider = try TestFileSystem(folders: [bundle, Folder.emptyHTMLTemplateDirectory])
@@ -2602,13 +2480,12 @@ class ConvertActionTests: XCTestCase {
26022480
temporaryDirectory: testDataProvider.uniqueTemporaryDirectory()
26032481
)
26042482

2605-
XCTAssertThrowsError(try action.performAndHandleResult(logHandle: .none), "The test bundle should have thrown an error about an incomplete symbol graph file")
2606-
XCTAssert(testDataProvider.fileExists(atPath: digestFileURL.path), "The digest file should have been written even though compilation errors occurred")
2483+
XCTAssertNoThrow(try action.performAndHandleResult(logHandle: .none))
2484+
XCTAssert(testDataProvider.fileExists(atPath: digestFileURL.path))
26072485

26082486
let data = try testDataProvider.contentsOfURL(digestFileURL)
26092487
let diagnostics = try RenderJSONDecoder.makeDecoder().decode([Digest.Diagnostic].self, from: data)
2610-
XCTAssertEqual(diagnostics.count, 1)
2611-
2488+
XCTAssertEqual(diagnostics.count, 0)
26122489
}
26132490

26142491
func testRenderIndexJSONGeneration() throws {

0 commit comments

Comments
 (0)