Skip to content

Commit dd1fa22

Browse files
authored
Merge pull request #273 from allevato/diagnostic-paths
Fix up file URL handling.
2 parents defb00d + 5a60e59 commit dd1fa22

File tree

8 files changed

+124
-105
lines changed

8 files changed

+124
-105
lines changed

Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import Foundation
1616

1717
let package = Package(
1818
name: "swift-format",
19+
platforms: [
20+
.macOS(.v10_11)
21+
],
1922
products: [
2023
.executable(name: "swift-format", targets: ["swift-format"]),
2124
.library(name: "SwiftFormat", targets: ["SwiftFormat", "SwiftFormatConfiguration"]),

Sources/SwiftFormatConfiguration/Configuration.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,21 +233,25 @@ public struct Configuration: Codable, Equatable {
233233

234234
/// Returns the URL of the configuration file that applies to the given file or directory.
235235
public static func url(forConfigurationFileApplyingTo url: URL) -> URL? {
236-
var path = url.absoluteURL
237-
let configFilename = ".swift-format"
236+
// Despite the variable's name, this value might start out first as a file path (the path to a
237+
// source file being formatted). However, it will immediately have its basename removed in the
238+
// loop below, and from then on serve as a directory path only.
239+
var candidateDirectory = url.absoluteURL.standardized
238240
var isDirectory: ObjCBool = false
239-
if FileManager.default.fileExists(atPath: path.path, isDirectory: &isDirectory),
240-
isDirectory.boolValue {
241-
// will be deleted in a loop
242-
path.appendPathComponent("placeholder")
241+
if FileManager.default.fileExists(atPath: candidateDirectory.path, isDirectory: &isDirectory),
242+
isDirectory.boolValue
243+
{
244+
// If the path actually was a directory, append a fake basename so that the trimming code
245+
// below doesn't have to deal with the first-time special case.
246+
candidateDirectory.appendPathComponent("placeholder")
243247
}
244248
repeat {
245-
path.deleteLastPathComponent()
246-
let candidateFile = path.appendingPathComponent(configFilename)
249+
candidateDirectory.deleteLastPathComponent()
250+
let candidateFile = candidateDirectory.appendingPathComponent(".swift-format")
247251
if FileManager.default.isReadableFile(atPath: candidateFile.path) {
248252
return candidateFile
249253
}
250-
} while path.path != "/"
254+
} while candidateDirectory.path != "/"
251255

252256
return nil
253257
}

Sources/SwiftFormatCore/Context.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ public final class Context {
6969
self.fileURL = fileURL
7070
self.importsXCTest = .notDetermined
7171
self.sourceLocationConverter =
72-
source.map { SourceLocationConverter(file: fileURL.path, source: $0) }
73-
?? SourceLocationConverter(file: fileURL.path, tree: sourceFileSyntax)
72+
source.map { SourceLocationConverter(file: fileURL.relativePath, source: $0) }
73+
?? SourceLocationConverter(file: fileURL.relativePath, tree: sourceFileSyntax)
7474
self.ruleMask = RuleMask(
7575
syntaxNode: Syntax(sourceFileSyntax),
7676
sourceLocationConverter: sourceLocationConverter

Sources/swift-format/Frontend/ConfigurationLoader.swift

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,17 @@ import SwiftFormatConfiguration
1616
/// Loads formatter configurations, caching them in memory so that multiple operations in the same
1717
/// directory do not repeatedly hit the file system.
1818
struct ConfigurationLoader {
19-
/// A mapping from configuration file URLs to the loaded configuration data.
20-
private var cache = [URL: Configuration]()
21-
22-
/// Returns the configuration associated with the configuration file at the given path.
23-
///
24-
/// - Throws: If an error occurred loading the configuration.
25-
mutating func configuration(atPath path: String) throws -> Configuration {
26-
return try configuration(at: URL(fileURLWithPath: path))
27-
}
19+
/// The cache of previously loaded configurations.
20+
private var cache = [String: Configuration]()
2821

2922
/// Returns the configuration found by searching in the directory (and ancestor directories)
3023
/// containing the given `.swift` source file.
3124
///
3225
/// If no configuration file was found during the search, this method returns nil.
3326
///
3427
/// - Throws: If a configuration file was found but an error occurred loading it.
35-
mutating func configuration(forSwiftFileAtPath path: String) throws -> Configuration? {
36-
let swiftFileURL = URL(fileURLWithPath: path)
37-
guard let configurationFileURL = Configuration.url(forConfigurationFileApplyingTo: swiftFileURL)
28+
mutating func configuration(forSwiftFileAt url: URL) throws -> Configuration? {
29+
guard let configurationFileURL = Configuration.url(forConfigurationFileApplyingTo: url)
3830
else {
3931
return nil
4032
}
@@ -44,13 +36,14 @@ struct ConfigurationLoader {
4436
/// Returns the configuration associated with the configuration file at the given URL.
4537
///
4638
/// - Throws: If an error occurred loading the configuration.
47-
private mutating func configuration(at url: URL) throws -> Configuration {
48-
if let cachedConfiguration = cache[url] {
39+
mutating func configuration(at url: URL) throws -> Configuration {
40+
let cacheKey = url.absoluteURL.standardized.path
41+
if let cachedConfiguration = cache[cacheKey] {
4942
return cachedConfiguration
5043
}
5144

5245
let configuration = try Configuration(contentsOf: url)
53-
cache[url] = configuration
46+
cache[cacheKey] = configuration
5447
return configuration
5548
}
5649
}

Sources/swift-format/Frontend/FormatFrontend.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,35 @@ class FormatFrontend: Frontend {
3232
let formatter = SwiftFormatter(configuration: fileToProcess.configuration, findingConsumer: nil)
3333
formatter.debugOptions = debugOptions
3434

35-
let path = fileToProcess.path
35+
let url = fileToProcess.url
3636
guard let source = fileToProcess.sourceText else {
37-
diagnosticsEngine.emitError("Unable to read source for formatting from \(path).")
37+
diagnosticsEngine.emitError(
38+
"Unable to format \(url.relativePath): file is not readable or does not exist.")
3839
return
3940
}
4041

4142
var stdoutStream = FileHandle.standardOutput
4243
do {
43-
let assumingFileURL = URL(fileURLWithPath: path)
4444
if inPlace {
4545
var buffer = ""
4646
try formatter.format(
4747
source: source,
48-
assumingFileURL: assumingFileURL,
48+
assumingFileURL: url,
4949
to: &buffer,
5050
parsingDiagnosticHandler: diagnosticsEngine.consumeParserDiagnostic)
5151

5252
let bufferData = buffer.data(using: .utf8)! // Conversion to UTF-8 cannot fail
53-
try bufferData.write(to: assumingFileURL, options: .atomic)
53+
try bufferData.write(to: url, options: .atomic)
5454
} else {
5555
try formatter.format(
5656
source: source,
57-
assumingFileURL: assumingFileURL,
57+
assumingFileURL: url,
5858
to: &stdoutStream,
5959
parsingDiagnosticHandler: diagnosticsEngine.consumeParserDiagnostic)
6060
}
6161
} catch SwiftFormatError.fileNotReadable {
6262
diagnosticsEngine.emitError(
63-
"Unable to format \(path): file is not readable or does not exist.")
63+
"Unable to format \(url.relativePath): file is not readable or does not exist.")
6464
return
6565
} catch SwiftFormatError.fileContainsInvalidSyntax(let position) {
6666
guard !lintFormatOptions.ignoreUnparsableFiles else {
@@ -71,12 +71,12 @@ class FormatFrontend: Frontend {
7171
stdoutStream.write(source)
7272
return
7373
}
74-
let location = SourceLocationConverter(file: path, source: source).location(for: position)
74+
let location = SourceLocationConverter(file: url.path, source: source).location(for: position)
7575
diagnosticsEngine.emitError(
7676
"file contains invalid or unrecognized Swift syntax.", location: location)
7777
return
7878
} catch {
79-
diagnosticsEngine.emitError("Unable to format \(path): \(error)")
79+
diagnosticsEngine.emitError("Unable to format \(url.relativePath): \(error)")
8080
}
8181
}
8282
}

Sources/swift-format/Frontend/Frontend.swift

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ class Frontend {
2323
/// An open file handle to the source code of the file.
2424
private let fileHandle: FileHandle
2525

26-
/// The path to the source file being processed.
26+
/// A file URL representing the path to the source file being processed.
2727
///
2828
/// It is the responsibility of the specific frontend to make guarantees about the validity of
2929
/// this path. For example, the formatting frontend ensures that it is a path to an existing
3030
/// file only when doing in-place formatting (so that the file can be replaced). In other
3131
/// situations, it may correspond to a different file than the underlying file handle (if
3232
/// standard input is used with the `--assume-filename` flag), or it may not be a valid path at
3333
/// all (the string `"<stdin>"`).
34-
let path: String
34+
let url: URL
3535

3636
/// The configuration that should applied for this file.
3737
let configuration: Configuration
@@ -46,9 +46,9 @@ class Frontend {
4646
return String(data: sourceData, encoding: .utf8)
4747
}()
4848

49-
init(fileHandle: FileHandle, path: String, configuration: Configuration) {
49+
init(fileHandle: FileHandle, url: URL, configuration: Configuration) {
5050
self.fileHandle = fileHandle
51-
self.path = path
51+
self.url = url
5252
self.configuration = configuration
5353
}
5454
}
@@ -81,12 +81,12 @@ class Frontend {
8181

8282
/// Runs the linter or formatter over the inputs.
8383
final func run() {
84-
let paths = lintFormatOptions.paths
85-
86-
if paths.isEmpty {
84+
if lintFormatOptions.paths.isEmpty {
8785
processStandardInput()
8886
} else {
89-
processPaths(paths, parallel: lintFormatOptions.parallel)
87+
processURLs(
88+
lintFormatOptions.paths.map(URL.init(fileURLWithPath:)),
89+
parallel: lintFormatOptions.parallel)
9090
}
9191
}
9292

@@ -103,52 +103,55 @@ class Frontend {
103103
/// Processes source content from standard input.
104104
private func processStandardInput() {
105105
guard let configuration = configuration(
106-
atPath: lintFormatOptions.configurationPath,
107-
orInferredFromSwiftFileAtPath: nil)
106+
at: lintFormatOptions.configurationPath.map(URL.init(fileURLWithPath:)),
107+
orInferredFromSwiftFileAt: nil)
108108
else {
109109
// Already diagnosed in the called method.
110110
return
111111
}
112112

113113
let fileToProcess = FileToProcess(
114114
fileHandle: FileHandle.standardInput,
115-
path: lintFormatOptions.assumeFilename ?? "<stdin>",
115+
url: URL(fileURLWithPath: lintFormatOptions.assumeFilename ?? "<stdin>"),
116116
configuration: configuration)
117117
processFile(fileToProcess)
118118
}
119119

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

126126
if parallel {
127-
let filesToProcess = FileIterator(paths: paths).compactMap(openAndPrepareFile)
127+
let filesToProcess = FileIterator(urls: urls).compactMap(openAndPrepareFile)
128128
DispatchQueue.concurrentPerform(iterations: filesToProcess.count) { index in
129129
processFile(filesToProcess[index])
130130
}
131131
} else {
132-
FileIterator(paths: paths).lazy.compactMap(openAndPrepareFile).forEach(processFile)
132+
FileIterator(urls: urls).lazy.compactMap(openAndPrepareFile).forEach(processFile)
133133
}
134134
}
135135

136136
/// Read and prepare the file at the given path for processing, optionally synchronizing
137137
/// diagnostic output.
138-
private func openAndPrepareFile(atPath path: String) -> FileToProcess? {
139-
guard let sourceFile = FileHandle(forReadingAtPath: path) else {
140-
diagnosticsEngine.emitError("Unable to open \(path)")
138+
private func openAndPrepareFile(at url: URL) -> FileToProcess? {
139+
guard let sourceFile = try? FileHandle(forReadingFrom: url) else {
140+
diagnosticsEngine.emitError(
141+
"Unable to open \(url.relativePath): file is not readable or does not exist")
141142
return nil
142143
}
143144

144-
guard let configuration = configuration(
145-
atPath: lintFormatOptions.configurationPath, orInferredFromSwiftFileAtPath: path)
145+
guard
146+
let configuration = configuration(
147+
at: lintFormatOptions.configurationPath.map(URL.init(fileURLWithPath:)),
148+
orInferredFromSwiftFileAt: url)
146149
else {
147150
// Already diagnosed in the called method.
148151
return nil
149152
}
150153

151-
return FileToProcess(fileHandle: sourceFile, path: path, configuration: configuration)
154+
return FileToProcess(fileHandle: sourceFile, url: url, configuration: configuration)
152155
}
153156

154157
/// Returns the configuration that applies to the given `.swift` source file, when an explicit
@@ -164,14 +167,14 @@ class Frontend {
164167
/// `swiftFilePath` if one exists, or the default configuration otherwise. If an error occurred
165168
/// when reading the configuration, a diagnostic is emitted and `nil` is returned.
166169
private func configuration(
167-
atPath configurationFilePath: String?,
168-
orInferredFromSwiftFileAtPath swiftFilePath: String?
170+
at configurationFileURL: URL?,
171+
orInferredFromSwiftFileAt swiftFileURL: URL?
169172
) -> Configuration? {
170173
// If an explicit configuration file path was given, try to load it and fail if it cannot be
171174
// loaded. (Do not try to fall back to a path inferred from the source file path.)
172-
if let configurationFilePath = configurationFilePath {
175+
if let configurationFileURL = configurationFileURL {
173176
do {
174-
return try configurationLoader.configuration(atPath: configurationFilePath)
177+
return try configurationLoader.configuration(at: configurationFileURL)
175178
} catch {
176179
diagnosticsEngine.emitError("Unable to read configuration: \(error.localizedDescription)")
177180
return nil
@@ -180,17 +183,15 @@ class Frontend {
180183

181184
// If no explicit configuration file path was given but a `.swift` source file path was given,
182185
// then try to load the configuration by inferring it based on the source file path.
183-
if let swiftFilePath = swiftFilePath {
186+
if let swiftFileURL = swiftFileURL {
184187
do {
185-
if let configuration =
186-
try configurationLoader.configuration(forSwiftFileAtPath: swiftFilePath)
187-
{
188+
if let configuration = try configurationLoader.configuration(forSwiftFileAt: swiftFileURL) {
188189
return configuration
189190
}
190191
// Fall through to the default return at the end of the function.
191192
} catch {
192193
diagnosticsEngine.emitError(
193-
"Unable to read configuration for \(swiftFilePath): \(error.localizedDescription)")
194+
"Unable to read configuration for \(swiftFileURL.path): \(error.localizedDescription)")
194195
return nil
195196
}
196197
}

Sources/swift-format/Frontend/LintFrontend.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,33 @@ class LintFrontend: Frontend {
2222
configuration: fileToProcess.configuration, findingConsumer: diagnosticsEngine.consumeFinding)
2323
linter.debugOptions = debugOptions
2424

25-
let path = fileToProcess.path
25+
let url = fileToProcess.url
2626
guard let source = fileToProcess.sourceText else {
27-
diagnosticsEngine.emitError("Unable to read source for linting from \(path).")
27+
diagnosticsEngine.emitError(
28+
"Unable to lint \(url.relativePath): file is not readable or does not exist.")
2829
return
2930
}
3031

3132
do {
32-
let assumingFileURL = URL(fileURLWithPath: path)
3333
try linter.lint(
3434
source: source,
35-
assumingFileURL: assumingFileURL,
35+
assumingFileURL: url,
3636
parsingDiagnosticHandler: diagnosticsEngine.consumeParserDiagnostic)
3737
} catch SwiftFormatError.fileNotReadable {
3838
diagnosticsEngine.emitError(
39-
"Unable to lint \(path): file is not readable or does not exist.")
39+
"Unable to lint \(url.relativePath): file is not readable or does not exist.")
4040
return
4141
} catch SwiftFormatError.fileContainsInvalidSyntax(let position) {
4242
guard !lintFormatOptions.ignoreUnparsableFiles else {
4343
// The caller wants to silently ignore this error.
4444
return
4545
}
46-
let location = SourceLocationConverter(file: path, source: source).location(for: position)
46+
let location = SourceLocationConverter(file: url.path, source: source).location(for: position)
4747
diagnosticsEngine.emitError(
4848
"file contains invalid or unrecognized Swift syntax.", location: location)
4949
return
5050
} catch {
51-
diagnosticsEngine.emitError("Unable to lint \(path): \(error)")
51+
diagnosticsEngine.emitError("Unable to lint \(url.relativePath): \(error)")
5252
return
5353
}
5454
}

0 commit comments

Comments
 (0)