diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index d31f98a02..35067789c 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -5,7 +5,7 @@ used as a command line tool or as an API. ## Command Line Configuration -A `swift-format` configuration file is a JSON file with the following +A `.swift-format` configuration file is a JSON file with the following top-level keys and values: * `version` _(number)_: The version of the configuration file. For now, this @@ -102,7 +102,7 @@ top-level keys and values: An example `.swift-format` configuration file is shown below. -```javascript +```json { "version": 1, "lineLength": 100, @@ -128,6 +128,24 @@ You can also run this command to see the list of rules in the default $ swift-format dump-configuration +## Global Configuration + +If no `.swift-format` can be found for the current project/file, the configuration directories +are searched for a `swift-format/config.json` file. While the filename is different, the +configuration format stays the same. + +Locations that are searched, in this order: + +- `$XDG_CONFIG_HOME/swift-format/config.json` +- `$HOME/Library/Application Support/swift-format/config.json` +- each path in `$XDG_CONFIG_DIRS` (system wide configuration) +- `/Library/Application Support/swift-format/config.json` (system wide configuration) + +or on windows: + +- `%LOCALAPPDATA%/swift-format/config.json` +- `%PROGRAMDATA%/swift-format/config.json` (system wide configuration) + ## API Configuration The `SwiftConfiguration` module contains a `Configuration` type that is diff --git a/README.md b/README.md index db2e090a9..8e5447961 100644 --- a/README.md +++ b/README.md @@ -170,10 +170,12 @@ subcommands: or off respectively, regardless of whether the output is going to a terminal. -* `--configuration `: The path to a JSON file that contains +* `--configuration `: The path to a JSON file that contains [configurable settings](#configuring-the-command-line-tool) for - `swift-format`. If omitted, a default configuration is use (which - can be seen by running `swift-format dump-configuration`). + `swift-format`. If no file is found, `swift-format` tries to load the json + data as a string, if valid. If the parameter is omitted, a default + configuration is used (which can be seen by running + `swift-format dump-configuration`). * `--ignore-unparsable-files`: If this option is specified and a source file contains syntax errors or can otherwise not be parsed successfully by the @@ -205,14 +207,18 @@ JSON-formatted file named `.swift-format` in the same directory. If one is found, then that file is loaded to determine the tool's configuration. If the file is not found, then it looks in the parent directory, and so on. +If there is no project specific configuration file, the +[config directories](Documentation/Configuration.md#Global-Configuration) +are checked for a `swift-format/config.json` configuration file. + If no configuration file is found, a default configuration is used. The settings in the default configuration can be viewed by running `swift-format dump-configuration`, which will dump it to standard output. -If the `--configuration ` option is passed to `swift-format`, then that -configuration will be used unconditionally and the file system will not be -searched. +If the `--configuration ` option is passed to `swift-format`, +then that configuration will be used unconditionally and the file system will +not be searched. See [Documentation/Configuration.md](Documentation/Configuration.md) for a description of the configuration file format and the settings that are diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index a3ea18a4f..4c260d7ce 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -206,7 +206,9 @@ class Frontend { /// it was provided, or by searching in paths inferred by `swiftFilePath` if one exists, or the /// default configuration otherwise. If an error occurred when reading the configuration, a /// diagnostic is emitted and `nil` is returned. If neither `pathOrString` nor `swiftFilePath` - /// were provided, a default `Configuration()` will be returned. + /// were provided, a configuration is searched at the current working directory or upwards the + /// path. Next the configuration is searched for at the OS default config locations as + /// swift-format/config.json. Finally the default `Configuration()` will be returned. private func configuration( fromPathOrString pathOrString: String?, orInferredFromSwiftFileAt swiftFileURL: URL? @@ -268,6 +270,64 @@ class Frontend { } } + // Load global configuration file + // First URLs are created, then they are queried. First match is loaded + var configLocations: [URL?] = [] + + #if os(Windows) + if let localAppData = ProcessInfo.processInfo.environment["LOCALAPPDATA"] { + configLocations.append(URL(fileURLWithPath: localAppData)) + } + if let programData = ProcessInfo.processInfo.environment["PROGRAMDATA"] { + configLocations.append(URL(fileURLWithPath: programData)) + } + #else + if let xdgConfigHome = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"] { + configLocations.append(URL(fileURLWithPath: xdgConfigHome)) + } else if let homeLocation = ProcessInfo.processInfo.environment["HOME"] { + let dotConfigUrl = URL(fileURLWithPath: homeLocation) + .appendingPathComponent(".config", isDirectory: true) + configLocations.append(dotConfigUrl) + } + + for supportDirectoryUrl in FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ) { + configLocations.append(supportDirectoryUrl) + } + + if let xdgConfigDirs = ProcessInfo.processInfo.environment["XDG_CONFIG_DIRS"] { + configLocations += xdgConfigDirs.split(separator: ":").map { xdgConfigDir in + URL(fileURLWithPath: String(xdgConfigDir)) + } + } + + if let libraryUrl = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .systemDomainMask + ).first { + configLocations.append(libraryUrl) + } + #endif + + for case var location? in configLocations { + location.appendPathComponent("swift-format", isDirectory: true) + location.appendPathComponent("config.json", isDirectory: false) + if FileManager.default.fileExists(atPath: location.path) { + do { + let configuration = try configurationLoader.configuration(at: location) + self.checkForUnrecognizedRules(in: configuration) + return configuration + } catch { + diagnosticsEngine.emitError( + "Unable to read configuration for \(location.path): \(error.localizedDescription)" + ) + return nil + } + } + } + // An explicit configuration has not been given, and one cannot be found. // Return the default configuration. return Configuration() @@ -281,7 +341,10 @@ class Frontend { // That way they will be printed out, but we'll continue execution on the valid rules. let invalidRules = configuration.rules.filter { !RuleRegistry.rules.keys.contains($0.key) } for rule in invalidRules { - diagnosticsEngine.emitWarning("Configuration contains an unrecognized rule: \(rule.key)", location: nil) + diagnosticsEngine.emitWarning( + "Configuration contains an unrecognized rule: \(rule.key)", + location: nil + ) } } }