Skip to content

Commit 97185d5

Browse files
committed
Refactored file generation logic to be string-based and added validation tests
1 parent 883cb8a commit 97185d5

14 files changed

+188
-142
lines changed

Package.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ var targets: [Target] = [
5858
"SwiftOperators", "SwiftParser", "SwiftParserDiagnostics", "SwiftSyntax", "SwiftSyntaxBuilder",
5959
])
6060
),
61+
.target(
62+
name: "GenerateSwiftFormat",
63+
dependencies: ["SwiftFormat"],
64+
path: "Sources/GenerateSwiftFormat"
65+
),
6166
.plugin(
6267
name: "Format Source Code",
6368
capability: .command(
@@ -86,9 +91,8 @@ var targets: [Target] = [
8691
),
8792
.executableTarget(
8893
name: "generate-swift-format",
89-
dependencies: [
90-
"SwiftFormat"
91-
]
94+
dependencies: ["GenerateSwiftFormat"],
95+
path: "Sources/generate-swift-format"
9296
),
9397
.executableTarget(
9498
name: "swift-format",
@@ -113,6 +117,7 @@ var targets: [Target] = [
113117
dependencies: [
114118
"SwiftFormat",
115119
"_SwiftFormatTestSupport",
120+
"GenerateSwiftFormat",
116121
.product(name: "Markdown", package: "swift-markdown"),
117122
] + swiftSyntaxDependencies(["SwiftOperators", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"])
118123
),

Sources/generate-swift-format/FileGenerator.swift renamed to Sources/GenerateSwiftFormat/FileGenerator.swift

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@
1313
import Foundation
1414

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

2221
private struct FailedToCreateFileError: Error {
@@ -25,26 +24,12 @@ private struct FailedToCreateFileError: Error {
2524

2625
extension FileGenerator {
2726
/// Generates a file at the given URL, overwriting it if it already exists.
28-
func generateFile(at url: URL) throws {
27+
@_spi(Internal) public func generateFile(at url: URL) throws {
2928
let fm = FileManager.default
3029
if fm.fileExists(atPath: url.path) {
3130
try fm.removeItem(at: url)
3231
}
33-
34-
if !fm.createFile(atPath: url.path, contents: nil, attributes: nil) {
35-
throw FailedToCreateFileError(url: url)
36-
}
37-
let handle = try FileHandle(forWritingTo: url)
38-
defer { handle.closeFile() }
39-
40-
try write(into: handle)
41-
}
42-
}
43-
44-
extension FileHandle {
45-
/// Writes the provided string as data to a file output stream.
46-
public func write(_ string: String) {
47-
guard let data = string.data(using: .utf8) else { return }
48-
write(data)
32+
let content = generateContent()
33+
try content.write(to: url, atomically: true, encoding: .utf8)
4934
}
5035
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
/// A namespace that provides paths for files generated by the `generate-swift-format` tool.
16+
@_spi(Internal) public enum GenerateSwiftFormatPaths {
17+
private static let sourcesDirectory =
18+
URL(fileURLWithPath: #file)
19+
.deletingLastPathComponent()
20+
.deletingLastPathComponent()
21+
@_spi(Internal) public static let rulesDirectory =
22+
sourcesDirectory
23+
.appendingPathComponent("SwiftFormat")
24+
.appendingPathComponent("Rules")
25+
@_spi(Internal) public static let pipelineFile =
26+
sourcesDirectory
27+
.appendingPathComponent("SwiftFormat")
28+
.appendingPathComponent("Core")
29+
.appendingPathComponent("Pipelines+Generated.swift")
30+
@_spi(Internal) public static let ruleRegistryFile =
31+
sourcesDirectory
32+
.appendingPathComponent("SwiftFormat")
33+
.appendingPathComponent("Core")
34+
.appendingPathComponent("RuleRegistry+Generated.swift")
35+
@_spi(Internal) public static let ruleNameCacheFile =
36+
sourcesDirectory
37+
.appendingPathComponent("SwiftFormat")
38+
.appendingPathComponent("Core")
39+
.appendingPathComponent("RuleNameCache+Generated.swift")
40+
@_spi(Internal) public static let ruleDocumentationFile =
41+
sourcesDirectory
42+
.appendingPathComponent("..")
43+
.appendingPathComponent("Documentation")
44+
.appendingPathComponent("RuleDocumentation.md")
45+
}

Sources/generate-swift-format/PipelineGenerator.swift renamed to Sources/GenerateSwiftFormat/PipelineGenerator.swift

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,24 @@
1313
import Foundation
1414

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

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

2121
/// Creates a new pipeline generator.
22-
init(ruleCollector: RuleCollector) {
22+
@_spi(Internal) public init(ruleCollector: RuleCollector) {
2323
self.ruleCollector = ruleCollector
2424
}
2525

26-
func write(into handle: FileHandle) throws {
27-
handle.write(
28-
"""
26+
@_spi(Internal) public func generateContent() -> String {
27+
var result = ""
28+
result += """
2929
//===----------------------------------------------------------------------===//
3030
//
3131
// This source file is part of the Swift.org open source project
3232
//
33-
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
33+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
3434
// Licensed under Apache License v2.0 with Runtime Library Exception
3535
//
3636
// See https://swift.org/LICENSE.txt for license information
@@ -65,58 +65,41 @@ final class PipelineGenerator: FileGenerator {
6565
}
6666
6767
"""
68-
)
6968

7069
for (nodeType, lintRules) in ruleCollector.syntaxNodeLinters.sorted(by: { $0.key < $1.key }) {
71-
handle.write(
72-
"""
70+
result += """
7371
7472
override func visit(_ node: \(nodeType)) -> SyntaxVisitorContinueKind {
7573
7674
"""
77-
)
78-
7975
for ruleName in lintRules.sorted() {
80-
handle.write(
81-
"""
76+
result += """
8277
visitIfEnabled(\(ruleName).visit, for: node)
8378
8479
"""
85-
)
8680
}
87-
88-
handle.write(
89-
"""
81+
result += """
9082
return .visitChildren
9183
}
9284
9385
"""
94-
)
95-
96-
handle.write(
97-
"""
86+
result += """
9887
override func visitPost(_ node: \(nodeType)) {
9988
10089
"""
101-
)
10290
for ruleName in lintRules.sorted() {
103-
handle.write(
104-
"""
91+
result += """
10592
onVisitPost(rule: \(ruleName).self, for: node)
10693
10794
"""
108-
)
10995
}
110-
handle.write(
111-
"""
96+
result += """
11297
}
11398
11499
"""
115-
)
116100
}
117101

118-
handle.write(
119-
"""
102+
result += """
120103
}
121104
122105
extension FormatPipeline {
@@ -125,24 +108,18 @@ final class PipelineGenerator: FileGenerator {
125108
var node = node
126109
127110
"""
128-
)
129-
130111
for ruleName in ruleCollector.allFormatters.map({ $0.typeName }).sorted() {
131-
handle.write(
132-
"""
112+
result += """
133113
node = \(ruleName)(context: context).rewrite(node)
134114
135115
"""
136-
)
137116
}
138-
139-
handle.write(
140-
"""
117+
result += """
141118
return node
142119
}
143120
}
144121
145122
"""
146-
)
123+
return result
147124
}
148125
}

Sources/generate-swift-format/RuleCollector.swift renamed to Sources/GenerateSwiftFormat/RuleCollector.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import SwiftParser
1616
import SwiftSyntax
1717

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

48+
@_spi(Internal) public init() {}
49+
4850
/// Populates the internal collections with rules in the given directory.
4951
///
5052
/// - Parameter url: The file system URL that should be scanned for rules.
51-
func collect(from url: URL) throws {
53+
@_spi(Internal) public func collect(from url: URL) throws {
5254
// For each file in the Rules directory, find types that either conform to SyntaxLintRule or
5355
// inherit from SyntaxFormatRule.
5456
let fm = FileManager.default

Sources/generate-swift-format/RuleDocumentationGenerator.swift renamed to Sources/GenerateSwiftFormat/RuleDocumentationGenerator.swift

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@ import Foundation
1414
import SwiftFormat
1515

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

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

2222
/// Creates a new rule registry generator.
23-
init(ruleCollector: RuleCollector) {
23+
@_spi(Internal) public init(ruleCollector: RuleCollector) {
2424
self.ruleCollector = ruleCollector
2525
}
2626

27-
func write(into handle: FileHandle) throws {
28-
handle.write(
29-
"""
27+
@_spi(Internal) public func generateContent() -> String {
28+
var result = ""
29+
result += """
3030
<!-- This file is automatically generated with generate-swift-format. Do not edit! -->
3131
3232
# `swift-format` Lint and Format Rules
@@ -41,29 +41,20 @@ final class RuleDocumentationGenerator: FileGenerator {
4141
4242
4343
"""
44-
)
45-
4644
for detectedRule in ruleCollector.allLinters.sorted(by: { $0.typeName < $1.typeName }) {
47-
handle.write(
48-
"""
49-
- [\(detectedRule.typeName)](#\(detectedRule.typeName))
50-
51-
"""
52-
)
45+
result += "- [\(detectedRule.typeName)](#\(detectedRule.typeName))\n"
5346
}
54-
5547
for detectedRule in ruleCollector.allLinters.sorted(by: { $0.typeName < $1.typeName }) {
56-
handle.write(
57-
"""
48+
result += """
5849
5950
### \(detectedRule.typeName)
6051
6152
\(detectedRule.description ?? "")
6253
\(ruleFormatSupportDescription(for: detectedRule))
6354
6455
"""
65-
)
6656
}
57+
return result
6758
}
6859

6960
private func ruleFormatSupportDescription(for rule: RuleCollector.DetectedRule) -> String {

Sources/generate-swift-format/RuleNameCacheGenerator.swift renamed to Sources/GenerateSwiftFormat/RuleNameCacheGenerator.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,24 @@
1313
import Foundation
1414

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

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

2121
/// Creates a new rule registry generator.
22-
init(ruleCollector: RuleCollector) {
22+
@_spi(Internal) public init(ruleCollector: RuleCollector) {
2323
self.ruleCollector = ruleCollector
2424
}
2525

26-
func write(into handle: FileHandle) throws {
27-
handle.write(
28-
"""
26+
@_spi(Internal) public func generateContent() -> String {
27+
var result = ""
28+
result += """
2929
//===----------------------------------------------------------------------===//
3030
//
3131
// This source file is part of the Swift.org open source project
3232
//
33-
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
33+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
3434
// Licensed under Apache License v2.0 with Runtime Library Exception
3535
//
3636
// See https://swift.org/LICENSE.txt for license information
@@ -45,11 +45,10 @@ final class RuleNameCacheGenerator: FileGenerator {
4545
public let ruleNameCache: [ObjectIdentifier: String] = [
4646
4747
"""
48-
)
49-
5048
for detectedRule in ruleCollector.allLinters.sorted(by: { $0.typeName < $1.typeName }) {
51-
handle.write(" ObjectIdentifier(\(detectedRule.typeName).self): \"\(detectedRule.typeName)\",\n")
49+
result += " ObjectIdentifier(\(detectedRule.typeName).self): \"\(detectedRule.typeName)\",\n"
5250
}
53-
handle.write("]\n")
51+
result += "]\n"
52+
return result
5453
}
5554
}

0 commit comments

Comments
 (0)