Skip to content

Commit

Permalink
Support Possible Values section (#1006)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sofiaromorales authored Aug 29, 2024
1 parent 1b6e17b commit a08f470
Show file tree
Hide file tree
Showing 13 changed files with 542 additions and 16 deletions.
45 changes: 45 additions & 0 deletions Sources/SwiftDocC/Model/DocumentationNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ public struct DocumentationNode {
returnsSectionVariants: .empty,
parametersSectionVariants: .empty,
dictionaryKeysSectionVariants: .empty,
possibleValuesSectionVariants: .empty,
httpEndpointSectionVariants: endpointVariants,
httpBodySectionVariants: .empty,
httpParametersSectionVariants: .empty,
Expand Down Expand Up @@ -421,6 +422,49 @@ public struct DocumentationNode {
semantic.httpResponsesSectionVariants[.fallback] = HTTPResponsesSection(responses: responses)
}

// The property list symbol's allowed values.
let symbolAllowedValues = symbol![mixin: SymbolGraph.Symbol.AllowedValues.self]

if let possibleValues = markupModel.discussionTags?.possibleValues, !possibleValues.isEmpty {
let validator = PropertyListPossibleValuesSection.Validator(diagnosticEngine: engine)
guard let symbolAllowedValues else {
possibleValues.forEach {
engine.emit(validator.makeExtraPossibleValueProblem($0, knownPossibleValues: [], symbolName: self.name.plainText))
}
return
}

// Ignore documented possible values that don't exist in the symbol's allowed values in the symbol graph.
let allowedPossibleValueNames = Set(symbolAllowedValues.value.map { String($0) })
var (knownPossibleValues, unknownPossibleValues) = possibleValues.categorize(where: {
allowedPossibleValueNames.contains($0.value)
})

// Add the symbol possible values that are not documented.
let knownPossibleValueNames = Set(knownPossibleValues.map(\.value))
knownPossibleValues.append(contentsOf: symbolAllowedValues.value.compactMap { possibleValue in
let possibleValueString = String(possibleValue)
guard !knownPossibleValueNames.contains(possibleValueString) else {
return nil
}
return PropertyListPossibleValuesSection.PossibleValue(value: possibleValueString, contents: [])
})

for unknownValue in unknownPossibleValues {
engine.emit(
validator.makeExtraPossibleValueProblem(unknownValue, knownPossibleValues: knownPossibleValueNames, symbolName: self.name.plainText)
)
}

// Record the possible values extracted from the markdown.
semantic.possibleValuesSectionVariants[.fallback] = PropertyListPossibleValuesSection(possibleValues: knownPossibleValues)
} else if let symbolAllowedValues {
// Record the symbol possible values even if none are documented.
semantic.possibleValuesSectionVariants[.fallback] = PropertyListPossibleValuesSection(possibleValues: symbolAllowedValues.value.map {
PropertyListPossibleValuesSection.PossibleValue(value: String($0), contents: [])
})
}

options = documentationExtension?.options[.local]
self.metadata = documentationExtension?.metadata

Expand Down Expand Up @@ -670,6 +714,7 @@ public struct DocumentationNode {
returnsSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.returns.isEmpty ? nil : ReturnsSection(content: $0.returns[0].contents) })),
parametersSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.parameters.isEmpty ? nil : ParametersSection(parameters: $0.parameters) })),
dictionaryKeysSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.dictionaryKeys.isEmpty ? nil : DictionaryKeysSection(dictionaryKeys: $0.dictionaryKeys) })),
possibleValuesSectionVariants: .init(swiftVariant: markupModel.discussionTags.flatMap({ $0.possibleValues.isEmpty ? nil : PropertyListPossibleValuesSection(possibleValues: $0.possibleValues) })),
httpEndpointSectionVariants: .empty,
httpBodySectionVariants: .empty,
httpParametersSectionVariants: .empty,
Expand Down
4 changes: 1 addition & 3 deletions Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
HTTPBodySectionTranslator(),
HTTPResponsesSectionTranslator(),
PlistDetailsSectionTranslator(),
PossibleValuesSectionTranslator(),
DictionaryKeysSectionTranslator(),
AttributesSectionTranslator(),
ReturnsSectionTranslator(),
Expand Down Expand Up @@ -1886,9 +1887,6 @@ public struct RenderNodeTranslator: SemanticVisitor {
if let constraint = symbol.maximumExclusive {
attributes.append(RenderAttribute.maximumExclusive(String(constraint)))
}
if let constraint = symbol.allowedValues {
attributes.append(RenderAttribute.allowedValues(constraint.map{String($0)}))
}
if let constraint = symbol.isReadOnly {
isReadOnly = constraint
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ struct AttributesSectionTranslator: RenderSectionTranslator {
translateSectionToVariantCollection(
documentationDataVariants: symbol.attributesVariants
) { _, attributes in
guard !attributes.isEmpty else { return nil }

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

return AttributesRenderSection(
let attributesRenderSection = AttributesRenderSection(
title: "Attributes",
attributes: attributes.compactMap { kind, attribute in

Expand All @@ -62,15 +61,17 @@ struct AttributesSectionTranslator: RenderSectionTranslator {
case (.allowedTypes, let types as [SymbolGraph.Symbol.TypeDetail]):
let tokens = types.compactMap { $0.fragments.map(translateFragments) }
return RenderAttribute.allowedTypes(tokens)
case (.allowedValues, let values as [SymbolGraph.AnyScalar]):
let stringValues = values.map { String($0) }
return RenderAttribute.allowedValues(stringValues)
default:
return nil
}

}.sorted { $0.title < $1.title }
)
guard let attributes = attributesRenderSection.attributes, !attributes.isEmpty else {
return nil
}

return attributesRenderSection
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation
import SymbolKit
import Markdown

/// Translates a symbol's possible values into a render nodes's section.
struct PossibleValuesSectionTranslator: RenderSectionTranslator {

func translateSection(for symbol: Symbol, renderNode: inout RenderNode, renderNodeTranslator: inout RenderNodeTranslator) -> VariantCollection<CodableContentSection?>? {

return translateSectionToVariantCollection(
documentationDataVariants: symbol.possibleValuesSectionVariants
) { _, possibleValuesSection in
// Render the possible values with the matching description from the
// possible values listed in the markdown.
return PossibleValuesRenderSection(
title: PropertyListPossibleValuesSection.title,
values: possibleValuesSection.possibleValues.map { possibleValueTag in
let valueContent = renderNodeTranslator.visitMarkupContainer(
MarkupContainer(possibleValueTag.contents)
) as! [RenderBlockContent]
return PossibleValuesRenderSection.NamedValue(
name: possibleValueTag.value,
content: valueContent
)
}
)
}
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation
import Markdown
import SymbolKit

public struct PropertyListPossibleValuesSection {

/// A possible value.
///
/// Documentation about a possible value of a symbol.
/// Write a possible value by prepending a line of prose with "- PossibleValue:" or "- PossibleValues:".
public struct PossibleValue {
/// The string representation of the value.
public var value: String
/// The content that describes the value.
public var contents: [Markup]
/// The text range where the parameter name was parsed.
var nameRange: SourceRange?
/// The text range where this parameter was parsed.
var range: SourceRange?

init(value: String, contents: [Markup], nameRange: SourceRange? = nil, range: SourceRange? = nil) {
self.value = value
self.contents = contents
self.nameRange = nameRange
self.range = range
}
}

public static var title: String {
return "Possible Values"
}

/// The list of possible values.
public let possibleValues: [PossibleValue]

struct Validator {
/// The engine that collects problems encountered while validating the possible values documentation.
var diagnosticEngine: DiagnosticEngine

/// Creates a new problem about documentation for a possible value that's not known to that symbol.
///
/// ## Example
///
/// ```swift
/// /// - PossibleValues:
/// /// - someValue: Some description of this value.
/// /// - anotherValue: Some description of a non-defined value.
/// /// ^~~~~~~~~~~~
/// /// 'anotherValue' is not a known possible value for 'SymbolName'.
/// ```
///
/// - Parameters:
/// - unknownPossibleValue: The authored documentation for the unknown possible value name.
/// - knownPossibleValues: All known possible value names for that symbol.
/// - Returns: A new problem that suggests that the developer removes the documentation for the unknown possible value.
func makeExtraPossibleValueProblem(_ unknownPossibleValue: PossibleValue, knownPossibleValues: Set<String>, symbolName: String) -> Problem {

let source = unknownPossibleValue.range?.source
let summary = """
\(unknownPossibleValue.value.singleQuoted) is not a known possible value for \(symbolName.singleQuoted).
"""
let identifier = "org.swift.docc.DocumentedPossibleValueNotFound"
let solutionSummary = """
Remove \(unknownPossibleValue.value.singleQuoted) possible value documentation or replace it with a known value.
"""
let nearMisses = NearMiss.bestMatches(for: knownPossibleValues, against: unknownPossibleValue.value)

if nearMisses.isEmpty {
// If this possible value doesn't resemble any of this symbols possible values, suggest to remove it.
return Problem(
diagnostic: Diagnostic(source: source, severity: .warning, range: unknownPossibleValue.range, identifier: identifier, summary: summary),
possibleSolutions: [
Solution(
summary: solutionSummary,
replacements: unknownPossibleValue.range.map { [Replacement(range: $0, replacement: "")] } ?? []
)
]
)
}
// Otherwise, suggest to replace the documented possible value name with the one of the similarly named possible values.
return Problem(
diagnostic: Diagnostic(source: source, severity: .warning, range: unknownPossibleValue.nameRange, identifier: identifier, summary: summary),
possibleSolutions: nearMisses.map { candidate in
Solution(
summary: "Replace \(unknownPossibleValue.value.singleQuoted) with \(candidate.singleQuoted)",
replacements: unknownPossibleValue.nameRange.map { [Replacement(range: $0, replacement: candidate)] } ?? []
)
}
)
}
}

}

8 changes: 8 additions & 0 deletions Sources/SwiftDocC/Semantics/ReferenceResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,13 @@ struct ReferenceResolver: SemanticVisitor {
return HTTPResponsesSection(responses: responses)
}

let possibleValuesVariants = symbol.possibleValuesSectionVariants.map { possibleValuesSection -> PropertyListPossibleValuesSection in
let possibleValues = possibleValuesSection.possibleValues.map {
PropertyListPossibleValuesSection.PossibleValue(value: $0.value, contents: $0.contents.map { visitMarkup($0) }, nameRange: $0.nameRange, range: $0.range)
}
return PropertyListPossibleValuesSection(possibleValues: possibleValues)
}

// It's important to carry over aggregate data like the merged declarations
// or the merged default implementations to the new `Symbol` instance.

Expand Down Expand Up @@ -500,6 +507,7 @@ struct ReferenceResolver: SemanticVisitor {
returnsSectionVariants: newReturnsVariants,
parametersSectionVariants: newParametersVariants,
dictionaryKeysSectionVariants: newDictionaryKeysVariants,
possibleValuesSectionVariants: possibleValuesVariants,
httpEndpointSectionVariants: newHTTPEndpointVariants,
httpBodySectionVariants: newHTTPBodyVariants,
httpParametersSectionVariants: newHTTPParametersVariants,
Expand Down
10 changes: 7 additions & 3 deletions Sources/SwiftDocC/Semantics/Symbol/Symbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import SymbolKit
/// - ``returnsSectionVariants``
/// - ``parametersSectionVariants``
/// - ``dictionaryKeysSectionVariants``
/// - ``possibleValuesSectionVariants``
/// - ``httpEndpointSectionVariants``
/// - ``httpParametersSectionVariants``
/// - ``httpResponsesSectionVariants``
Expand Down Expand Up @@ -148,7 +149,7 @@ public final class Symbol: Semantic, Abstracted, Redirected, AutomaticTaskGroups
/// The symbol's alternate declarations in each language variant the symbol is available in.
public var alternateDeclarationVariants = DocumentationDataVariants<[[PlatformName?]: [SymbolGraph.Symbol.DeclarationFragments]]>()

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

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

/// Any dictionary keys of the symbol, if the symbol accepts keys, in each language variant the symbol is available in.
public var dictionaryKeysSectionVariants: DocumentationDataVariants<DictionaryKeysSection>

/// The symbol's possible values in each language variant the symbol is available in.
public var possibleValuesSectionVariants: DocumentationDataVariants<PropertyListPossibleValuesSection>

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

self.deprecatedSummaryVariants = deprecatedSummaryVariants
self.declarationVariants = declarationVariants
self.possibleValuesSectionVariants = possibleValuesSectionVariants
self.alternateDeclarationVariants = alternateDeclarationVariants

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

case let attribute as SymbolGraph.Symbol.TypeDetails:
attributes[.allowedTypes] = attribute.value
case let attribute as SymbolGraph.Symbol.AllowedValues:
attributes[.allowedValues] = attribute.value
default: break;
}
}
Expand Down
Loading

0 comments on commit a08f470

Please sign in to comment.