Skip to content

Commit

Permalink
Provide ConvertService with mapping of USRs to minimal access level r…
Browse files Browse the repository at this point in the history
…equired for extended documentation to be available

rdar://105460209
  • Loading branch information
daniel-grumberg committed Apr 25, 2023
1 parent 88d0fd6 commit 06cb3d9
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class DocumentationContextConverter {
/// Whether the documentation converter should include access level information for symbols.
let shouldEmitSymbolAccessLevels: Bool

let symbolIdentifiersWithExpandedDocumentation: [String]?

/// The remote source control repository where the documented module's source is hosted.
let sourceRepository: SourceRepository?

Expand All @@ -61,14 +63,16 @@ public class DocumentationContextConverter {
renderContext: RenderContext,
emitSymbolSourceFileURIs: Bool = false,
emitSymbolAccessLevels: Bool = false,
sourceRepository: SourceRepository? = nil
sourceRepository: SourceRepository? = nil,
symbolIdentifiersWithExpandedDocumentation: [String]? = nil
) {
self.bundle = bundle
self.context = context
self.renderContext = renderContext
self.shouldEmitSymbolSourceFileURIs = emitSymbolSourceFileURIs
self.shouldEmitSymbolAccessLevels = emitSymbolAccessLevels
self.sourceRepository = sourceRepository
self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation
}

/// Converts a documentation node to a render node.
Expand All @@ -91,7 +95,8 @@ public class DocumentationContextConverter {
renderContext: renderContext,
emitSymbolSourceFileURIs: shouldEmitSymbolSourceFileURIs,
emitSymbolAccessLevels: shouldEmitSymbolAccessLevels,
sourceRepository: sourceRepository
sourceRepository: sourceRepository,
symbolIdentifiersWithExpandedDocumentation: symbolIdentifiersWithExpandedDocumentation
)
return translator.visit(node.semantic) as? RenderNode
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ public struct ConvertService: DocumentationService {
additionalSymbolGraphFiles: []
),
emitSymbolSourceFileURIs: request.emitSymbolSourceFileURIs,
emitSymbolAccessLevels: true
emitSymbolAccessLevels: true,
symbolIdentifiersWithExpandedDocumentation: request.symbolIdentifiersWithExpandedDocumentation
)

// Run the conversion.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import SymbolKit
import Foundation

/// A request to convert in-memory documentation.
Expand Down Expand Up @@ -130,6 +129,13 @@ public struct ConvertRequest: Codable {
/// - ``DocumentationBundle/miscResourceURLs``
public var miscResourceURLs: [URL]

/// The symbol identifiers that have an expanded documentation page available if they meet the associated access level requirement.
///
/// DocC sets the ``RenderMetadata/hasExpandedDocumentationForSymbols`` property to `true`
/// for these symbols if they meet the provided requirements, so that renderers can display a "View More" link
/// that navigates the user to the full version of the documentation page.
public var symbolIdentifiersWithExpandedDocumentation: [String: ExpandedDocumentationRequirements]?

/// The default code listing language for the documentation bundle to convert.
///
/// ## See Also
Expand Down Expand Up @@ -177,6 +183,8 @@ public struct ConvertRequest: Codable {
version: version,
defaultCodeListingLanguage: defaultCodeListingLanguage
)

self.symbolIdentifiersWithExpandedDocumentation = nil
}

