Skip to content

Commit a08f470

Browse files
Support Possible Values section (#1006)
Support for Possible Values section. Support a new markdown tag, `possibleValues`, and a new `Possible Values` render section to document possible values extracted from the SymbolGraph. If a documented possible value does not correspond with the values from the SymbolGraph, it is dropped. rdar://123262314 - Links inside possible values description gets resolved. - Remove possible values from the Attributes render section. - Display all possible values in its own section. - Display possible values even when any is documented. - Made `Possible Values` public API.
1 parent 1b6e17b commit a08f470

File tree

13 files changed

+542
-16
lines changed

13 files changed

+542
-16
lines changed

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ public struct DocumentationNode {
311311
returnsSectionVariants: .empty,
312312
parametersSectionVariants: .empty,
313313
dictionaryKeysSectionVariants: .empty,
314+
possibleValuesSectionVariants: .empty,
314315
httpEndpointSectionVariants: endpointVariants,
315316
httpBodySectionVariants: .empty,
316317
httpParametersSectionVariants: .empty,
@@ -421,6 +422,49 @@ public struct DocumentationNode {
421422
semantic.httpResponsesSectionVariants[.fallback] = HTTPResponsesSection(responses: responses)
422423
}
423424

425+
// The property list symbol's allowed values.
426+
let symbolAllowedValues = symbol![mixin: SymbolGraph.Symbol.AllowedValues.self]
427+
428+
if let possibleValues = markupModel.discussionTags?.possibleValues, !possibleValues.isEmpty {
429+
let validator = PropertyListPossibleValuesSection.Validator(diagnosticEngine: engine)
430+
guard let symbolAllowedValues else {
431+
possibleValues.forEach {
432+
engine.emit(validator.makeExtraPossibleValueProblem($0, knownPossibleValues: [], symbolName: self.name.plainText))
433+
}
434+
return
435+
}
436+
437+
// Ignore documented possible values that don't exist in the symbol's allowed values in the symbol graph.
438+
let allowedPossibleValueNames = Set(symbolAllowedValues.value.map { String($0) })
439+
var (knownPossibleValues, unknownPossibleValues) = possibleValues.categorize(where: {
440+
allowedPossibleValueNames.contains($0.value)
441+
})
442+
443+
// Add the symbol possible values that are not documented.
444+
let knownPossibleValueNames = Set(knownPossibleValues.map(\.value))
445+
knownPossibleValues.append(contentsOf: symbolAllowedValues.value.compactMap { possibleValue in
446+
let possibleValueString = String(possibleValue)
447+
guard !knownPossibleValueNames.contains(possibleValueString) else {
448+
return nil
449+
}
450+
return PropertyListPossibleValuesSection.PossibleValue(value: possibleValueString, contents: [])
451+
})
452+
453+
for unknownValue in unknownPossibleValues {
454+
engine.emit(
455+
validator.makeExtraPossibleValueProblem(unknownValue, knownPossibleValues: knownPossibleValueNames, symbolName: self.name.plainText)
456+
)
457+
}
458+
459+
// Record the possible values extracted from the markdown.
460+
semantic.possibleValuesSectionVariants[.fallback] = PropertyListPossibleValuesSection(possibleValues: knownPossibleValues)
461+
} else if let symbolAllowedValues {
462+
// Record the symbol possible values even if none are documented.
463+
semantic.possibleValuesSectionVariants[.fallback] = PropertyListPossibleValuesSection(possibleValues: symbolAllowedValues.value.map {
464+
PropertyListPossibleValuesSection.PossibleValue(value: String($0), contents: [])
465+
})
466+
}
467+
424468
options = documentationExtension?.options[.local]
425469
self.metadata = documentationExtension?.metadata
426470

@@ -670,6 +714,7 @@ public struct DocumentationNode {
670714
returnsSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.returns.isEmpty ? nil : ReturnsSection(content: $0.returns[0].contents) })),
671715
parametersSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.parameters.isEmpty ? nil : ParametersSection(parameters: $0.parameters) })),
672716
dictionaryKeysSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.dictionaryKeys.isEmpty ? nil : DictionaryKeysSection(dictionaryKeys: $0.dictionaryKeys) })),
717+
possibleValuesSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.possibleValues.isEmpty ? nil : PropertyListPossibleValuesSection(possibleValues: $0.possibleValues) })),
673718
httpEndpointSectionVariants: .empty,
674719
httpBodySectionVariants: .empty,
675720
httpParametersSectionVariants: .empty,

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
13641364
HTTPBodySectionTranslator(),
13651365
HTTPResponsesSectionTranslator(),
13661366
PlistDetailsSectionTranslator(),
1367+
PossibleValuesSectionTranslator(),
13671368
DictionaryKeysSectionTranslator(),
13681369
AttributesSectionTranslator(),
13691370
ReturnsSectionTranslator(),
@@ -1886,9 +1887,6 @@ public struct RenderNodeTranslator: SemanticVisitor {
18861887
if let constraint = symbol.maximumExclusive {
18871888
attributes.append(RenderAttribute.maximumExclusive(String(constraint)))
18881889
}
1889-
if let constraint = symbol.allowedValues {
1890-
attributes.append(RenderAttribute.allowedValues(constraint.map{String($0)}))
1891-
}
18921890
if let constraint = symbol.isReadOnly {
18931891
isReadOnly = constraint
18941892
}

Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/AttributesSectionTranslator.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ struct AttributesSectionTranslator: RenderSectionTranslator {
2121
translateSectionToVariantCollection(
2222
documentationDataVariants: symbol.attributesVariants
2323
) { _, attributes in
24-
guard !attributes.isEmpty else { return nil }
2524

2625
func translateFragments(_ fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment]) -> [DeclarationRenderSection.Token] {
2726
return fragments.map { fragment in
@@ -40,7 +39,7 @@ struct AttributesSectionTranslator: RenderSectionTranslator {
4039
}
4140
}
4241

43-
return AttributesRenderSection(
42+
let attributesRenderSection = AttributesRenderSection(
4443
title: "Attributes",
4544
attributes: attributes.compactMap { kind, attribute in
4645

@@ -62,15 +61,17 @@ struct AttributesSectionTranslator: RenderSectionTranslator {
6261
case (.allowedTypes, let types as [SymbolGraph.Symbol.TypeDetail]):
6362
let tokens = types.compactMap { $0.fragments.map(translateFragments) }
6463
return RenderAttribute.allowedTypes(tokens)
65-
case (.allowedValues, let values as [SymbolGraph.AnyScalar]):
66-
let stringValues = values.map { String($0) }
67-
return RenderAttribute.allowedValues(stringValues)
6864
default:
6965
return nil
7066
}
7167

7268
}.sorted { $0.title < $1.title }
7369
)
70+
guard let attributes = attributesRenderSection.attributes, !attributes.isEmpty else {
71+
return nil
72+
}
73+
74+
return attributesRenderSection
7475
}
7576
}
7677

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
import SymbolKit
13+
import Markdown
14+
15+
/// Translates a symbol's possible values into a render nodes's section.
16+
struct PossibleValuesSectionTranslator: RenderSectionTranslator {
17+
18+
func translateSection(for symbol: Symbol, renderNode: inout RenderNode, renderNodeTranslator: inout RenderNodeTranslator) -> VariantCollection<CodableContentSection?>? {
19+
20+
return translateSectionToVariantCollection(
21+
documentationDataVariants: symbol.possibleValuesSectionVariants
22+
) { _, possibleValuesSection in
23+
// Render the possible values with the matching description from the
24+
// possible values listed in the markdown.
25+
return PossibleValuesRenderSection(
26+
title: PropertyListPossibleValuesSection.title,
27+
values: possibleValuesSection.possibleValues.map { possibleValueTag in
28+
let valueContent = renderNodeTranslator.visitMarkupContainer(
29+
MarkupContainer(possibleValueTag.contents)
30+
) as! [RenderBlockContent]
31+
return PossibleValuesRenderSection.NamedValue(
32+
name: possibleValueTag.value,
33+
content: valueContent
34+
)
35+
}
36+
)
37+
}
38+
}
39+
40+
}
41+
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
import Markdown
13+
import SymbolKit
14+
15+
public struct PropertyListPossibleValuesSection {
16+
17+
/// A possible value.
18+
///
19+
/// Documentation about a possible value of a symbol.
20+
/// Write a possible value by prepending a line of prose with "- PossibleValue:" or "- PossibleValues:".
21+
public struct PossibleValue {
22+
/// The string representation of the value.
23+
public var value: String
24+
/// The content that describes the value.
25+
public var contents: [Markup]
26+
/// The text range where the parameter name was parsed.
27+
var nameRange: SourceRange?
28+
/// The text range where this parameter was parsed.
29+
var range: SourceRange?
30+
31+
init(value: String, contents: [Markup], nameRange: SourceRange? = nil, range: SourceRange? = nil) {
32+
self.value = value
33+
self.contents = contents
34+
self.nameRange = nameRange
35+
self.range = range
36+
}
37+
}
38+
39+
public static var title: String {
40+
return "Possible Values"
41+
}
42+
43+
/// The list of possible values.
44+
public let possibleValues: [PossibleValue]
45+
46+
struct Validator {
47+
/// The engine that collects problems encountered while validating the possible values documentation.
48+
var diagnosticEngine: DiagnosticEngine
49+
50+
/// Creates a new problem about documentation for a possible value that's not known to that symbol.
51+
///
52+
/// ## Example
53+
///
54+
/// ```swift
55+
/// /// - PossibleValues:
56+
/// /// - someValue: Some description of this value.
57+
/// /// - anotherValue: Some description of a non-defined value.
58+
/// /// ^~~~~~~~~~~~
59+
/// /// 'anotherValue' is not a known possible value for 'SymbolName'.
60+
/// ```
61+
///
62+
/// - Parameters:
63+
/// - unknownPossibleValue: The authored documentation for the unknown possible value name.
64+
/// - knownPossibleValues: All known possible value names for that symbol.
65+
/// - Returns: A new problem that suggests that the developer removes the documentation for the unknown possible value.
66+
func makeExtraPossibleValueProblem(_ unknownPossibleValue: PossibleValue, knownPossibleValues: Set<String>, symbolName: String) -> Problem {
67+
68+
let source = unknownPossibleValue.range?.source
69+
let summary = """
70+
\(unknownPossibleValue.value.singleQuoted) is not a known possible value for \(symbolName.singleQuoted).
71+
"""
72+
let identifier = "org.swift.docc.DocumentedPossibleValueNotFound"
73+
let solutionSummary = """
74+
Remove \(unknownPossibleValue.value.singleQuoted) possible value documentation or replace it with a known value.
75+
"""
76+
let nearMisses = NearMiss.bestMatches(for: knownPossibleValues, against: unknownPossibleValue.value)
77+
78+
if nearMisses.isEmpty {
79+
// If this possible value doesn't resemble any of this symbols possible values, suggest to remove it.
80+
return Problem(
81+
diagnostic: Diagnostic(source: source, severity: .warning, range: unknownPossibleValue.range, identifier: identifier, summary: summary),
82+
possibleSolutions: [
83+
Solution(
84+
summary: solutionSummary,
85+
replacements: unknownPossibleValue.range.map { [Replacement(range: $0, replacement: "")] } ?? []
86+
)
87+
]
88+
)
89+
}
90+
// Otherwise, suggest to replace the documented possible value name with the one of the similarly named possible values.
91+
return Problem(
92+
diagnostic: Diagnostic(source: source, severity: .warning, range: unknownPossibleValue.nameRange, identifier: identifier, summary: summary),
93+
possibleSolutions: nearMisses.map { candidate in
94+
Solution(
95+
summary: "Replace \(unknownPossibleValue.value.singleQuoted) with \(candidate.singleQuoted)",
96+
replacements: unknownPossibleValue.nameRange.map { [Replacement(range: $0, replacement: candidate)] } ?? []
97+
)
98+
}
99+
)
100+
}
101+
}
102+
103+
}
104+

Sources/SwiftDocC/Semantics/ReferenceResolver.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,13 @@ struct ReferenceResolver: SemanticVisitor {
472472
return HTTPResponsesSection(responses: responses)
473473
}
474474

475+
let possibleValuesVariants = symbol.possibleValuesSectionVariants.map { possibleValuesSection -> PropertyListPossibleValuesSection in
476+
let possibleValues = possibleValuesSection.possibleValues.map {
477+
PropertyListPossibleValuesSection.PossibleValue(value: $0.value, contents: $0.contents.map { visitMarkup($0) }, nameRange: $0.nameRange, range: $0.range)
478+
}
479+
return PropertyListPossibleValuesSection(possibleValues: possibleValues)
480+
}
481+
475482
// It's important to carry over aggregate data like the merged declarations
476483
// or the merged default implementations to the new `Symbol` instance.
477484

@@ -500,6 +507,7 @@ struct ReferenceResolver: SemanticVisitor {
500507
returnsSectionVariants: newReturnsVariants,
501508
parametersSectionVariants: newParametersVariants,
502509
dictionaryKeysSectionVariants: newDictionaryKeysVariants,
510+
possibleValuesSectionVariants: possibleValuesVariants,
503511
httpEndpointSectionVariants: newHTTPEndpointVariants,
504512
httpBodySectionVariants: newHTTPBodyVariants,
505513
httpParametersSectionVariants: newHTTPParametersVariants,

Sources/SwiftDocC/Semantics/Symbol/Symbol.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import SymbolKit
4949
/// - ``returnsSectionVariants``
5050
/// - ``parametersSectionVariants``
5151
/// - ``dictionaryKeysSectionVariants``
52+
/// - ``possibleValuesSectionVariants``
5253
/// - ``httpEndpointSectionVariants``
5354
/// - ``httpParametersSectionVariants``
5455
/// - ``httpResponsesSectionVariants``
@@ -148,7 +149,7 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
148149
/// The symbol's alternate declarations in each language variant the symbol is available in.
149150
public var alternateDeclarationVariants = DocumentationDataVariants<[[PlatformName?]: [SymbolGraph.Symbol.DeclarationFragments]]>()
150151

151-
/// The symbol's possible values in each language variant the symbol is available in.
152+
/// The symbol's set of attributes in each language variant the symbol is available in.
152153
public var attributesVariants = DocumentationDataVariants<[RenderAttribute.Kind: Any]>()
153154

154155
public var locationVariants = DocumentationDataVariants<SymbolGraph.Symbol.Location>()
@@ -204,6 +205,9 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
204205

205206
/// Any dictionary keys of the symbol, if the symbol accepts keys, in each language variant the symbol is available in.
206207
public var dictionaryKeysSectionVariants: DocumentationDataVariants<DictionaryKeysSection>
208+
209+
/// The symbol's possible values in each language variant the symbol is available in.
210+
public var possibleValuesSectionVariants: DocumentationDataVariants<PropertyListPossibleValuesSection>
207211

208212
/// The HTTP endpoint of an HTTP request, in each language variant the symbol is available in.
209213
public var httpEndpointSectionVariants: DocumentationDataVariants<HTTPEndpointSection>
@@ -275,6 +279,7 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
275279
returnsSectionVariants: DocumentationDataVariants<ReturnsSection>,
276280
parametersSectionVariants: DocumentationDataVariants<ParametersSection>,
277281
dictionaryKeysSectionVariants: DocumentationDataVariants<DictionaryKeysSection>,
282+
possibleValuesSectionVariants: DocumentationDataVariants<PropertyListPossibleValuesSection>,
278283
httpEndpointSectionVariants: DocumentationDataVariants<HTTPEndpointSection>,
279284
httpBodySectionVariants: DocumentationDataVariants<HTTPBodySection>,
280285
httpParametersSectionVariants: DocumentationDataVariants<HTTPParametersSection>,
@@ -304,6 +309,7 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
304309

305310
self.deprecatedSummaryVariants = deprecatedSummaryVariants
306311
self.declarationVariants = declarationVariants
312+
self.possibleValuesSectionVariants = possibleValuesSectionVariants
307313
self.alternateDeclarationVariants = alternateDeclarationVariants
308314

309315
self.mixinsVariants = mixinsVariants
@@ -343,8 +349,6 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
343349

344350
case let attribute as SymbolGraph.Symbol.TypeDetails:
345351
attributes[.allowedTypes] = attribute.value
346-
case let attribute as SymbolGraph.Symbol.AllowedValues:
347-
attributes[.allowedValues] = attribute.value
348352
default: break;
349353
}
350354
}

0 commit comments

Comments
 (0)