Skip to content

Commit

Permalink
Diagnose duplicate representations of @AlternateRepresentation
Browse files Browse the repository at this point in the history
  • Loading branch information
anferbui committed Nov 22, 2024
1 parent 03f91e6 commit 83a9e65
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 3 deletions.
96 changes: 96 additions & 0 deletions Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2779,6 +2779,9 @@ public class DocumentationContext {
}
}

// Run analysis to determine whether manually configured alternate representations are valid.
analyzeAlternateRepresentations()

// Run global ``TopicGraph`` global analysis.
analyzeTopicGraph()
}
Expand Down Expand Up @@ -3143,6 +3146,77 @@ extension DocumentationContext {
}
diagnosticEngine.emit(problems)
}

func analyzeAlternateRepresentations() {
var problems = [Problem]()
for node in topicGraph.nodes.values {
guard let entity = try? self.entity(with: node.reference) else { continue }

var sourceLanguageToReference: [SourceLanguage: AlternateRepresentation] = [:]
for alternateRepresentation in entity.metadata?.alternateRepresentations ?? [] {
guard case .resolved(.success(let counterpartReference)) = alternateRepresentation.reference,
let counterpartEntity = try? self.entity(with: counterpartReference) else {
continue
}

// Case where the original symbol already was defined in the languages of the counterpart symbol.
let duplicateSourceLanguages = counterpartEntity.availableSourceLanguages.intersection(entity.availableSourceLanguages)
if !duplicateSourceLanguages.isEmpty {
problems
.append(
Problem(
diagnostic: Diagnostic(
source: alternateRepresentation.originalMarkup.range?.source,
severity: .warning,
range: alternateRepresentation.originalMarkup.range,
identifier: "org.swift.docc.AlternateRepresentation.DuplicateLanguageDefinition",
summary: "This node already has a representation in \(duplicateSourceLanguages.diagnosticString)",
explanation: "This node is already available in \(entity.availableSourceLanguages.diagnosticString).",
),
possibleSolutions: [Solution(summary: "Replace the counterpart link with a node which isn't available in \(entity.availableSourceLanguages.diagnosticString)", replacements: [])]
)
)

}

let duplicateCounterpartLanguages = Set(sourceLanguageToReference.keys).intersection(counterpartEntity.availableSourceLanguages)
if !duplicateCounterpartLanguages.isEmpty {
let replacements = alternateRepresentation.originalMarkup.range.flatMap { [Replacement(range: $0, replacement: "")] } ?? []
let notes: [DiagnosticNote] = duplicateCounterpartLanguages.compactMap { duplicateCounterpartLanguage in
guard let alreadyExistingCounterpart = sourceLanguageToReference[duplicateCounterpartLanguage],
let range = alreadyExistingCounterpart.originalMarkup.range,
let source = range.source else {
return nil
}

return DiagnosticNote(source: source, range: range, message: """
An alternate representation for \(duplicateCounterpartLanguage.name) has already been defined by an @\(AlternateRepresentation.self) directive.
""")
}
problems
.append(
Problem(
diagnostic: Diagnostic(
source: alternateRepresentation.originalMarkup.range?.source,
severity: .warning,
range: alternateRepresentation.originalMarkup.range,
identifier: "org.swift.docc.AlternateRepresentation.DuplicateLanguageDefinition",
summary: "An alternate representation for \(duplicateCounterpartLanguages.diagnosticString) already exists",
explanation: "This node is already available in \(entity.availableSourceLanguages.union(sourceLanguageToReference.keys).diagnosticString).",
notes: notes
),
possibleSolutions: [Solution(summary: "Remove this alternate representation", replacements: replacements)]
)
)
}

// Update mapping from source language to alternate declaration, for diagnostic purposes
counterpartEntity.availableSourceLanguages.forEach { sourceLanguageToReference[$0] = alternateRepresentation }
}
}

diagnosticEngine.emit(problems)
}
}

extension GraphCollector.GraphKind {
Expand Down Expand Up @@ -3192,5 +3266,27 @@ extension DataAsset {
}
}

extension Set where Element == SourceLanguage {
fileprivate var diagnosticString: String {
var languageNames = self.sorted(by: { language1, language2 in
// Emit Swift first, then alphabetically.
switch (language1, language2) {
case (.swift, _): return true
case (_, .swift): return false
default: return language1.id < language2.id
}
}).map(\.name)

guard languageNames.count > 1 else {
return languageNames.first ?? ""
}

// Returns "Language1, Language2 and Language3"
let finalElement = languageNames.removeLast()
let commaSeparatedElements = languageNames.joined(separator: ", ")
return "\(commaSeparatedElements) and \(finalElement)"
}
}

