Skip to content

Add remote, parent & child configuration features #3058

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

Merged
merged 5 commits into from
Nov 22, 2020
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@
within a function even if breaking the maximum `type_level`.
[Skoti](https://github.com/Skoti)
[#1151](https://github.com/realm/SwiftLint/issues/1151)

* Add option to specify a `child_config` / `parent_config` file
(local or remote) in any SwiftLint configuration file.
Allow passing multiple configuration files via the command line.
Improve documentation for multiple configuration files.
[Frederick Pietschmann](https://github.com/fredpi)
[#1352](https://github.com/realm/SwiftLint/issues/1352)

* Add an `always_keep_imports` configuration option for the
`unused_import` rule.
Expand Down
123 changes: 110 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ Here's a reference of which SwiftLint version to use for a given Swift version.

## Rules

Over 75 rules are included in SwiftLint and the Swift community (that's you!)
Over 100 rules are included in SwiftLint and the Swift community (that's you!)
continues to contribute more over time.
[Pull requests](CONTRIBUTING.md) are encouraged.

Expand Down Expand Up @@ -456,18 +456,6 @@ are all the possible syntax kinds:
If using custom rules in combination with `only_rules`, make sure to add
`custom_rules` as an item under `only_rules`.

#### Nested Configurations

SwiftLint supports nesting configuration files for more granular control over
the linting process.

* Include additional `.swiftlint.yml` files where necessary in your directory
structure.
* Each file will be linted using the configuration file that is in its
directory or at the deepest level of its parent directories. Otherwise the
root configuration will be used.
* `included` is ignored for nested configurations.

### Auto-correct

SwiftLint can automatically correct certain violations. Files on disk are
Expand Down Expand Up @@ -496,6 +484,115 @@ This command and related code in SwiftLint is subject to substantial changes at
any time while this feature is marked as experimental. Analyzer rules also tend
to be considerably slower than lint rules.

## Using Multiple Configuration Files

SwiftLint offers a variety of ways to include multiple configuration files.
Multiple configuration files get merged into one single configuration that is then applied
just as a single configuration file would get applied.

There are quite a lot of use cases where using multiple configuration files could be helpful:

For instance, one could use a team-wide shared SwiftLint configuration while allowing overrrides
in each project via a child configuration file.

Team-Wide Configuration:

```yaml
disabled_rules:
- force_cast
```

Project-Specific Configuration:

```yaml
opt_in_rules:
- force_cast
```

### Child / Parent Configs (Locally)

You can specify a `child_config` and / or a `parent_config` reference within a configuration file.
These references should be local paths relative to the folder of the configuration file they are specified in.
This even works recursively, as long as there are no cycles and no ambiguities.

**A child config is treated as a refinement and therefore has a higher priority**,
while a parent config is considered a base with lower priority in case of conflicts.

Here's an example, assuming you have the following file structure:

```
ProjectRoot
|_ .swiftlint.yml
|_ .swiftlint_refinement.yml
|_ Base
|_ .swiftlint_base.yml
```

To include both the refinement and the base file, your `.swiftlint.yml` should look like this:

```yaml
child_config: .swiftlint_refinement.yml
parent_config: Base/.swiftlint_base.yml
```

When merging parent and child configs, `included` and `excluded` configurations
are processed carefully to account for differences in the directory location
of the containing configuration files.

### Child / Parent Configs (Remote)

Just as you can provide local `child_config` / `parent_config` references, instead of
referencing local paths, you can just put urls that lead to configuration files.
In order for SwiftLint to detect these remote references, they must start with `http://` or `https://`.

The referenced remote configuration files may even recursively reference other
remote configuration files, but aren't allowed to include local references.

Using a remote reference, your `.swiftlint.yml` could look like this:

```yaml
parent_config: https://myteamserver.com/our-base-swiftlint-config.yml
```

Every time you run SwiftLint and have an Internet connection, SwiftLint tries to get a new version of
every remote configuration that is referenced. If this request times out, a cached version is
used if available. If there is no cached version available, SwiftLint fails – but no worries, a cached version
should be there once SwiftLint has run successfully at least once.

If needed, the timeouts for the remote configuration fetching can be specified manually via the
configuration file(s) using the `remote_timeout` / `remote_timeout_if_cached` specifiers.
These values default to 2 / 1 second(s).

### Command Line

Instead of just providing one configuration file when running SwiftLint via the command line,
you can also pass a hierarchy, where the first configuration is treated as a parent,
while the last one is treated as the highest-priority child.

A simple example including just two configuration files looks like this:

`swiftlint --config ".swiftlint.yml .swiftlint_child.yml"`

### Nested Configurations

In addition to a main configuration (the `.swiftlint.yml` file in the root folder),
you can put other configuration files named `.swiftlint.yml` into the directory structure
that then get merged as a child config, but only with an effect for those files
that are within the same directory as the config or in a deeper directory where
there isn't another configuration file. In other words: Nested configurations don't work
recursively – there's a maximum number of one nested configuration per file
that may be applied in addition to the main configuration.

`.swiftlint.yml` files are only considered as a nested configuration if they have not been
used to build the main configuration already (e. g. by having been referenced via something
like `child_config: Folder/.swiftlint.yml`). Also, `parent_config` / `child_config`
specifications of nested configurations are getting ignored because there's no sense to that.

If one (or more) SwiftLint file(s) are explicitly specified via the `--config` parameter,
that configuration will be treated as an override, no matter whether there exist
other `.swiftlint.yml` files somewhere within the directory. **So if you want to use
use nested configurations, you can't use the `-- config` parameter.**

## License

[MIT licensed.](LICENSE)
Expand Down
13 changes: 13 additions & 0 deletions Source/SwiftLintFramework/Extensions/Array+SwiftLint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ extension Array where Element: Equatable {
}
}

extension Array where Element: Hashable {
static func array(of obj: Any?) -> [Element]? {
if let array = obj as? [Element] {
return array
} else if let set = obj as? Set<Element> {
return Array(set)
} else if let obj = obj as? Element {
return [obj]
}
return nil
}
}

extension Array {
static func array(of obj: Any?) -> [Element]? {
if let array = obj as? [Element] {
Expand Down
53 changes: 30 additions & 23 deletions Source/SwiftLintFramework/Extensions/Configuration+Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@ import CryptoSwift
import Foundation

extension Configuration {
// MARK: Caching Configurations By Path (In-Memory)
// MARK: Caching Configurations By Identifier (In-Memory)
private static var cachedConfigurationsByIdentifier = [String: Configuration]()
private static var cachedConfigurationsByIdentifierLock = NSLock()

private static var cachedConfigurationsByPath = [String: Configuration]()
private static var cachedConfigurationsByPathLock = NSLock()

internal func setCached(atPath path: String) {
Configuration.cachedConfigurationsByPathLock.lock()
Configuration.cachedConfigurationsByPath[path] = self
Configuration.cachedConfigurationsByPathLock.unlock()
internal func setCached(forIdentifier identifier: String) {
Configuration.cachedConfigurationsByIdentifierLock.lock()
Configuration.cachedConfigurationsByIdentifier[identifier] = self
Configuration.cachedConfigurationsByIdentifierLock.unlock()
}

internal static func getCached(atPath path: String) -> Configuration? {
cachedConfigurationsByPathLock.lock()
defer { cachedConfigurationsByPathLock.unlock() }
return cachedConfigurationsByPath[path]
internal static func getCached(forIdentifier identifier: String) -> Configuration? {
cachedConfigurationsByIdentifierLock.lock()
defer { cachedConfigurationsByIdentifierLock.unlock() }
return cachedConfigurationsByIdentifier[identifier]
}

/// Returns a copy of the current `Configuration` with its `computedCacheDescription` property set to the value of
Expand All @@ -31,24 +30,32 @@ extension Configuration {
return result
}

// MARK: SwiftLint Cache (On-Disk)
// MARK: Nested Config Is Self Cache
private static var nestedConfigIsSelfByIdentifier = [String: Bool]()
private static var nestedConfigIsSelfByIdentifierLock = NSLock()

internal static func setIsNestedConfigurationSelf(forIdentifier identifier: String, value: Bool) {
Configuration.nestedConfigIsSelfByIdentifierLock.lock()
Configuration.nestedConfigIsSelfByIdentifier[identifier] = value
Configuration.nestedConfigIsSelfByIdentifierLock.unlock()
}

internal static func getIsNestedConfigurationSelf(forIdentifier identifier: String) -> Bool? {
Configuration.nestedConfigIsSelfByIdentifierLock.lock()
defer { Configuration.nestedConfigIsSelfByIdentifierLock.unlock() }
return Configuration.nestedConfigIsSelfByIdentifier[identifier]
}

// MARK: SwiftLint Cache (On-Disk)
internal var cacheDescription: String {
if let computedCacheDescription = computedCacheDescription {
return computedCacheDescription
}

let cacheRulesDescriptions = rules
.map { rule in
return [type(of: rule).description.identifier, rule.cacheDescription]
}
.sorted { rule1, rule2 in
return rule1[0] < rule2[0]
}
let jsonObject: [Any] = [
rootPath ?? FileManager.default.currentDirectoryPath,
cacheRulesDescriptions
]
.map { rule in [type(of: rule).description.identifier, rule.cacheDescription] }
.sorted { $0[0] < $1[0] }
let jsonObject: [Any] = [rootDirectory, cacheRulesDescriptions]
if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject),
let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString.md5()
Expand Down
Loading