Skip to content

Commit

Permalink
Emit diagnostics for non-symbol pages
Browse files Browse the repository at this point in the history
`@AlternateRepresentation` is not expected for non-symbol pages, and we now emit a diagnostic for this case.
  • Loading branch information
anferbui committed Dec 12, 2024
1 parent ad4d279 commit 149a977
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 11 deletions.
46 changes: 41 additions & 5 deletions Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3207,19 +3207,56 @@ extension DocumentationContext {
var problems = [Problem]()

func listSourceLanguages(_ sourceLanguages: Set<SourceLanguage>) -> String {
return sourceLanguages.map(\.name).list(finalConjunction: .and)
sourceLanguages.map(\.name).list(finalConjunction: .and)
}
func removeAlternateRepresentationSolution(_ alternateRepresentation: AlternateRepresentation) -> [Solution] {
[Solution(
summary: "Remove this alternate representation",
replacements: alternateRepresentation.originalMarkup.range.flatMap { [Replacement(range: $0, replacement: "")] } ?? [])]
}

for node in topicGraph.nodes.values {
guard let entity = try? self.entity(with: node.reference) else { continue }
guard let entity = try? self.entity(with: node.reference), let alternateRepresentations = entity.metadata?.alternateRepresentations else { continue }

var sourceLanguageToReference: [SourceLanguage: AlternateRepresentation] = [:]
for alternateRepresentation in entity.metadata?.alternateRepresentations ?? [] {
for alternateRepresentation in alternateRepresentations {
// Check if the entity is not a symbol, as only symbols are allowed to specify custom alternate representations
guard entity.symbol != nil else {
problems.append(Problem(
diagnostic: Diagnostic(
source: alternateRepresentation.originalMarkup.range?.source,
severity: .warning,
range: alternateRepresentation.originalMarkup.range,
identifier: "org.swift.docc.AlternateRepresentation.UnsupportedPageKind",
summary: "Custom alternate representations are not supported for page kind \(entity.kind.name.singleQuoted)",
explanation: "Alternate representations are only supported for symbols."
),
possibleSolutions: removeAlternateRepresentationSolution(alternateRepresentation)
))
continue
}

guard case .resolved(.success(let alternateRepresentationReference)) = alternateRepresentation.reference,
let alternateRepresentationEntity = try? self.entity(with: alternateRepresentationReference) else {
continue
}

// Check if the resolved entity is not a symbol, as only symbols are allowed as custom alternate representations
guard alternateRepresentationEntity.symbol != nil else {
problems.append(Problem(
diagnostic: Diagnostic(
source: alternateRepresentation.originalMarkup.range?.source,
severity: .warning,
range: alternateRepresentation.originalMarkup.range,
identifier: "org.swift.docc.AlternateRepresentation.UnsupportedPageKind",
summary: "Page kind \(alternateRepresentationEntity.kind.name.singleQuoted) is not allowed as a custom alternate language representation",
explanation: "Symbols can only specify other symbols as custom language representations."
),
possibleSolutions: removeAlternateRepresentationSolution(alternateRepresentation)
))
continue
}

// Check if the documented symbol already has alternate representations from in-source annotations.
let duplicateSourceLanguages = alternateRepresentationEntity.availableSourceLanguages.intersection(entity.availableSourceLanguages)
if !duplicateSourceLanguages.isEmpty {
Expand All @@ -3238,7 +3275,6 @@ extension DocumentationContext {

let duplicateAlternateLanguages = Set(sourceLanguageToReference.keys).intersection(alternateRepresentationEntity.availableSourceLanguages)
if !duplicateAlternateLanguages.isEmpty {
let replacements = alternateRepresentation.originalMarkup.range.flatMap { [Replacement(range: $0, replacement: "")] } ?? []
let notes: [DiagnosticNote] = duplicateAlternateLanguages.compactMap { duplicateAlternateLanguage in
guard let alreadyExistingRepresentation = sourceLanguageToReference[duplicateAlternateLanguage],
let range = alreadyExistingRepresentation.originalMarkup.range,
Expand All @@ -3258,7 +3294,7 @@ extension DocumentationContext {
explanation: "Only one custom alternate language representation can be specified per language.",
notes: notes
),
possibleSolutions: [Solution(summary: "Remove this alternate representation", replacements: replacements)]
possibleSolutions: removeAlternateRepresentationSolution(alternateRepresentation)
))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5365,6 +5365,7 @@ let expected = """
# ``Symbol``
@Metadata {
@AlternateRepresentation(``CounterpartSymbol``)
@AlternateRepresentation(OtherCounterpartSymbol)
@AlternateRepresentation(``MissingSymbol``)
}
A symbol extension file defining an alternate representation.
Expand All @@ -5387,38 +5388,54 @@ let expected = """
]
)
),
JSONFile(
name: "unit-test.js.symbols.json",
content: makeSymbolGraph(
moduleName: "unit-test",
symbols: [
makeSymbol(id: "other-counterpart-symbol-id", language: .javaScript, kind: .class, pathComponents: ["OtherCounterpartSymbol"]),
]
)
),
]
))

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

