|
11 | 11 | //===----------------------------------------------------------------------===//
|
12 | 12 |
|
13 | 13 | import Foundation
|
14 |
| -@_spi(Internal) import SwiftFormat |
15 | 14 | import SwiftSyntax
|
16 | 15 | import SwiftParser
|
17 | 16 |
|
| 17 | +@_spi(Internal) import SwiftFormat |
| 18 | + |
18 | 19 | class Frontend {
|
19 | 20 | /// Represents a file to be processed by the frontend and any file-specific options associated
|
20 | 21 | /// with it.
|
@@ -176,50 +177,80 @@ class Frontend {
|
176 | 177 | at configurationFileURL: URL?,
|
177 | 178 | orInferredFromSwiftFileAt swiftFileURL: URL?
|
178 | 179 | ) -> 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. |
181 | 185 | 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() |
190 | 195 | }
|
| 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!) |
191 | 202 |
|
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 { |
202 | 206 | diagnosticsEngine.emitError(
|
203 | 207 | "Unable to read configuration for \(swiftFileURL.path): \(error.localizedDescription)")
|
204 |
| - return nil |
| 208 | + } else { |
| 209 | + diagnosticsEngine.emitError("Unable to read configuration: \(error.localizedDescription)") |
205 | 210 | }
|
| 211 | + return nil |
206 | 212 | }
|
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() |
212 | 213 | }
|
213 | 214 |
|
214 | 215 | /// 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 { |
217 | 222 | // If any rules in the decoded configuration are not supported by the registry,
|
218 | 223 | // emit them into the diagnosticsEngine as warnings.
|
219 |
| - // That way they will be printed out, but we'll continue execution on the valid rules. |
220 | 224 | 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 | + ) |
223 | 238 | }
|
224 | 239 | }
|
| 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 | + } |
225 | 256 | }
|
0 commit comments