Skip to content

Refactored file generation logic to be string-based and added validation tests #1047

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

Merged
merged 1 commit into from
Jul 21, 2025
Merged
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
9 changes: 6 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ var targets: [Target] = [
"SwiftOperators", "SwiftParser", "SwiftParserDiagnostics", "SwiftSyntax", "SwiftSyntaxBuilder",
])
),
.target(
name: "_GenerateSwiftFormat",
dependencies: ["SwiftFormat"]
),
.plugin(
name: "Format Source Code",
capability: .command(
Expand Down Expand Up @@ -86,9 +90,7 @@ var targets: [Target] = [
),
.executableTarget(
name: "generate-swift-format",
dependencies: [
"SwiftFormat"
]
dependencies: ["_GenerateSwiftFormat"]
),
.executableTarget(
name: "swift-format",
Expand All @@ -113,6 +115,7 @@ var targets: [Target] = [
dependencies: [
"SwiftFormat",
"_SwiftFormatTestSupport",
"_GenerateSwiftFormat",
.product(name: "Markdown", package: "swift-markdown"),
] + swiftSyntaxDependencies(["SwiftOperators", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"])
),
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftFormat/Core/Pipelines+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Copyright (c) 2014 - 2025 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
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftFormat/Core/RuleNameCache+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Copyright (c) 2014 - 2025 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
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftFormat/Core/RuleRegistry+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Copyright (c) 2014 - 2025 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@
import Foundation

/// Common behavior used to generate source files.
protocol FileGenerator {
/// Types conforming to this protocol must implement this method to write their content into the
/// given file handle.
func write(into handle: FileHandle) throws
@_spi(Internal) public protocol FileGenerator {
/// Generates the file content as a String.
func generateContent() -> String
}

private struct FailedToCreateFileError: Error {
Expand All @@ -25,26 +24,12 @@ private struct FailedToCreateFileError: Error {

extension FileGenerator {
/// Generates a file at the given URL, overwriting it if it already exists.
func generateFile(at url: URL) throws {
public func generateFile(at url: URL) throws {
let fm = FileManager.default
if fm.fileExists(atPath: url.path) {
try fm.removeItem(at: url)
}

if !fm.createFile(atPath: url.path, contents: nil, attributes: nil) {
throw FailedToCreateFileError(url: url)
}
let handle = try FileHandle(forWritingTo: url)
defer { handle.closeFile() }

try write(into: handle)
}
}

extension FileHandle {
/// Writes the provided string as data to a file output stream.
public func write(_ string: String) {
guard let data = string.data(using: .utf8) else { return }
write(data)
let content = generateContent()
try content.write(to: url, atomically: true, encoding: .utf8)
}
}
45 changes: 45 additions & 0 deletions Sources/_GenerateSwiftFormat/GenerateSwiftFormatPaths.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2025 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 the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation

/// A namespace that provides paths for files generated by the `generate-swift-format` tool.
@_spi(Internal) public enum GenerateSwiftFormatPaths {
private static let sourcesDirectory =
URL(fileURLWithPath: #file)
.deletingLastPathComponent()
.deletingLastPathComponent()
public static let rulesDirectory =
sourcesDirectory
.appendingPathComponent("SwiftFormat")
.appendingPathComponent("Rules")
public static let pipelineFile =
sourcesDirectory
.appendingPathComponent("SwiftFormat")
.appendingPathComponent("Core")
.appendingPathComponent("Pipelines+Generated.swift")
public static let ruleRegistryFile =
sourcesDirectory
.appendingPathComponent("SwiftFormat")
.appendingPathComponent("Core")
.appendingPathComponent("RuleRegistry+Generated.swift")
public static let ruleNameCacheFile =
sourcesDirectory
.appendingPathComponent("SwiftFormat")
.appendingPathComponent("Core")
.appendingPathComponent("RuleNameCache+Generated.swift")
public static let ruleDocumentationFile =
sourcesDirectory
.appendingPathComponent("..")
.appendingPathComponent("Documentation")
.appendingPathComponent("RuleDocumentation.md")
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@
import Foundation

/// Generates the extensions to the lint and format pipelines.
final class PipelineGenerator: FileGenerator {
@_spi(Internal) public final class PipelineGenerator: FileGenerator {

/// The rules collected by scanning the formatter source code.
let ruleCollector: RuleCollector

/// Creates a new pipeline generator.
init(ruleCollector: RuleCollector) {
public init(ruleCollector: RuleCollector) {
self.ruleCollector = ruleCollector
}

func write(into handle: FileHandle) throws {
handle.write(
"""
public func generateContent() -> String {
var result = ""
result += """
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Copyright (c) 2014 - 2025 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
Expand Down Expand Up @@ -65,58 +65,41 @@ final class PipelineGenerator: FileGenerator {
}

"""
)

for (nodeType, lintRules) in ruleCollector.syntaxNodeLinters.sorted(by: { $0.key < $1.key }) {
handle.write(
"""
result += """

override func visit(_ node: \(nodeType)) -> SyntaxVisitorContinueKind {

"""
)

for ruleName in lintRules.sorted() {
handle.write(
"""
result += """
visitIfEnabled(\(ruleName).visit, for: node)

"""
)
}

handle.write(
"""
result += """
return .visitChildren
}

"""
)

handle.write(
"""
result += """
override func visitPost(_ node: \(nodeType)) {

"""
)
for ruleName in lintRules.sorted() {
handle.write(
"""
result += """
onVisitPost(rule: \(ruleName).self, for: node)

"""
)
}
handle.write(
"""
result += """
}

"""
)
}

handle.write(
"""
result += """
}

extension FormatPipeline {
Expand All @@ -125,24 +108,18 @@ final class PipelineGenerator: FileGenerator {
var node = node

"""
)

for ruleName in ruleCollector.allFormatters.map({ $0.typeName }).sorted() {
handle.write(
"""
result += """
node = \(ruleName)(context: context).rewrite(node)

"""
)
}

handle.write(
"""
result += """
return node
}
}

"""
)
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import SwiftParser
import SwiftSyntax

/// Collects information about rules in the formatter code base.
final class RuleCollector {
@_spi(Internal) public final class RuleCollector {
/// Information about a detected rule.
struct DetectedRule: Hashable {
/// The type name of the rule.
Expand Down Expand Up @@ -45,10 +45,12 @@ final class RuleCollector {
/// A dictionary mapping syntax node types to the lint/format rules that visit them.
var syntaxNodeLinters = [String: [String]]()

public init() {}

/// Populates the internal collections with rules in the given directory.
///
/// - Parameter url: The file system URL that should be scanned for rules.
func collect(from url: URL) throws {
public func collect(from url: URL) throws {
// For each file in the Rules directory, find types that either conform to SyntaxLintRule or
// inherit from SyntaxFormatRule.
let fm = FileManager.default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ import Foundation
import SwiftFormat

/// Generates the markdown file with extended documenation on the available rules.
final class RuleDocumentationGenerator: FileGenerator {
@_spi(Internal) public final class RuleDocumentationGenerator: FileGenerator {

/// The rules collected by scanning the formatter source code.
let ruleCollector: RuleCollector

/// Creates a new rule registry generator.
init(ruleCollector: RuleCollector) {
public init(ruleCollector: RuleCollector) {
self.ruleCollector = ruleCollector
}

func write(into handle: FileHandle) throws {
handle.write(
"""
public func generateContent() -> String {
var result = ""
result += """
<!-- This file is automatically generated with generate-swift-format. Do not edit! -->

# `swift-format` Lint and Format Rules
Expand All @@ -41,29 +41,20 @@ final class RuleDocumentationGenerator: FileGenerator {


"""
)

for detectedRule in ruleCollector.allLinters.sorted(by: { $0.typeName < $1.typeName }) {
handle.write(
"""
- [\(detectedRule.typeName)](#\(detectedRule.typeName))

"""
)
result += "- [\(detectedRule.typeName)](#\(detectedRule.typeName))\n"
}

for detectedRule in ruleCollector.allLinters.sorted(by: { $0.typeName < $1.typeName }) {
handle.write(
"""
result += """

### \(detectedRule.typeName)

\(detectedRule.description ?? "")
\(ruleFormatSupportDescription(for: detectedRule))

"""
)
}
return result
}

private func ruleFormatSupportDescription(for rule: RuleCollector.DetectedRule) -> String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@
import Foundation

/// Generates the rule registry file used to populate the default configuration.
final class RuleNameCacheGenerator: FileGenerator {
@_spi(Internal) public final class RuleNameCacheGenerator: FileGenerator {

/// The rules collected by scanning the formatter source code.
let ruleCollector: RuleCollector

/// Creates a new rule registry generator.
init(ruleCollector: RuleCollector) {
public init(ruleCollector: RuleCollector) {
self.ruleCollector = ruleCollector
}

func write(into handle: FileHandle) throws {
handle.write(
"""
public func generateContent() -> String {
var result = ""
result += """
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Copyright (c) 2014 - 2025 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
Expand All @@ -45,11 +45,10 @@ final class RuleNameCacheGenerator: FileGenerator {
public let ruleNameCache: [ObjectIdentifier: String] = [

"""
)

for detectedRule in ruleCollector.allLinters.sorted(by: { $0.typeName < $1.typeName }) {
handle.write(" ObjectIdentifier(\(detectedRule.typeName).self): \"\(detectedRule.typeName)\",\n")
result += " ObjectIdentifier(\(detectedRule.typeName).self): \"\(detectedRule.typeName)\",\n"
}
handle.write("]\n")
result += "]\n"
return result
}
}
Loading