Skip to content

Commit 8ed627e

Browse files
committed
Adds @_spi to RuleGenerator, and improves broken rule warnings.
1 parent ef9bc7d commit 8ed627e

File tree

3 files changed

+72
-35
lines changed

3 files changed

+72
-35
lines changed

Sources/swift-format/Frontend/Frontend.swift

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14-
@_spi(Internal) import SwiftFormat
1514
import SwiftSyntax
1615
import SwiftParser
1716

17+
@_spi(Internal) import SwiftFormat
18+
1819
class Frontend {
1920
/// Represents a file to be processed by the frontend and any file-specific options associated
2021
/// with it.
@@ -176,50 +177,80 @@ class Frontend {
176177
at configurationFileURL: URL?,
177178
orInferredFromSwiftFileAt swiftFileURL: URL?
178179
) -> Configuration? {
179-
// If an explicit configuration file path was given, try to load it and fail if it cannot be
180-
// loaded. (Do not try to fall back to a path inferred from the source file path.)
180+
// The actual config file that this frontend is going to use.
181+
let configURL: URL?
182+
183+
// If the config path is provided, use that.
184+
// Otherwise, try inferring config path based on the input .swift file path.
181185
if let configurationFileURL = configurationFileURL {
182-
do {
183-
let configuration = try configurationLoader.configuration(at: configurationFileURL)
184-
self.verifyConfigurationRules(for: configuration)
185-
return configuration
186-
} catch {
187-
diagnosticsEngine.emitError("Unable to read configuration: \(error.localizedDescription)")
188-
return nil
189-
}
186+
configURL = configurationFileURL
187+
} else if let swiftFileURL = swiftFileURL {
188+
configURL = Configuration.url(forConfigurationFileApplyingTo: swiftFileURL)
189+
} else {
190+
191+
// If neither path was given (for example, formatting standard input with no assumed filename)
192+
// or if there was no configuration found by inferring it from the source file path, return the
193+
// default configuration.
194+
return Configuration()
190195
}
196+
197+
// Now that we have the configURL, we're going to load it and validate the rules in it.
198+
do {
199+
// Ok to force unwrap because the only branch where configURL would not be set returned early.
200+
let configuration = try configurationLoader.configuration(at: configURL!)
201+
try self.verifyConfigurationRules(for: configuration, at: configURL!)
191202

192-
// If no explicit configuration file path was given but a `.swift` source file path was given,
193-
// then try to load the configuration by inferring it based on the source file path.
194-
if let swiftFileURL = swiftFileURL {
195-
do {
196-
if let configuration = try configurationLoader.configuration(forSwiftFileAt: swiftFileURL) {
197-
self.verifyConfigurationRules(for: configuration)
198-
return configuration
199-
}
200-
// Fall through to the default return at the end of the function.
201-
} catch {
203+
return configuration
204+
} catch {
205+
if let swiftFileURL {
202206
diagnosticsEngine.emitError(
203207
"Unable to read configuration for \(swiftFileURL.path): \(error.localizedDescription)")
204-
return nil
208+
} else {
209+
diagnosticsEngine.emitError("Unable to read configuration: \(error.localizedDescription)")
205210
}
211+
return nil
206212
}
207-
208-
// If neither path was given (for example, formatting standard input with no assumed filename)
209-
// or if there was no configuration found by inferring it from the source file path, return the
210-
// default configuration.
211-
return Configuration()
212213
}
213214

214215
/// Checks if all the rules in the given configuration are supported by the registry.
215-
/// If there are any rules that are not supported, they are emitted as a warning.
216-
private func verifyConfigurationRules(for configuration: Configuration) {
216+
/// If there are any rules that are not recognized by this version of swift-format, they are emitted as a warning.
217+
///
218+
/// - parameters:
219+
/// - configuration: `Configuration` to verify
220+
/// - configURL: Configuration file `URL` to use in `Diagnostic.Location`
221+
private func verifyConfigurationRules(for configuration: Configuration, at configURL: URL) throws {
217222
// If any rules in the decoded configuration are not supported by the registry,
218223
// emit them into the diagnosticsEngine as warnings.
219-
// That way they will be printed out, but we'll continue execution on the valid rules.
220224
let invalidRules = configuration.rules.filter { !RuleRegistry.rules.keys.contains($0.key) }
221-
if !invalidRules.isEmpty {
222-
diagnosticsEngine.emitWarning("Configuration contains an unrecognized rule: \(invalidRules.keys.joined(separator: ", "))", location: nil)
225+
226+
let configData = try Data(contentsOf: configURL)
227+
guard let configString = String(data: configData, encoding: .utf8) else {
228+
diagnosticsEngine.emitError("Unable to read the configuration file at \(configURL.description)")
229+
return
230+
}
231+
232+
for rule in invalidRules {
233+
let location = diagnosticLocationFor(substring: rule.key, in: configString, filePath: configURL.relativePath)
234+
diagnosticsEngine.emitWarning(
235+
"Configuration contains an unrecognized rule \(rule.key)",
236+
location: location
237+
)
223238
}
224239
}
240+
241+
/// Returns a `Diagnostic.Location` of the substring in an array of Strings.
242+
/// It's intented to find the location of a configuration Rule in an array of Strings representing the encoded JSON swift-format configuration file.
243+
private func diagnosticLocationFor(substring: String, in configuration: String, filePath: String) -> Diagnostic.Location? {
244+
if let offset = configuration.range(of: substring)?.lowerBound {
245+
let splits = configuration.prefix(through: offset).split(separator: "\n")
246+
let line = splits.count
247+
248+
// In theory, that should never happen, but if we can't fetch the column, just use 1
249+
let column = splits.last?.count ?? 1
250+
return Diagnostic.Location(file: filePath, line: line, column: column)
251+
}
252+
253+
// If the config string did not contain the substring at all, return nil.
254+
return nil
255+
}
225256
}

Sources/swift-format/Utilities/Diagnostic.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ struct Diagnostic {
4747
self.line = findingLocation.line
4848
self.column = findingLocation.column
4949
}
50+
51+
init(file: String, line: Int, column: Int) {
52+
self.file = file
53+
self.line = line
54+
self.column = column
55+
}
5056
}
5157

5258
/// The severity of the diagnostic.

Sources/swift-format/Utilities/DiagnosticsEngine.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ final class DiagnosticsEngine {
6969
///
7070
/// - Parameters:
7171
/// - message: The message associated with the error.
72-
/// - location: The location in the source code associated with the error, or nil if there is no
72+
/// - location: The `Diagnostic.Location` in the source code associated with the error, or nil if there is no
7373
/// location associated with the error.
74-
func emitWarning(_ message: String, location: SourceLocation? = nil) {
74+
func emitWarning(_ message: String, location: Diagnostic.Location? = nil) {
7575
emit(
7676
Diagnostic(
7777
severity: .warning,
78-
location: location.map(Diagnostic.Location.init),
78+
location: location,
7979
message: message))
8080
}
8181

0 commit comments

Comments
 (0)