Skip to content

Format Swift symbol declarations using swift-format #1048

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@
"revision" : "4c245d4b7264fbabb0fa1f7b3411c2c5bce4e2d9"
}
},
{
"identity" : "swift-format",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-format",
"state" : {
"revision" : "93ebb779c07dad2598919de8202d6df1f97189d4",
"version" : "601.0.0-prerelease-2024-10-01"
}
},
{
"identity" : "swift-lmdb",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -90,6 +99,15 @@
"version" : "2.68.0"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax.git",
"state" : {
"revision" : "f4acb89d4a542c3ba54cadcf17f01c857dda309c",
"version" : "601.0.0-prerelease-2024-09-30"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
Expand Down
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ let swiftSettings: [SwiftSetting] = [
let package = Package(
name: "SwiftDocC",
platforms: [
.macOS(.v10_15),
.macOS(.v12), // TODO: see if this can be configured back to 10.15
.iOS(.v13)
],
products: [
Expand All @@ -45,6 +45,8 @@ let package = Package(
.product(name: "SymbolKit", package: "swift-docc-symbolkit"),
.product(name: "CLMDB", package: "swift-lmdb"),
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "SwiftFormat", package: "swift-format"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
],
swiftSettings: swiftSettings
),
Expand Down Expand Up @@ -140,6 +142,8 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
.package(url: "https://github.com/apple/swift-docc-symbolkit", branch: "main"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "2.5.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.2.0"),
.package(url: "https://github.com/swiftlang/swift-format", from: "601.0.0-prerelease-2024-10-01"),
.package(url: "https://github.com/swiftlang/swift-syntax", from: "601.0.0-prerelease-2024-09-30"),
]
} else {
// Building in the Swift.org CI system, so rely on local versions of dependencies.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
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 SymbolKit

// All logic related to taking a list of fragments from a symbolgraph symbol
// and turning it into a similar list with additional whitespace formatting.
//
// The basic logic is as follows:
//
// 1. Extract the text from the fragment list and use `SwiftFormat` to output
// a String of formatted source code
// (see `Utility/Formatting/SyntaxFormatter.swift`)
// 2. Given the String of formatted source code from step 1, use
// `FragmentBuilder` to turn that text back into a new list of fragments
// (see `Utility/Formatting/FragmentBuilder.swift`)
//
// ```
// [Fragment] -> String -> [Fragment]
// ```
extension DeclarationsSectionTranslator {
typealias DeclarationFragments = SymbolGraph.Symbol.DeclarationFragments
typealias Fragment = DeclarationFragments.Fragment

func formatted(declarations: [[PlatformName?]:DeclarationFragments])
-> [[PlatformName?]:DeclarationFragments] {
declarations.mapValues { formatted(declaration: $0) }
}

func formatted(declaration: DeclarationFragments) -> DeclarationFragments {
let formattedFragments = formatted(fragments: declaration.declarationFragments)
return DeclarationFragments(declarationFragments: formattedFragments)
}

/// Returns an array of `Fragment` elements with additional whitespace
/// formatting text, like indentation and newlines.
///
/// - Parameter fragments: An array of `Fragment` elements for a declaration
/// provided by `SymbolKit`.
///
/// - Returns: A new array of `Fragment` elements with the same source code
/// text as the input fragments and also including some additional text
/// for indentation and splitting the code across multiple lines.
func formatted(fragments: [Fragment]) -> [Fragment] {
do {
let ids = createIdentifierMap(fragments)
let rawText = extractText(from: fragments)
let formattedText = try format(source: rawText)
let formattedFragments = buildFragments(from: formattedText, identifiedBy: ids)

return formattedFragments
} catch {
// if there's an error that happens when using swift-format, ignore
// it and simply return back the original, unformatted fragments
return fragments
}
}

private func createIdentifierMap(_ fragments: [Fragment]) -> [String:String] {
var map: [String:String] = [:]

for fragment in fragments {
if let id = fragment.preciseIdentifier {
map[fragment.spelling] = id
}
}

return map
}

private func extractText(from fragments: [Fragment]) -> String {
fragments.reduce("") { "\($0)\($1.spelling)" }
}

private func format(source: String) throws -> String {
try SyntaxFormatter().format(source: source)
}

private func buildFragments(
from source: String,
identifiedBy ids: [String:String] = [:]
) -> [Fragment] {
FragmentBuilder().buildFragments(from: source, identifiers: ids)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator {
return nil
}

// if --enable-experimental-declaration-formatting is enabled, try
// to format the decl fragments using swift-format
// (see `DeclarationSectionTranslator+Formatting.swift`)
let declaration = FeatureFlags.current.isExperimentalDeclarationFormattingEnabled ? (
formatted(declarations: declaration)
) : (
declaration
)

/// Convert a ``SymbolGraph`` declaration fragment into a ``DeclarationRenderSection/Token``
/// by resolving any symbol USRs to the appropriate reference link.
func translateFragment(
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftDocC/Utility/FeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public struct FeatureFlags: Codable {
get { isParametersAndReturnsValidationEnabled }
set { isParametersAndReturnsValidationEnabled = newValue }
}

/// Whether or not experimental support for formatting Swift symbol
/// declarations using swift-format is enabled.
public var isExperimentalDeclarationFormattingEnabled = false

/// Creates a set of feature flags with the given values.
///
Expand Down
215 changes: 215 additions & 0 deletions Sources/SwiftDocC/Utility/Formatting/FragmentBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
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 SwiftParser
import SwiftSyntax
import SymbolKit

extension SymbolGraph.Symbol.DeclarationFragments.Fragment {
init(
spelling: String,
kind: Kind = .text,
preciseIdentifier: String? = nil
) {
self.init(
kind: kind,
spelling: spelling,
preciseIdentifier: preciseIdentifier
)
}
}

extension Trivia {
var text: String? {
guard pieces.count > 0 else {
return nil
}

var string: String = ""
write(to: &string)
return string
}
}

extension TokenSyntax {
var leadingText: String? {
leadingTrivia.text
}

var trailingText: String? {
trailingTrivia.text
}
}

/// A subclass of `SwiftSyntax.SyntaxVisitor` which can traverse a syntax tree
/// and build up a simpler, flat `Fragment` array representing it.
///
/// The main job of this class is to help convert a formatted string for a Swift
/// symbol declaration back into a list of fragments that closely resemble how
/// the same code would be presented in a `SymbolKit` symbol graph.
final class FragmentBuilder: SyntaxVisitor {
typealias Fragment = SymbolGraph.Symbol.DeclarationFragments.Fragment

private var identifiers: [String:String]
private var fragments: [Fragment]

init() {
identifiers = [:]
fragments = []
super.init(viewMode: .sourceAccurate)
}

/// Returns an array of `Fragment` elements that represents the given String
/// of Swift source code.
///
/// - Parameter source: A string of Swift source code.
/// - Parameter identifiers: A lookup table of symbol names and precise
/// identifiers to map them to.
///
/// - Returns: An array of `Fragment` elements.
func buildFragments(
from source: String,
identifiers: [String:String] = [:]
) -> [Fragment] {
let syntax = Parser.parse(source: source)
return buildFragments(from: syntax, identifiers: identifiers)
}

func buildFragments(
from syntax: some SyntaxProtocol,
identifiers: [String:String] = [:]
) -> [Fragment] {
self.identifiers = identifiers
fragments = []

walk(syntax)

return fragments
}

override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind {
walk(node.atSign)

let name = node.attributeName.as(TypeSyntaxEnum.self)
switch name {
case .identifierType(let idType):
emitFragments(for: idType.name, as: .attribute)
if let genericArgumentClause = idType.genericArgumentClause {
walk(genericArgumentClause)
}
default:
walk(node.attributeName)
}

if let leftParen = node.leftParen {
walk(leftParen)
}
if let args = node.arguments {
walk(args)
}
if let rightParen = node.rightParen {
walk(rightParen)
}

return .skipChildren
}

override func visit(_ node: FunctionParameterSyntax) -> SyntaxVisitorContinueKind {
walk(node.attributes)
walk(node.modifiers)

emitFragments(for: node.firstName, as: .externalParameter)
if let secondName = node.secondName {
emitFragments(for: secondName, as: .internalParameter)
}

walk(node.colon)
walk(node.type)
if let ellipsis = node.ellipsis {
walk(ellipsis)
}
if let defaultValue = node.defaultValue {
walk(defaultValue)
}
if let trailingComma = node.trailingComma {
walk(trailingComma)
}

return .skipChildren
}

override func visit(_ node: IdentifierTypeSyntax) -> SyntaxVisitorContinueKind {
emitFragments(for: node.name, as: .typeIdentifier)

if let genericArgumentClause = node.genericArgumentClause {
walk(genericArgumentClause)
}

return .skipChildren
}

override func visit(_ node: MemberTypeSyntax) -> SyntaxVisitorContinueKind {
walk(node.baseType)
walk(node.period)

emitFragments(for: node.name, as: .typeIdentifier)

if let genericArgumentClause = node.genericArgumentClause {
walk(genericArgumentClause)
}

return .skipChildren
}

override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
let kind: Fragment.Kind = switch token.tokenKind {
case .integerLiteral: .numberLiteral
case .atSign, .keyword: .keyword
case .stringQuote, .stringSegment: .stringLiteral
default: .text
}
emitFragments(for: token, as: kind)

return .skipChildren
}

private func emit(fragment: Fragment) {
if let lastFragment = fragments.last,
lastFragment.preciseIdentifier == nil,
fragment.preciseIdentifier == nil,
lastFragment.kind == fragment.kind {
// if we're going to emit the same fragment kind as the last one,
// go ahead and just combine them together into a single fragment
// (unless this is an identifier type)
fragments = fragments.dropLast()
fragments.append(Fragment(
spelling: lastFragment.spelling + fragment.spelling,
kind: lastFragment.kind
))
} else {
// add a new fragment that has a distinct kind from the last one
var newFragment = fragment
newFragment.preciseIdentifier = identifiers[fragment.spelling]
fragments.append(newFragment)
}
}

private func emitFragments(for token: TokenSyntax, as kind: Fragment.Kind) {
if let leadingText = token.leadingText {
emit(fragment: Fragment(spelling: leadingText))
}

emit(fragment: Fragment(spelling: token.text, kind: kind))

if let trailingText = token.trailingText {
emit(fragment: Fragment(spelling: trailingText))
}
}
}
Loading