Skip to content

Commit

Permalink
Merge pull request #307 from realm/jp-sh-recursive-configs
Browse files Browse the repository at this point in the history
Recursive Configs
  • Loading branch information
jpsim committed Jan 3, 2016
2 parents 029a5eb + 1e553a5 commit c6f464a
Show file tree
Hide file tree
Showing 19 changed files with 387 additions and 19 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
[JP Simard](https://github.com/jpsim)
[#222](https://github.com/realm/SwiftLint/issues/222)

* Add nested `.swiftlint.yml` configuration support.
[Scott Hoyt](https://github.com/scottrhoyt)
[#299](https://github.com/realm/SwiftLint/issues/299)

##### Bug Fixes

* Fix multibyte handling in many rules.
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ directory to see the currently implemented rules.
### Disable a rule in code

Rules can be disabled with a comment inside a source file with the following
format:
format:

`// swiftlint:disable <rule>`

Expand Down Expand Up @@ -146,6 +146,20 @@ type_body_length:
reporter: "csv" # reporter type (xcode, json, csv, checkstyle)
```
#### Nested Configurations
SwiftLint supports nesting configuration files for more granular control over
the linting process.
* Set the `use_nested_configs: true` value in your root `.swiftlint.yml` file
* 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.
* `excluded`, `included`, and `use_nested_configs` are ignored for nested
configurations.

### Auto-correct

SwiftLint can automatically correct certain violations. Files on disk are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension NSFileManager {
}

public func filesToLintAtPath(path: String) -> [String] {
let absolutePath = (path.absolutePathRepresentation() as NSString).stringByStandardizingPath
let absolutePath = path.absolutePathStandardized()
var isDirectory: ObjCBool = false
guard fileExistsAtPath(absolutePath, isDirectory: &isDirectory) else {
return []
Expand Down
4 changes: 4 additions & 0 deletions Source/SwiftLintFramework/Extensions/String+SwiftLint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,8 @@ extension String {
}
return nil
}

public func absolutePathStandardized() -> String {
return (self.absolutePathRepresentation() as NSString).stringByStandardizingPath
}
}
95 changes: 83 additions & 12 deletions Source/SwiftLintFramework/Models/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ extension Yaml {
}
}

public struct Configuration {
public struct Configuration: Equatable {
public let disabledRules: [String] // disabled_rules
public let included: [String] // included
public let excluded: [String] // excluded
public let reporter: String // reporter (xcode, json, csv, checkstyle)
public let rules: [Rule]
public let useNestedConfigs: Bool // process nested configs, will default to false
public var rootPath: String? // the root path of the lint to search for nested configs
private var configPath: String? // if successfully load from a path

public var reporterFromString: Reporter.Type {
switch reporter {
Expand All @@ -47,10 +50,12 @@ public struct Configuration {
included: [String] = [],
excluded: [String] = [],
reporter: String = "xcode",
rules: [Rule] = Configuration.rulesFromYAML()) {
rules: [Rule] = Configuration.rulesFromYAML(),
useNestedConfigs: Bool = false) {
self.included = included
self.excluded = excluded
self.reporter = reporter
self.useNestedConfigs = useNestedConfigs

// Validate that all rule identifiers map to a defined rule

Expand Down Expand Up @@ -89,23 +94,35 @@ public struct Configuration {
}

public init?(yaml: String) {
let yamlResult = Yaml.load(yaml)
guard let yamlConfig = yamlResult.value else {
if let error = yamlResult.error {
queuedPrint(error)
}
guard let yamlConfig = Configuration.loadYaml(yaml) else {
return nil
}
self.init(yamlConfig: yamlConfig)
}

private init?(yamlConfig: Yaml) {
self.init(
disabledRules: yamlConfig["disabled_rules"].arrayOfStrings ?? [],
included: yamlConfig["included"].arrayOfStrings ?? [],
excluded: yamlConfig["excluded"].arrayOfStrings ?? [],
reporter: yamlConfig["reporter"].string ?? XcodeReporter.identifier,
rules: Configuration.rulesFromYAML(yamlConfig)
rules: Configuration.rulesFromYAML(yamlConfig),
useNestedConfigs: yamlConfig["use_nested_configs"].bool ?? false
)
}

public init(path: String = ".swiftlint.yml", optional: Bool = true) {
private static func loadYaml(yaml: String) -> Yaml? {
let yamlResult = Yaml.load(yaml)
if let yamlConfig = yamlResult.value {
return yamlConfig
}
if let error = yamlResult.error {
queuedPrint(error)
}
return nil
}

public init(path: String = ".swiftlint.yml", optional: Bool = true, silent: Bool = false) {
let fullPath = (path as NSString).absolutePathRepresentation()
let failIfRequired = {
if !optional { fatalError("Could not read configuration file at path '\(fullPath)'") }
Expand All @@ -118,9 +135,12 @@ public struct Configuration {
do {
let yamlContents = try NSString(contentsOfFile: fullPath,
encoding: NSUTF8StringEncoding) as String
if let _ = Configuration(yaml: yamlContents) {
queuedPrintError("Loading configuration from '\(path)'")
self.init(yaml: yamlContents)!
if let yamlConfig = Configuration.loadYaml(yamlContents) {
if !silent {
queuedPrintError("Loading configuration from '\(path)'")
}
self.init(yamlConfig: yamlConfig)!
configPath = fullPath
return
} else {
failIfRequired()
Expand Down Expand Up @@ -188,4 +208,55 @@ public struct Configuration {
let allPaths = self.lintablePathsForPath(path)
return allPaths.flatMap { File(path: $0) }
}

public func configForFile(file: File) -> Configuration {
if useNestedConfigs,
let containingDir = (file.path as NSString?)?.stringByDeletingLastPathComponent {
return configForPath(containingDir)
}
return self
}
}

// MARK: - Nested Configurations Extension

public extension Configuration {
func configForPath(path: String) -> Configuration {
let path = path as NSString
let configSearchPath = path.stringByAppendingPathComponent(".swiftlint.yml")

// If a config exists and it isn't us, load and merge the configs
if configSearchPath != configPath &&
NSFileManager.defaultManager().fileExistsAtPath(configSearchPath) {
return merge(Configuration(path: configSearchPath, optional: false, silent: true))
}

// If we are not at the root path, continue down the tree
if path != rootPath {
return configForPath(path.stringByDeletingLastPathComponent)
}

// If nothing else, return self
return self
}

// Currently merge simply overrides the current configuration with the new configuration.
// This requires that all config files be fully specified. In the future this will be changed
// to do a more intelligent merge allowing for partial nested configs.
func merge(config: Configuration) -> Configuration {
return config
}
}

// Mark - == Implementation

public func == (lhs: Configuration, rhs: Configuration) -> Bool {
return (lhs.disabledRules == rhs.disabledRules) &&
(lhs.excluded == rhs.excluded) &&
(lhs.included == rhs.included) &&
(lhs.reporter == rhs.reporter) &&
(lhs.useNestedConfigs == rhs.useNestedConfigs) &&
(lhs.configPath == rhs.configPath) &&
(lhs.rootPath == lhs.rootPath) &&
(lhs.rules == rhs.rules)
}
8 changes: 7 additions & 1 deletion Source/SwiftLintFramework/Models/RuleParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Copyright (c) 2015 Realm. All rights reserved.
//

public struct RuleParameter<T> {
public struct RuleParameter<T: Equatable>: Equatable {
public let severity: ViolationSeverity
public let value: T

Expand All @@ -15,3 +15,9 @@ public struct RuleParameter<T> {
self.value = value
}
}

// MARK: - Equatable

public func ==<T: Equatable>(lhs: RuleParameter<T>, rhs: RuleParameter<T>) -> Bool {
return lhs.value == rhs.value && lhs.severity == rhs.severity
}
25 changes: 24 additions & 1 deletion Source/SwiftLintFramework/Protocols/Rule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,35 @@ public protocol Rule {
func validateFile(file: File) -> [StyleViolation]
}

extension Rule {
func isEqualTo(rule: Rule) -> Bool {
return self.dynamicType.description == rule.dynamicType.description
}
}

public protocol ParameterizedRule: Rule {
typealias ParameterType
typealias ParameterType: Equatable
init(parameters: [RuleParameter<ParameterType>])
var parameters: [RuleParameter<ParameterType>] { get }
}

extension ParameterizedRule {
func isEqualTo(rule: Self) -> Bool {
return (self.dynamicType.description == rule.dynamicType.description) &&
(self.parameters == rule.parameters)
}
}

public protocol CorrectableRule: Rule {
func correctFile(file: File) -> [Correction]
}

// MARK: - == Implementations

func == (lhs: [Rule], rhs: [Rule]) -> Bool {
if lhs.count == rhs.count {
return zip(lhs, rhs).map { $0.isEqualTo($1) }.reduce(true) { $0 && $1 }
}

return false
}
108 changes: 108 additions & 0 deletions Source/SwiftLintFrameworkTests/ConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import SwiftLintFramework
import SourceKittenFramework
import XCTest

class ConfigurationTests: XCTestCase {
Expand Down Expand Up @@ -89,4 +90,111 @@ class ConfigurationTests: XCTestCase {
let paths = configuration.lintablePathsForPath("", fileManager: TestFileManager())
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"], paths)
}

// MARK: - Testing Configuration Equality

private var projectMockConfig0: Configuration {
var config = Configuration(path: projectMockYAML0, optional: false, silent: true)
config.rootPath = projectMockPathLevel0
return config
}

private var projectMockConfig2: Configuration {
return Configuration(path: projectMockYAML2, optional: false, silent: true)
}

func testIsEqualTo() {
XCTAssertEqual(projectMockConfig0, projectMockConfig0)
}

func testIsNotEqualTo() {
XCTAssertNotEqual(projectMockConfig0, projectMockConfig2)
}

// MARK: - Testing Nested Configurations

func testMerge() {
XCTAssertEqual(projectMockConfig0.merge(projectMockConfig2), projectMockConfig2)
}

func testLevel0() {
XCTAssertEqual(projectMockConfig0.configForFile(File(path: projectMockSwift0)!),
projectMockConfig0)
}

func testLevel1() {
XCTAssertEqual(projectMockConfig0.configForFile(File(path: projectMockSwift1)!),
projectMockConfig0)
}

func testLevel2() {
XCTAssertEqual(projectMockConfig0.configForFile(File(path: projectMockSwift2)!),
projectMockConfig0.merge(projectMockConfig2))
}

func testLevel3() {
XCTAssertEqual(projectMockConfig0.configForFile(File(path: projectMockSwift3)!),
projectMockConfig0.merge(projectMockConfig2))
}

func testDoNotUseNestedConfigs() {
var config = Configuration(yaml: "use_nested_configs: false\n")!
config.rootPath = projectMockPathLevel0
XCTAssertEqual(config.configForFile(File(path: projectMockSwift3)!),
config)
}
}

// MARK: - ProjectMock Paths

extension String {
func stringByAppendingPathComponent(pathComponent: String) -> String {
return (self as NSString).stringByAppendingPathComponent(pathComponent)
}
}

extension XCTestCase {
var bundlePath: String {
return NSBundle(forClass: self.dynamicType).resourcePath!
}

var projectMockPathLevel0: String {
return bundlePath.stringByAppendingPathComponent("ProjectMock")
}

var projectMockPathLevel1: String {
return projectMockPathLevel0.stringByAppendingPathComponent("Level1")
}

var projectMockPathLevel2: String {
return projectMockPathLevel1.stringByAppendingPathComponent("Level2")
}

var projectMockPathLevel3: String {
return projectMockPathLevel2.stringByAppendingPathComponent("Level3")
}

var projectMockYAML0: String {
return projectMockPathLevel0.stringByAppendingPathComponent(".swiftlint.yml")
}

var projectMockYAML2: String {
return projectMockPathLevel2.stringByAppendingPathComponent(".swiftlint.yml")
}

var projectMockSwift0: String {
return projectMockPathLevel0.stringByAppendingPathComponent("Level0.swift")
}

var projectMockSwift1: String {
return projectMockPathLevel1.stringByAppendingPathComponent("Level1.swift")
}

var projectMockSwift2: String {
return projectMockPathLevel2.stringByAppendingPathComponent("Level2.swift")
}

var projectMockSwift3: String {
return projectMockPathLevel3.stringByAppendingPathComponent("Level3.swift")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
disabled_rules:
- force_cast
included:
- "everything"
exluded:
- "the place where i committed many coding sins"
line_length: 10000000000
reporter: "json"
use_nested_configs: true
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// This is just a mock Swift file
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// This is just a mock Swift file
Loading

0 comments on commit c6f464a

Please sign in to comment.