Skip to content

Commit b80422c

Browse files
committed
Re-architect format.py in swift
Make a couple of changes to `format.py` and use the opportunity to re-write it in Swift. 1. Always build a local copy of swift-format in `swift-syntax/.swift-format-build` in release mode. This eliminates issues that occur if you had `swift-format` installed on your system and `format.py` would pick it up 2. Since the local swift-format build is persistent (doesn’t live in `/tmp`), we can build it in release. This increases the first run but decreases the time for any subsequent runs from ~15s to ~1s, which should pay usually pay off for frequent contributors because of faster turnaround times. 3. Add a little tip to add `swift-format` as a git hook to Contributing.md And as an added bonus, the rewrite reduced the runtime of `format.(py|swift)` from 1.2s to 900ms.
1 parent feee0ef commit b80422c

File tree

5 files changed

+278
-140
lines changed

5 files changed

+278
-140
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ Package.resolved
2626
*.pyc
2727

2828
Tests/PerformanceTest/baselines.json
29+
30+
# The local build of swift-format to format swift-syntax
31+
.swift-format-build

CONTRIBUTING.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,27 @@ swift-syntax is a SwiftPM package, so you can build and test it using anything t
2121

2222
swift-syntax is formatted using [swift-format](http://github.com/apple/swift-format) to ensure a consistent style.
2323

24-
To format your changes run `format.py` at the root of this repository. If you have a `swift-format` executable ready, you can pass it to `format.py`. If you do not, `format.py` will build its own copy of `swift-format` in `/tmp/swift-format`.
25-
26-
If you are seeing surprising formatting results, you most likely have a `swift-format` installed on your system that’s not the most recent version built from the `main` branch. To fix this, clone [swift-format](http://github.com/apple/swift-format), build it using `swift build` and pass the freshly built executable to `format.py` as `--swift-format path/to/swift-format/.build/debug/swift-format`. Alternatively, you can uninstall `swift-format` on your system and `format.py` will build it from scratch.
24+
To format your changes run the formatter using the following command
25+
```bash
26+
swift run --package-path SwiftSyntaxDevUtils/ swift-syntax-dev-utils format
27+
```
28+
It will build a local copy of swift-format from the `main` branch and format the repository. Since it is building a release version of `swift-format`, the first run will take few minutes. Subsequent runs take less than 2 seconds.
2729

2830
Generated source code is not formatted to make it easier to spot changes when re-running code generation.
2931

32+
> [!NOTE]
33+
> You can add a git hook to ensure all commits to the swift-syntax repository are correctly formatted.
34+
> 1. Save the following contents to `swift-syntax/.git/hooks/pre-commit`
35+
> ```bash
36+
> #!/usr/bin/env bash
37+
> set -e
38+
> SOURCE_DIR=$(realpath "$(dirname $0)/../..")
39+
> swift run --package-path "$SOURCE_DIR/SwiftSyntaxDevUtils" swift-syntax-dev-utils format --lint
40+
> ```
41+
> 2. Mark the file as executable: `chmod a+x swift-syntax/.git/hooks/pre-commit`
42+
> 3. If you have global git hooks installed, be sure to call them at the end of the script with `path/to/global/hooks/pre-commit "$@"`
43+
44+
3045
## Generating Source Code
3146
3247
If you want to modify the generated files, open the [CodeGeneration](CodeGeneration) package and run the `generate-swift-syntax` executable.

SwiftSyntaxDevUtils/Sources/swift-syntax-dev-utils/SwiftSyntaxDevUtils.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ struct SwiftSyntaxDevUtils: ParsableCommand {
2626
""",
2727
subcommands: [
2828
Build.self,
29+
Format.self,
2930
GenerateSourceCode.self,
3031
Test.self,
3132
VerifySourceCode.self,
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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 ArgumentParser
14+
import Foundation
15+
16+
/// Directories that should not be formatted.
17+
fileprivate let directoriesToExclude = [
18+
"lit_tests",
19+
"generated",
20+
"build",
21+
"Inputs",
22+
".build",
23+
".swift-format-build",
24+
]
25+
26+
struct Format: ParsableCommand {
27+
static var configuration: CommandConfiguration {
28+
return CommandConfiguration(
29+
abstract: "Format files in SwiftSyntax using swift-format.",
30+
discussion: """
31+
This command automatically builds the '\(Self.swiftFormatBranch)' branch \
32+
of swift-format in the '\(Paths.swiftFormatBuildDir.lastPathComponent)' \
33+
directory of this repository and uses the build to format the swift-syntax \
34+
sources.
35+
"""
36+
)
37+
}
38+
39+
@Flag(help: "Update the sources of swift-format and rebuild swift-format")
40+
var update: Bool = false
41+
42+
@Flag(help: "Instead of formatting in-place, verify that the files are correctly formatted. Exit with 1 if files are not correctly formatted.")
43+
var lint: Bool = false
44+
45+
@Option(
46+
help: """
47+
Instead of building a local swift-format, use this swift-format executable. \
48+
Should primarily be used for CI, which has already built swift-format.
49+
"""
50+
)
51+
var swiftFormat: String? = nil
52+
53+
@Flag(help: "Enable verbose logging.")
54+
var verbose: Bool = false
55+
56+
/// The configuration to build swift-format in.
57+
private static let swiftFormatBuildConfiguration: String = "release"
58+
59+
/// The branch of swift-format to build.
60+
private static let swiftFormatBranch: String = "main"
61+
62+
private enum Error: Swift.Error, CustomStringConvertible {
63+
case swiftFormatNotFound
64+
case lintFailed
65+
66+
var description: String {
67+
switch self {
68+
case .swiftFormatNotFound:
69+
return "The locally built swift-format could not be found"
70+
case .lintFailed:
71+
return """
72+
The swift-syntax repo is not formatted according to the style guides.
73+
74+
Run the following command to format swift-syntax
75+
76+
swift run --package-path SwiftSyntaxDevUtils/ swift-syntax-dev-utils format
77+
78+
If the issue persists, try updating swift-format by running
79+
80+
swift run --package-path SwiftSyntaxDevUtils/ swift-syntax-dev-utils format --update
81+
82+
Should that still fail, fix any remaining issues manually and verify they match the swift-format style using
83+
84+
swift run --package-path SwiftSyntaxDevUtils/ swift-syntax-dev-utils format --lint
85+
"""
86+
}
87+
}
88+
}
89+
90+
/// Run `git` in the .swift-format-build directory with the provided arguments.
91+
private func runGitCommand(_ arguments: String...) throws {
92+
try ProcessRunner(
93+
executableURL: Paths.gitExec,
94+
arguments: ["-C", Paths.swiftFormatBuildDir.path] + arguments
95+
).run(
96+
captureStdout: false,
97+
captureStderr: false,
98+
verbose: verbose
99+
)
100+
}
101+
102+
/// Run `swift` for the `.swift-format-build` package with the provided arguments.
103+
private func runSwiftCommand(_ action: String, _ arguments: String...) throws {
104+
try ProcessRunner(
105+
executableURL: Paths.swiftExec,
106+
arguments: [action, "--package-path", Paths.swiftFormatBuildDir.path] + arguments
107+
).run(
108+
captureStdout: false,
109+
captureStderr: false,
110+
verbose: verbose
111+
)
112+
}
113+
114+
/// Ensure that we have an up-to-date checkout of swift-format in `.swift-format-build`.
115+
private func cloneOrUpdateSwiftFormat() throws {
116+
if FileManager.default.fileExists(atPath: Paths.swiftFormatBuildDir.appendingPathComponent(".git").path) {
117+
try runGitCommand("checkout", Self.swiftFormatBranch)
118+
try runGitCommand("pull")
119+
} else {
120+
try FileManager.default.createDirectory(atPath: Paths.swiftFormatBuildDir.path, withIntermediateDirectories: true)
121+
try runGitCommand("clone", "https://github.com/apple/swift-format.git", ".")
122+
try runGitCommand("checkout", Self.swiftFormatBranch)
123+
}
124+
try runSwiftCommand("package", "update")
125+
}
126+
127+
/// Build the swift-format executable.
128+
private func buildSwiftFormat() throws {
129+
try runSwiftCommand("build", "--product", "swift-format", "--configuration", Self.swiftFormatBuildConfiguration)
130+
}
131+
132+
/// Get the URL of the locally-built swift-format executable.
133+
private func findSwiftFormatExecutable() throws -> URL {
134+
if let swiftFormat = swiftFormat {
135+
return URL(fileURLWithPath: swiftFormat)
136+
}
137+
138+
// We could run `swift build --show-bin-path` here but that takes 0.4s.
139+
// Since the path seems really stable, let’s build the path ourselves.
140+
let swiftFormatExec = URL(fileURLWithPath: Paths.swiftFormatBuildDir.path)
141+
.appendingPathComponent(".build")
142+
.appendingPathComponent(Self.swiftFormatBuildConfiguration)
143+
.appendingPathComponent("swift-format")
144+
if !swiftFormatExec.isExecutableFile {
145+
throw Error.swiftFormatNotFound
146+
}
147+
return swiftFormatExec
148+
}
149+
150+
/// Get the list of files that should be formatted using swift-format.
151+
///
152+
/// This excludes some files like generated files or test inputs.
153+
private func filesToFormat() -> [URL] {
154+
guard let enumerator = FileManager.default.enumerator(at: Paths.packageDir.resolvingSymlinksInPath(), includingPropertiesForKeys: [], options: []) else {
155+
return []
156+
}
157+
158+
var result: [URL] = []
159+
for case let url as URL in enumerator {
160+
if directoriesToExclude.contains(url.lastPathComponent) {
161+
enumerator.skipDescendants()
162+
}
163+
if url.pathExtension == "swift" {
164+
result.append(url)
165+
}
166+
}
167+
168+
return result
169+
}
170+
171+
/// Format all files in the repo using the locally-built swift-format.
172+
private func formatFilesInRepo() throws {
173+
let swiftFormatExecutable = try findSwiftFormatExecutable()
174+
175+
let filesToFormat = self.filesToFormat()
176+
177+
try ProcessRunner(
178+
executableURL: swiftFormatExecutable,
179+
arguments: [
180+
"format",
181+
"--in-place",
182+
"--parallel",
183+
] + filesToFormat.map { $0.path }
184+
)
185+
.run(
186+
captureStdout: false,
187+
captureStderr: false,
188+
verbose: verbose
189+
)
190+
}
191+
192+
/// Lint all files in the repo using the locally-built swift-format.
193+
private func lintFilesInRepo() throws {
194+
let swiftFormatExecutable = try findSwiftFormatExecutable()
195+
196+
let filesToFormat = self.filesToFormat()
197+
198+
do {
199+
try ProcessRunner(
200+
executableURL: swiftFormatExecutable,
201+
arguments: [
202+
"lint",
203+
"--strict",
204+
"--parallel",
205+
] + filesToFormat.map { $0.path }
206+
)
207+
.run(
208+
captureStdout: false,
209+
captureStderr: false,
210+
verbose: verbose
211+
)
212+
} catch is NonZeroExitCodeError {
213+
throw Error.lintFailed
214+
}
215+
}
216+
217+
func run() throws {
218+
#if compiler(<5.10)
219+
print("💡 You are building running the format script with Swift 5.9 or lower. Running it with SwiftPM 5.10 is about 10s faster.")
220+
#endif
221+
222+
try run(updateAndBuild: self.update)
223+
}
224+
225+
/// - Parameter updateAndBuild: Whether to update the locally checked out
226+
/// swift-format sources and rebuild swift-format.
227+
func run(updateAndBuild: Bool) throws {
228+
if updateAndBuild {
229+
try cloneOrUpdateSwiftFormat()
230+
try buildSwiftFormat()
231+
}
232+
do {
233+
if lint {
234+
try lintFilesInRepo()
235+
} else {
236+
try formatFilesInRepo()
237+
}
238+
} catch Error.swiftFormatNotFound {
239+
if !updateAndBuild {
240+
print(
241+
"""
242+
No build of swift-format was found in '\(Paths.swiftFormatBuildDir.lastPathComponent)'.
243+
Building swift-format now. This may take a couple of minutes.
244+
Future invocations of this command will re-use the build and are much faster.
245+
246+
"""
247+
)
248+
249+
// If swift-format cannot be found, try again, updating (aka cloning + building) swift-format this time
250+
try run(updateAndBuild: true)
251+
} else {
252+
throw Error.swiftFormatNotFound
253+
}
254+
}
255+
}
256+
}

0 commit comments

Comments
 (0)