@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
extension DocumentationContext: DocumentationContextDataProviderDelegate {}
Original file line number Diff line number Diff line change
Expand Up @@ -5376,7 +5376,7 @@ let expected = """
)
let tempURL = try createTempFolder(content: [exampleDocumentation])
let (_, bundle, context) = try loadBundle(from: tempURL)

let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/unit-test/Symbol", sourceLanguage: .swift)

let entity = try context.entity(with: reference)
Expand All @@ -5396,14 +5396,80 @@ let expected = """
return
}
XCTAssertEqual(unresolvedPath, .init(topicURL: .init(parsingAuthoredLink: "MissingSymbol")!))

// And an error should have been reportes
XCTAssertEqual(context.problems.count, 1)

let problem = try XCTUnwrap(context.problems.first)
XCTAssertEqual(problem.diagnostic.severity, .warning)
XCTAssertEqual(problem.diagnostic.summary, "Can't resolve 'MissingSymbol'")
}
}

func testDiagnosesAlternateDeclarations() throws {
let exampleDocumentation = Folder(
name: "unit-test.docc",
content: [
TextFile(name: "Symbol.md", utf8Content: """
# ``Symbol``
@Metadata {
@AlternateRepresentation(``CounterpartSymbol``)
@AlternateRepresentation(``OtherCounterpartSymbol``)
}
A symbol extension file defining an alternate representation which overlaps source languages with another one.
"""),
TextFile(name: "SwiftSymbol.md", utf8Content: """
# ``SwiftSymbol``
@Metadata {
@AlternateRepresentation(``Symbol``)
}
A symbol extension file defining an alternate representation which overlaps source languages with the current node.
"""),
JSONFile(
name: "unit-test.symbols.json",
content: makeSymbolGraph(
moduleName: "unit-test",
symbols: [
makeSymbol(id: "symbol-id", kind: .class, pathComponents: ["Symbol"]),
makeSymbol(id: "other-symbol-id", kind: .class, pathComponents: ["SwiftSymbol"]),
makeSymbol(id: "counterpart-symbol-id", language: .objectiveC, kind: .class, pathComponents: ["CounterpartSymbol"]),
makeSymbol(id: "other-counterpart-symbol-id", language: .objectiveC, kind: .class, pathComponents: ["OtherCounterpartSymbol"]),
]
)
),
]
)
let tempURL = try createTempFolder(content: [exampleDocumentation])

let (_, _, context) = try loadBundle(from: tempURL)

let alternateRepresentationProblems = context.problems.sorted(by: \.diagnostic.summary)
XCTAssertEqual(alternateRepresentationProblems.count, 2)

// Verify a problem is reported for having alternate representations with duplicate source languages
var problem = try XCTUnwrap(alternateRepresentationProblems.first)
XCTAssertEqual(problem.diagnostic.severity, .warning)
XCTAssertEqual(problem.diagnostic.summary, "An alternate representation for Objective-C already exists")
XCTAssertEqual(problem.diagnostic.explanation, "This node is already available in Swift and Objective-C.")
XCTAssertEqual(problem.possibleSolutions.count, 1)

// Verify solutions provide context and suggest to remove the duplicate directive
var solution = try XCTUnwrap(problem.possibleSolutions.first)
XCTAssertEqual(solution.summary, "Remove this alternate representation")
XCTAssertEqual(solution.replacements.count, 1)
XCTAssertEqual(solution.replacements.first?.replacement, "")

// Verify a problem is reported for trying to define an alternate representation for a language the symbol already supports
problem = try XCTUnwrap(alternateRepresentationProblems[1])
XCTAssertEqual(problem.diagnostic.severity, .warning)
XCTAssertEqual(problem.diagnostic.summary, "This node already has a representation in Swift")
XCTAssertEqual(problem.diagnostic.explanation, "This node is already available in Swift.")
XCTAssertEqual(problem.possibleSolutions.count, 1)

// Verify solutions provide context, but no replacements
solution = try XCTUnwrap(problem.possibleSolutions.first)
XCTAssertEqual(solution.summary, "Replace the counterpart link with a node which isn\'t available in Swift")
XCTAssertEqual(solution.replacements.count, 0)
}
}

func assertEqualDumps(_ lhs: String, _ rhs: String, file: StaticString = #file, line: UInt = #line) {
Expand Down

0 comments on commit 83a9e65

Please sign in to comment.