Skip to content
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

Migrate to Swift concurrency. #675

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
45 changes: 35 additions & 10 deletions Sources/swift-format/Frontend/ConfigurationLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,60 @@ import SwiftFormat

/// Loads formatter configurations, caching them in memory so that multiple operations in the same
/// directory do not repeatedly hit the file system.
struct ConfigurationLoader {
actor ConfigurationLoader {
/// Keeps track of the state of configurations in the cache.
private enum CacheEntry {
/// The configuration has been fully loaded.
case ready(Configuration)

/// The configuration is in the process of being loaded.
case loading(Task<Configuration, Error>)
}

/// The cache of previously loaded configurations.
private var cache = [String: Configuration]()
private var cache = [String: CacheEntry]()

/// Returns the configuration found by searching in the directory (and ancestor directories)
/// containing the given `.swift` source file.
///
/// If no configuration file was found during the search, this method returns nil.
///
/// - Throws: If a configuration file was found but an error occurred loading it.
mutating func configuration(forSwiftFileAt url: URL) throws -> Configuration? {
func configuration(forSwiftFileAt url: URL) async throws -> Configuration? {
guard let configurationFileURL = Configuration.url(forConfigurationFileApplyingTo: url)
else {
return nil
}
return try configuration(at: configurationFileURL)
return try await configuration(at: configurationFileURL)
}

/// Returns the configuration associated with the configuration file at the given URL.
///
/// - Throws: If an error occurred loading the configuration.
mutating func configuration(at url: URL) throws -> Configuration {
func configuration(at url: URL) async throws -> Configuration {
let cacheKey = url.absoluteURL.standardized.path
if let cachedConfiguration = cache[cacheKey] {
return cachedConfiguration

if let cached = cache[cacheKey] {
switch cached {
case .ready(let configuration):
return configuration
case .loading(let task):
return try await task.value
}
}

let configuration = try Configuration(contentsOf: url)
cache[cacheKey] = configuration
return configuration
let task = Task {
try Configuration(contentsOf: url)
}
cache[cacheKey] = .loading(task)

do {
let configuration = try await task.value
cache[cacheKey] = .ready(configuration)
return configuration
} catch {
cache[cacheKey] = nil
throw error
}
}
}
4 changes: 2 additions & 2 deletions Sources/swift-format/Frontend/FormatFrontend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ class FormatFrontend: Frontend {
/// Whether or not to format the Swift file in-place.
private let inPlace: Bool

init(lintFormatOptions: LintFormatOptions, inPlace: Bool) {
init(lintFormatOptions: LintFormatOptions, inPlace: Bool) async {
self.inPlace = inPlace
super.init(lintFormatOptions: lintFormatOptions)
await super.init(lintFormatOptions: lintFormatOptions)
}

override func processFile(_ fileToProcess: FileToProcess) {
Expand Down
49 changes: 27 additions & 22 deletions Sources/swift-format/Frontend/Frontend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,25 @@ class Frontend {
/// Creates a new frontend with the given options.
///
/// - Parameter lintFormatOptions: Options that apply during formatting or linting.
init(lintFormatOptions: LintFormatOptions) {
init(lintFormatOptions: LintFormatOptions) async {
self.lintFormatOptions = lintFormatOptions

self.diagnosticPrinter = StderrDiagnosticPrinter(
colorMode: lintFormatOptions.colorDiagnostics.map { $0 ? .on : .off } ?? .auto)
self.diagnosticsEngine =
DiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic])
await DiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic])
}

/// Runs the linter or formatter over the inputs.
final func run() {
final func run() async {
if lintFormatOptions.paths.isEmpty {
processStandardInput()
await processStandardInput()
} else {
processURLs(
await processURLs(
lintFormatOptions.paths.map(URL.init(fileURLWithPath:)),
parallel: lintFormatOptions.parallel)
}
await diagnosticsEngine.flush()
}

/// Called by the frontend to process a single file.
Expand All @@ -107,8 +108,8 @@ class Frontend {
}

/// Processes source content from standard input.
private func processStandardInput() {
guard let configuration = configuration(
private func processStandardInput() async {
guard let configuration = await configuration(
fromPathOrString: lintFormatOptions.configuration,
orInferredFromSwiftFileAt: nil)
else {
Expand All @@ -124,37 +125,41 @@ class Frontend {
}

/// Processes source content from a list of files and/or directories provided as file URLs.
private func processURLs(_ urls: [URL], parallel: Bool) {
private func processURLs(_ urls: [URL], parallel: Bool) async {
precondition(
!urls.isEmpty,
"processURLs(_:) should only be called when 'urls' is non-empty.")

if parallel {
let filesToProcess =
FileIterator(urls: urls, followSymlinks: lintFormatOptions.followSymlinks)
.compactMap(openAndPrepareFile)
DispatchQueue.concurrentPerform(iterations: filesToProcess.count) { index in
processFile(filesToProcess[index])
await withTaskGroup(of: Void.self) { group in
for url in FileIterator(urls: urls, followSymlinks: lintFormatOptions.followSymlinks) {
group.addTask {
if let fileToProcess = await self.openAndPrepareFile(at: url) {
self.processFile(fileToProcess)
}
}
}
}
} else {
FileIterator(urls: urls, followSymlinks: lintFormatOptions.followSymlinks)
.lazy
.compactMap(openAndPrepareFile)
.forEach(processFile)
for url in FileIterator(urls: urls, followSymlinks: lintFormatOptions.followSymlinks) {
if let fileToProcess = await openAndPrepareFile(at: url) {
processFile(fileToProcess)
}
}
}
}

/// Read and prepare the file at the given path for processing, optionally synchronizing
/// diagnostic output.
private func openAndPrepareFile(at url: URL) -> FileToProcess? {
private func openAndPrepareFile(at url: URL) async -> FileToProcess? {
guard let sourceFile = try? FileHandle(forReadingFrom: url) else {
diagnosticsEngine.emitError(
"Unable to open \(url.relativePath): file is not readable or does not exist")
return nil
}

guard
let configuration = configuration(
let configuration = await configuration(
fromPathOrString: lintFormatOptions.configuration,
orInferredFromSwiftFileAt: url)
else {
Expand Down Expand Up @@ -185,13 +190,13 @@ class Frontend {
private func configuration(
fromPathOrString pathOrString: String?,
orInferredFromSwiftFileAt swiftFileURL: URL?
) -> Configuration? {
) async -> Configuration? {
if let pathOrString = pathOrString {
// If an explicit configuration file path was given, try to load it and fail if it cannot be
// loaded. (Do not try to fall back to a path inferred from the source file path.)
let configurationFileURL = URL(fileURLWithPath: pathOrString)
do {
let configuration = try configurationLoader.configuration(at: configurationFileURL)
let configuration = try await configurationLoader.configuration(at: configurationFileURL)
self.checkForUnrecognizedRules(in: configuration)
return configuration
} catch {
Expand All @@ -213,7 +218,7 @@ class Frontend {
// then try to load the configuration by inferring it based on the source file path.
if let swiftFileURL = swiftFileURL {
do {
if let configuration = try configurationLoader.configuration(forSwiftFileAt: swiftFileURL) {
if let configuration = try await configurationLoader.configuration(forSwiftFileAt: swiftFileURL) {
self.checkForUnrecognizedRules(in: configuration)
return configuration
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/swift-format/Subcommands/Format.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import ArgumentParser

extension SwiftFormatCommand {
/// Formats one or more files containing Swift code.
struct Format: ParsableCommand {
struct Format: AsyncParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Format Swift source code",
discussion: "When no files are specified, it expects the source from standard input.")
Expand All @@ -39,11 +39,11 @@ extension SwiftFormatCommand {
}
}

func run() throws {
try performanceMeasurementOptions.printingInstructionCountIfRequested() {
let frontend = FormatFrontend(lintFormatOptions: formatOptions, inPlace: inPlace)
frontend.run()
if frontend.diagnosticsEngine.hasErrors { throw ExitCode.failure }
func run() async throws {
try await performanceMeasurementOptions.printingInstructionCountIfRequested() {
let frontend = await FormatFrontend(lintFormatOptions: formatOptions, inPlace: inPlace)
await frontend.run()
if await frontend.diagnosticsEngine.hasErrors { throw ExitCode.failure }
}
}
}
Expand Down
16 changes: 9 additions & 7 deletions Sources/swift-format/Subcommands/Lint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import ArgumentParser

extension SwiftFormatCommand {
/// Emits style diagnostics for one or more files containing Swift code.
struct Lint: ParsableCommand {
struct Lint: AsyncParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Diagnose style issues in Swift source code",
discussion: "When no files are specified, it expects the source from standard input.")
Expand All @@ -31,12 +31,14 @@ extension SwiftFormatCommand {
@OptionGroup(visibility: .hidden)
var performanceMeasurementOptions: PerformanceMeasurementsOptions

func run() throws {
try performanceMeasurementOptions.printingInstructionCountIfRequested {
let frontend = LintFrontend(lintFormatOptions: lintOptions)
frontend.run()

if frontend.diagnosticsEngine.hasErrors || strict && frontend.diagnosticsEngine.hasWarnings {
func run() async throws {
try await performanceMeasurementOptions.printingInstructionCountIfRequested {
let frontend = await LintFrontend(lintFormatOptions: lintOptions)
await frontend.run()

let hasErrors = await frontend.diagnosticsEngine.hasErrors
let hasWarnings = await frontend.diagnosticsEngine.hasWarnings
if hasErrors || strict && hasWarnings {
throw ExitCode.failure
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ struct PerformanceMeasurementsOptions: ParsableArguments {

/// If `measureInstructions` is set, execute `body` and print the number of instructions
/// executed by it. Otherwise, just execute `body`
func printingInstructionCountIfRequested<T>(_ body: () throws -> T) rethrows -> T {
func printingInstructionCountIfRequested<T>(_ body: () async throws -> T) async rethrows -> T {
if !measureInstructions {
return try body()
return try await body()
} else {
let startInstructions = getInstructionsExecuted()
defer {
print("Instructions executed: \(getInstructionsExecuted() - startInstructions)")
}
return try body()
return try await body()
}
}
}
2 changes: 1 addition & 1 deletion Sources/swift-format/SwiftFormatCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import ArgumentParser
/// Collects the command line options that were passed to `swift-format` and dispatches to the
/// appropriate subcommand.
@main
struct SwiftFormatCommand: ParsableCommand {
struct SwiftFormatCommand: AsyncParsableCommand {
static var configuration = CommandConfiguration(
commandName: "swift-format",
abstract: "Format or lint Swift source code",
Expand Down
Loading