/// Creates a request to convert in-memory documentation.
Expand All @@ -195,6 +203,8 @@ public struct ConvertRequest: Codable {
/// - markupFiles: The article and documentation extension file data included in the documentation bundle to convert.
/// - tutorialFiles: The tutorial file data included in the documentation bundle to convert.
/// - miscResourceURLs: The on-disk resources in the documentation bundle to convert.
/// - symbolIdentifiersWithExpandedDocumentation: A dictionary of identifiers to requirements for these symbols to have expanded
/// documentation available.
public init(
bundleInfo: DocumentationBundle.Info,
featureFlags: FeatureFlags = FeatureFlags(),
Expand All @@ -208,7 +218,8 @@ public struct ConvertRequest: Codable {
emitSymbolSourceFileURIs: Bool = true,
markupFiles: [Data],
tutorialFiles: [Data] = [],
miscResourceURLs: [URL]
miscResourceURLs: [URL],
symbolIdentifiersWithExpandedDocumentation: [String: ExpandedDocumentationRequirements]? = nil
) {
self.externalIDsToConvert = externalIDsToConvert
self.documentPathsToConvert = documentPathsToConvert
Expand All @@ -229,6 +240,7 @@ public struct ConvertRequest: Codable {
self.miscResourceURLs = miscResourceURLs
self.bundleInfo = bundleInfo
self.featureFlags = featureFlags
self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation
}
}

Expand Down Expand Up @@ -288,4 +300,12 @@ extension ConvertRequest {
self.character = character
}
}

public struct ExpandedDocumentationRequirements: Codable {
public let accessControlLevel: AccessControlLevel?

public init(accessControlLevel: AccessControlLevel?) {
self.accessControlLevel = accessControlLevel
}
}
}
32 changes: 30 additions & 2 deletions Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
/// The source repository where the documentation's sources are hosted.
var sourceRepository: SourceRepository?

/// The identifiers and access level requirements for symbols that have an expanded version of their documentation page if the requirements are met
var symbolIdentifiersWithExpandedDocumentation: [String: ConvertRequest.ExpandedDocumentationRequirements]? = nil

/// `true` if the conversion is cancelled.
private var isCancelled: Synchronized<Bool>? = nil

Expand All @@ -118,6 +121,8 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
/// Before passing `true` please confirm that your use case doesn't include public
/// distribution of any created render nodes as there are filesystem privacy and security
/// concerns with distributing this data.
/// - Parameter symbolIdentifiersWithExpandedDocumentation: Identifiers and access level requirements for symbols
/// that have an expanded version of their documentation page if the access level requirement is met.
public init(
documentationBundleURL: URL?,
emitDigest: Bool,
Expand All @@ -133,7 +138,8 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
emitSymbolAccessLevels: Bool = false,
sourceRepository: SourceRepository? = nil,
isCancelled: Synchronized<Bool>? = nil,
diagnosticEngine: DiagnosticEngine = .init()
diagnosticEngine: DiagnosticEngine = .init(),
symbolIdentifiersWithExpandedDocumentation: [String: ConvertRequest.ExpandedDocumentationRequirements]? = nil
) {
self.rootURL = documentationBundleURL
self.emitDigest = emitDigest
Expand All @@ -149,6 +155,7 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
self.sourceRepository = sourceRepository
self.isCancelled = isCancelled
self.diagnosticEngine = diagnosticEngine
self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation

// Inject current platform versions if provided
if let currentPlatforms = currentPlatforms {
Expand Down Expand Up @@ -250,13 +257,22 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
// Copy images, sample files, and other static assets.
try outputConsumer.consume(assetsInBundle: bundle)

let symbolIdentifiersMeetingRequirementsForExpandedDocumentation: [String]? = symbolIdentifiersWithExpandedDocumentation?.compactMap { (identifier, expandedDocsRequirement) -> String? in
guard let documentationNode = context.nodeWithSymbolIdentifier(identifier) else {
return nil
}

return documentationNode.meetsExpandedDocumentationRequirements(expandedDocsRequirement) ? identifier : nil
}

let converter = DocumentationContextConverter(
bundle: bundle,
context: context,
renderContext: renderContext,
emitSymbolSourceFileURIs: shouldEmitSymbolSourceFileURIs,
emitSymbolAccessLevels: shouldEmitSymbolAccessLevels,
sourceRepository: sourceRepository
sourceRepository: sourceRepository,
symbolIdentifiersWithExpandedDocumentation: symbolIdentifiersMeetingRequirementsForExpandedDocumentation
)

var indexingRecords = [IndexingRecord]()
Expand Down Expand Up @@ -455,3 +471,15 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
}
}
}