let entity = try context.entity(with: reference)
XCTAssertEqual(entity.metadata?.alternateRepresentations.count, 2)
XCTAssertEqual(entity.metadata?.alternateRepresentations.count, 3)

// First counterpart should have been resolved successfully
// First alternate representation should have been resolved successfully
var alternateRepresentation = try XCTUnwrap(entity.metadata?.alternateRepresentations.first)
XCTAssertEqual(
alternateRepresentation.reference,
.resolved(.success(.init(bundleID: bundle.id, path: "/documentation/unit-test/CounterpartSymbol", sourceLanguage: .objectiveC)))
)

// Second counterpart shouldn't have been resolved at all
alternateRepresentation = try XCTUnwrap(entity.metadata?.alternateRepresentations[1])
// Second alternate representation without "``" should also have been resolved successfully
alternateRepresentation = try XCTUnwrap(entity.metadata?.alternateRepresentations.dropFirst().first)
XCTAssertEqual(
alternateRepresentation.reference,
.resolved(.success(.init(bundleID: bundle.id, path: "/documentation/unit-test/OtherCounterpartSymbol", sourceLanguage: .objectiveC)))
)

// Third alternate representation shouldn't have been resolved at all
alternateRepresentation = try XCTUnwrap(entity.metadata?.alternateRepresentations.dropFirst().last)
guard case .resolved(.failure(let unresolvedPath, _)) = alternateRepresentation.reference else {
XCTFail("Expected alternate representation to be unresolved, but was resolved as \(alternateRepresentation.reference)")
return
}
XCTAssertEqual(unresolvedPath, .init(topicURL: .init(parsingAuthoredLink: "MissingSymbol")!))

// And an error should have been reportes
// And an error should have been reported
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 {
func testDiagnosesSymbolAlternateDeclarations() throws {
let (_, context) = try loadBundle(catalog: Folder(
name: "unit-test.docc",
content: [
Expand Down Expand Up @@ -5488,6 +5505,67 @@ let expected = """
XCTAssertEqual(solution.replacements.count, 1)
XCTAssertEqual(solution.replacements.first?.replacement, "")
}

func testDiagnosesArticleAlternateDeclarations() throws {
let (_, context) = try loadBundle(catalog: Folder(
name: "unit-test.docc",
content: [
TextFile(name: "Symbol.md", utf8Content: """
# ``Symbol``
@Metadata {
@AlternateRepresentation("doc:Article")
}
A symbol extension file specifying an alternate representation which is an article.
"""),
TextFile(name: "Article.md", utf8Content: """
# Article
@Metadata {
@AlternateRepresentation(``Symbol``)
}
An article specifying a custom alternate representation.
"""),
JSONFile(
name: "unit-test.occ.symbols.json",
content: makeSymbolGraph(
moduleName: "unit-test",
symbols: [
makeSymbol(id: "symbol-id", kind: .class, pathComponents: ["Symbol"]),
]
)
)
]
))

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

// Verify a problem is reported for trying to define an alternate representation for a language the symbol already supports
var problem = try XCTUnwrap(alternateRepresentationProblems.first)
XCTAssertEqual(problem.diagnostic.severity, .warning)
XCTAssertEqual(problem.diagnostic.summary, "Custom alternate representations are not supported for page kind 'Article'")
XCTAssertEqual(problem.diagnostic.explanation, "Alternate representations are only supported for symbols.")
XCTAssertEqual(problem.possibleSolutions.count, 1)

// Verify solutions provide context and suggest to remove the invalid 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 having alternate representations with duplicate source languages
problem = try XCTUnwrap(alternateRepresentationProblems[1])
XCTAssertEqual(problem.diagnostic.severity, .warning)
XCTAssertEqual(problem.diagnostic.summary, "Page kind 'Article' is not allowed as a custom alternate language representation")
XCTAssertEqual(problem.diagnostic.explanation, "Symbols can only specify other symbols as custom language representations.")
XCTAssertEqual(problem.possibleSolutions.count, 1)

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

}

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

0 comments on commit 149a977

Please sign in to comment.