Skip to content

Commit

Permalink
Merge pull request realm#314 from realm/sh-dictionary-parameters
Browse files Browse the repository at this point in the history
Dictionary Parameterization of rules
  • Loading branch information
jpsim committed Jan 12, 2016
2 parents 8b66171 + 47f4889 commit 9d138ad
Show file tree
Hide file tree
Showing 39 changed files with 692 additions and 167 deletions.
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,24 @@

##### Breaking

* None.
* `ParameterizedRule` is deprecated. Use `ConfigurableRule` instead.
[Scott Hoyt](https://github.com/scottrhoyt)

* To activate a `Rule`, it must be added to the global `masterRuleList`.
[Scott Hoyt](https://github.com/scottrhoyt)

##### Enhancements

* `ConfigurableRule` protocol allows for improved rule configuration. See
`CONTRIBUTING` for more details.
[Scott Hoyt](https://github.com/scottrhoyt)
[#303](https://github.com/realm/SwiftLint/issues/303)

* `VariableNameMinLengthRule` now supports excluding certain variable names
(e.g. "id")
[Scott Hoyt](https://github.com/scottrhoyt)
[#231](https://github.com/realm/SwiftLint/issues/231)

* Add AutoCorrect for StatementPositionRule.
[Raphael Randschau](https://github.com/nicolai86)

Expand Down
36 changes: 34 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

New rules should be added in the `Source/SwiftLintFramework/Rules` directory.

Rules should conform to either the `Rule`, `ASTRule` or `ParameterizedRule`
protocols.
Rules should conform to either the `Rule`, `ASTRule` or `ConfigurableRule`
protocols. To activate a rule, add the rule to `masterRuleList` in
`RuleList.swift`.

All new rules or changes to existing rules should be accompanied by unit tests.

Expand All @@ -13,6 +14,37 @@ those test cases in the unit tests directly. This makes it easier to understand
what rules do by reading their source, and simplifies adding more test cases
over time.

### `ConfigurableRule`

If your rule supports user-configurable options via `.swiftlint.yml`, you can
accomplish this by conforming to `ConfigurableRule`:

* `init?(config: AnyObject)` will be passed the result of parsing the value
from `.swiftlint.yml` associated with your rule's `identifier` as a key (if
present).
* `config` may be of any type supported by YAML (e.g. `Int`, `String`, `Array`,
`Dictionary`, etc.).
* This initializer must fail if it does not understand the configuration, or
it cannot be fully initialized with the configuration.
* If this initializer fails, your rule will be initialized with its default
values by calling `init()`.

See [VariableNameMinLengthRule](https://github.com/realm/SwiftLint/blob/647371517e57de3499a77781e45f181605b21045/Source/SwiftLintFramework/Rules/VariableNameMinLengthRule.swift)
for an example that supports the following configurations:

``` yaml
variable_name_min_length: 3

variable_name_min_length:
- 3
- 2

variable_name_min_length:
warning: 3
error: 2
excluded: id
```
## Tracking changes
All changes should be made via pull requests on GitHub.
Expand Down
20 changes: 20 additions & 0 deletions Source/SwiftLintFramework/Extensions/Array+SwiftLint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Array+SwiftLint.swift
// SwiftLint
//
// Created by Scott Hoyt on 1/11/16.
// Copyright © 2016 Realm. All rights reserved.
//

import Foundation

extension Array {
static func arrayOf(obj: AnyObject?) -> [Element]? {
if let array = obj as? [Element] {
return array
} else if let obj = obj as? Element {
return [obj]
}
return nil
}
}
65 changes: 65 additions & 0 deletions Source/SwiftLintFramework/Extensions/Yaml+SwiftLint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// Yaml+SwiftLint.swift
// SwiftLint
//
// Created by Scott Hoyt on 12/28/15.
// Copyright © 2015 Realm. All rights reserved.
//

import Yaml

extension Yaml {
var flatDictionary: [Swift.String : AnyObject]? {
if let dict = dictionary {
var newDict: [Swift.String : AnyObject] = [:]
for (key, value) in dict {
newDict[key.stringValue] = value.flatValue
}
return newDict
} else if self.count == 0 || self == .Null {
return [:]
}

return nil
}

var flatArray: [AnyObject]? { return array?.map { $0.flatValue } }

var flatValue: AnyObject {
switch self {
case .Bool(let myBool):
return myBool
case .Int(let myInt):
return myInt
case .Double(let myDouble):
return myDouble
case .String(let myString):
return myString
case .Array:
return flatArray! // This is valid because .Array will always flatten
case .Dictionary:
return flatDictionary! // This is valid because .Dictionary will always flatten
case .Null:
return NSNull()
}
}

var stringValue: Swift.String {
switch self {
case .Bool(let myBool):
return myBool.description
case .Int(let myInt):
return myInt.description
case .Double(let myDouble):
return myDouble.description
case .String(let myString):
return myString
case .Array(let myArray):
return myArray.description
case .Dictionary(let myDictionary):
return myDictionary.description
case .Null:
return "Null"
}
}
}
120 changes: 32 additions & 88 deletions Source/SwiftLintFramework/Models/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,9 @@

import Foundation
import SourceKittenFramework
import Yaml

private let fileManager = NSFileManager.defaultManager()

extension Yaml {
var arrayOfStrings: [Swift.String]? {
return array?.flatMap { $0.string } ?? string.map { [$0] }
}
var arrayOfInts: [Swift.Int]? {
return array?.flatMap { $0.int } ?? int.map { [$0] }
}
}

public struct Configuration: Equatable {
public let disabledRules: [String] // disabled_rules
public let included: [String] // included
Expand Down Expand Up @@ -50,7 +40,7 @@ public struct Configuration: Equatable {
included: [String] = [],
excluded: [String] = [],
reporter: String = "xcode",
rules: [Rule] = Configuration.rulesFromYAML(),
rules: [Rule] = Configuration.rulesFromDict(),
useNestedConfigs: Bool = false) {
self.included = included
self.excluded = excluded
Expand All @@ -59,7 +49,7 @@ public struct Configuration: Equatable {

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

let validRuleIdentifiers = Configuration.rulesFromYAML().map {
let validRuleIdentifiers = Configuration.rulesFromDict().map {
$0.dynamicType.description.identifier
}

Expand Down Expand Up @@ -93,35 +83,17 @@ public struct Configuration: Equatable {
}
}

public init?(yaml: String) {
guard let yamlConfig = Configuration.loadYaml(yaml) else {
return nil
}
self.init(yamlConfig: yamlConfig)
}

private init?(yamlConfig: Yaml) {
public init?(dict: [String: AnyObject]) {
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),
useNestedConfigs: yamlConfig["use_nested_configs"].bool ?? false
disabledRules: dict["disabled_rules"] as? [String] ?? [],
included: dict["included"] as? [String] ?? [],
excluded: dict["excluded"] as? [String] ?? [],
reporter: dict["reporter"] as? String ?? XcodeReporter.identifier,
useNestedConfigs: dict["use_nested_configs"] as? Bool ?? false,
rules: Configuration.rulesFromDict(dict)
)
}

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 fail = { fatalError("Could not read configuration file at path '\(fullPath)'") }
Expand All @@ -133,66 +105,38 @@ public struct Configuration: Equatable {
do {
let yamlContents = try NSString(contentsOfFile: fullPath,
encoding: NSUTF8StringEncoding) as String
if let yamlConfig = Configuration.loadYaml(yamlContents) {
if !silent {
queuedPrintError("Loading configuration from '\(path)'")
}
self.init(yamlConfig: yamlConfig)!
configPath = fullPath
return
} else {
fail()
let dict = try YamlParser.parse(yamlContents)
if !silent {
queuedPrintError("Loading configuration from '\(path)'")
}
self.init(dict: dict)!
configPath = fullPath
return
} catch {
fail()
}
self.init()!
}

public static func rulesFromYAML(yaml: Yaml? = nil) -> [Rule] {
return [
ClosingBraceRule(),
ColonRule(),
CommaRule(),
ConditionalBindingCascadeRule(),
ControlStatementRule(),
ForceCastRule(),
ForceTryRule(),
LeadingWhitespaceRule(),
LegacyConstructorRule(),
NestingRule(),
OpeningBraceRule(),
OperatorFunctionWhitespaceRule(),
ReturnArrowWhitespaceRule(),
StatementPositionRule(),
TodoRule(),
TrailingNewlineRule(),
TrailingSemicolonRule(),
TrailingWhitespaceRule(),
TypeNameRule(),
ValidDocsRule(),
VariableNameRule(),
] + parameterRulesFromYAML(yaml)
}

private static func parameterRulesFromYAML(yaml: Yaml? = nil) -> [Rule] {
let intParams: (Rule.Type) -> [RuleParameter<Int>]? = {
(yaml?[.String($0.description.identifier)].arrayOfInts).map(ruleParametersFromArray)
public static func rulesFromDict(dict: [String: AnyObject]? = nil,
ruleList: RuleList = masterRuleList) -> [Rule] {
var rules = [Rule]()
for rule in ruleList.list.values {
let identifier = rule.description.identifier
if let ConfigurableRuleType = rule as? ConfigurableRule.Type,
ruleConfig = dict?[identifier] {
if let configuredRule = ConfigurableRuleType.init(config: ruleConfig) {
rules.append(configuredRule)
} else {
queuedPrintError("Invalid config for '\(identifier)'. Falling back to default.")
rules.append(rule.init())
}
} else {
rules.append(rule.init())
}
}
// swiftlint:disable line_length
return [
intParams(FileLengthRule).map(FileLengthRule.init) ?? FileLengthRule(),
intParams(FunctionBodyLengthRule).map(FunctionBodyLengthRule.init) ?? FunctionBodyLengthRule(),
intParams(LineLengthRule).map(LineLengthRule.init) ?? LineLengthRule(),
intParams(TypeBodyLengthRule).map(TypeBodyLengthRule.init) ?? TypeBodyLengthRule(),
intParams(VariableNameMaxLengthRule).map(VariableNameMaxLengthRule.init) ?? VariableNameMaxLengthRule(),
intParams(VariableNameMinLengthRule).map(VariableNameMinLengthRule.init) ?? VariableNameMinLengthRule(),
]
// swiftlint:enable line_length
}

private static func ruleParametersFromArray<T>(array: [T]) -> [RuleParameter<T>] {
return zip([.Warning, .Error], array).map(RuleParameter.init)
return rules
}

public func lintablePathsForPath(path: String,
Expand Down
48 changes: 48 additions & 0 deletions Source/SwiftLintFramework/Models/MasterRuleList.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// MasterRuleList.swift
// SwiftLint
//
// Created by Scott Hoyt on 12/28/15.
// Copyright © 2015 Realm. All rights reserved.
//

import Foundation

public struct RuleList {
public let list: [String: Rule.Type]
public init(rules: Rule.Type...) {
var tmpList = [String: Rule.Type]()
for rule in rules {
tmpList[rule.description.identifier] = rule
}
list = tmpList
}
}

public let masterRuleList = RuleList( rules: ClosingBraceRule.self,
ColonRule.self,
CommaRule.self,
ConditionalBindingCascadeRule.self,
ControlStatementRule.self,
FileLengthRule.self,
ForceCastRule.self,
ForceTryRule.self,
FunctionBodyLengthRule.self,
LeadingWhitespaceRule.self,
LegacyConstructorRule.self,
LineLengthRule.self,
NestingRule.self,
OpeningBraceRule.self,
OperatorFunctionWhitespaceRule.self,
ReturnArrowWhitespaceRule.self,
StatementPositionRule.self,
TodoRule.self,
TrailingNewlineRule.self,
TrailingSemicolonRule.self,
TrailingWhitespaceRule.self,
TypeBodyLengthRule.self,
TypeNameRule.self,
ValidDocsRule.self,
VariableNameMaxLengthRule.self,
VariableNameMinLengthRule.self,
VariableNameRule.self)
4 changes: 4 additions & 0 deletions Source/SwiftLintFramework/Models/RuleParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public struct RuleParameter<T: Equatable>: Equatable {
self.severity = severity
self.value = value
}

static func ruleParametersFromArray<T>(array: [T]) -> [RuleParameter<T>] {
return zip([.Warning, .Error], array).map(RuleParameter<T>.init)
}
}

// MARK: - Equatable
Expand Down
Loading

0 comments on commit 9d138ad

Please sign in to comment.