extension DocumentationNode {
func meetsExpandedDocumentationRequirements(_ requirements: ConvertRequest.ExpandedDocumentationRequirements) -> Bool {
// If we don't know the required access control level then any level is acceptable.
guard let requiredAccessControlLevel = requirements.accessControlLevel else { return true }

// If the symbol does not expose it's access control level let's assume it's most restrictive.
guard let symbolAccessLevel = symbol?.accessLevel else { return false }

return AccessControlLevel(symbolAccessLevel.rawValue) >= requiredAccessControlLevel
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,14 @@

import SymbolKit

extension SymbolGraph.Symbol.AccessControl: Comparable {
private var level: Int? {
switch self {
case .private:
return 0
case .filePrivate:
return 1
case .internal:
return 2
case .public:
return 3
case .open:
return 4
default:
assertionFailure("Unknown AccessControl case was used in comparison.")
return nil
}
extension AccessControlLevel {
init(from symbolKitAccessControlLevel: SymbolGraph.Symbol.AccessControl) {
self.init(symbolKitAccessControlLevel.rawValue)
}

}

extension SymbolGraph.Symbol.AccessControl: Comparable {
public static func < (lhs: SymbolGraph.Symbol.AccessControl, rhs: SymbolGraph.Symbol.AccessControl) -> Bool {
guard let lhs = lhs.level,
let rhs = rhs.level else {
return false
}

return lhs < rhs
return AccessControlLevel(from: lhs) < AccessControlLevel(from: rhs)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ public struct RenderMetadata: VariantContainer {

/// Any tags assigned to the node.
public var tags: [RenderNode.Tag]?

/// Whether there isn't a version of the page with more content that a renderer can link to.
///
/// This property indicates to renderers that an expanded version of the page does not exist for this render node,
/// which, for example, controls whether a 'View More' link should be displayed or not.
///
/// It's the renderer's responsibility to fetch the full version of the page, for example using
/// the ``RenderNode/variants`` property.
public var hasNoExpandedDocumentation: Bool = false
}

extension RenderMetadata: Codable {
Expand Down Expand Up @@ -238,6 +247,7 @@ extension RenderMetadata: Codable {
public static let images = CodingKeys(stringValue: "images")
public static let color = CodingKeys(stringValue: "color")
public static let customMetadata = CodingKeys(stringValue: "customMetadata")
public static let hasNoExpandedDocumentation = CodingKeys(stringValue: "hasNoExpandedDocumentation")
}

public init(from decoder: Decoder) throws {
Expand Down Expand Up @@ -267,6 +277,7 @@ extension RenderMetadata: Codable {
sourceFileURIVariants = try container.decodeVariantCollectionIfPresent(ofValueType: String?.self, forKey: .sourceFileURI)
remoteSourceVariants = try container.decodeVariantCollectionIfPresent(ofValueType: RemoteSource?.self, forKey: .remoteSource)
tags = try container.decodeIfPresent([RenderNode.Tag].self, forKey: .tags)
hasNoExpandedDocumentation = try container.decodeIfPresent(Bool.self, forKey: .hasNoExpandedDocumentation) ?? false

let extraKeys = Set(container.allKeys).subtracting(
[
Expand All @@ -288,7 +299,8 @@ extension RenderMetadata: Codable {
.navigatorTitle,
.sourceFileURI,
.remoteSource,
.tags
.tags,
.hasNoExpandedDocumentation,
]
)
for extraKey in extraKeys {
Expand Down Expand Up @@ -330,5 +342,6 @@ extension RenderMetadata: Codable {
try container.encodeIfNotEmpty(images, forKey: .images)
try container.encodeIfPresent(color, forKey: .color)
try container.encodeIfNotEmpty(customMetadata, forKey: .customMetadata)
try container.encodeIfTrue(hasNoExpandedDocumentation, forKey: .hasNoExpandedDocumentation)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,11 @@ extension KeyedEncodingContainer {
try encode(value, forKey: key)
}
}

/// Encodes the given boolean if its value is true.
mutating func encodeIfTrue(_ value: Bool, forKey key: Key) throws {
if value {
try encode(value, forKey: key)
}
}
}
15 changes: 12 additions & 3 deletions Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public struct RenderNodeTranslator: SemanticVisitor {
/// The source repository where the documentation's sources are hosted.
var sourceRepository: SourceRepository?

var symbolIdentifiersWithExpandedDocumentation: [String]? = nil

public mutating func visitCode(_ code: Code) -> RenderTree? {
let fileType = NSString(string: code.fileName).pathExtension
let fileReference = code.fileReference
Expand Down Expand Up @@ -1179,8 +1181,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
} else if let extendedModule = symbol.extendedModule, extendedModule != moduleName.displayName {
node.metadata.modulesVariants = VariantCollection(defaultValue: [RenderMetadata.Module(name: moduleName.displayName, relatedModules: [extendedModule])])
} else {
node.metadata.modulesVariants = VariantCollection(defaultValue: [RenderMetadata.Module(name: moduleName.displayName, relatedModules: nil)]
)
node.metadata.modulesVariants = VariantCollection(defaultValue: [RenderMetadata.Module(name: moduleName.displayName, relatedModules: nil)])
}

node.metadata.extendedModuleVariants = VariantCollection<String?>(defaultValue: symbol.extendedModule)
Expand Down Expand Up @@ -1355,6 +1356,12 @@ public struct RenderNodeTranslator: SemanticVisitor {
node.metadata.symbolAccessLevelVariants = VariantCollection<String?>(from: symbol.accessLevelVariants)
}

if let externalID = symbol.externalID,
let symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation
{
node.metadata.hasNoExpandedDocumentation = !symbolIdentifiersWithExpandedDocumentation.contains(externalID)
}

node.relationshipSectionsVariants = VariantCollection<[RelationshipsRenderSection]>(
from: documentationNode.availableVariantTraits,
fallbackDefaultValue: []
Expand Down Expand Up @@ -1878,7 +1885,8 @@ public struct RenderNodeTranslator: SemanticVisitor {
renderContext: RenderContext? = nil,
emitSymbolSourceFileURIs: Bool = false,
emitSymbolAccessLevels: Bool = false,
sourceRepository: SourceRepository? = nil
sourceRepository: SourceRepository? = nil,
symbolIdentifiersWithExpandedDocumentation: [String]? = nil
) {
self.context = context
self.bundle = bundle
Expand All @@ -1889,6 +1897,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
self.shouldEmitSymbolSourceFileURIs = emitSymbolSourceFileURIs
self.shouldEmitSymbolAccessLevels = emitSymbolAccessLevels
self.sourceRepository = sourceRepository
self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation
}
}

Expand Down
53 changes: 53 additions & 0 deletions Sources/SwiftDocC/Utility/AccessControlLevel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2023 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

/// Defines semantic access control levels for symbols
public struct AccessControlLevel: CustomStringConvertible, Codable {
private let rawValue: String

public var description: String { rawValue }

public init(_ accessLevel: String) { self.rawValue = accessLevel }

/// Represents private access level, which normally indicates the symbol is only available in the scope in which it's defined.
public static let `private` = Self.init("private")
/// Represents fileprivate access level, which normally indicates the symbol is only available in the file that defines it.
public static let `fileprivate` = Self.init("fileprivate")
/// Represents internal access level, which normally indicates the symbol is only available in the module that defines it.
public static let `internal` = Self.init("internal")
/// Represents public access level, which normally indicates the symbol is available to clients.
public static let `public` = Self.init("public")
/// Represents open access level, which normally indicates the symbol is extensible and overridable by clients.
public static let `open` = Self.init("open")
}

extension AccessControlLevel: Comparable {
private var level: UInt? {
switch self {
case .private : return 1
case .fileprivate: return 2
case .internal: return 3
case .public: return 4
case .open: return 5
default:
assertionFailure("Unknown access control level was (with representation \(description)) used in comparison")
return nil
}
}

/// Compares the restrictiveness of access control levels.
public static func < (lhs: Self, rhs: Self) -> Bool {
let lhsLevel = lhs.level ?? .min
let rhsLevel = rhs.level ?? .min
return lhsLevel < rhsLevel
}
}
Loading

0 comments on commit 06cb3d9

Please sign in to comment.