<commit_before>//
//  Configuration.swift
//  SwiftLint
//
//  Created by <NAME> on 2015-08-23.
//  Copyright (c) 2015 Realm. All rights reserved.
//

import Foundation
import SourceKittenFramework

private let fileManager = NSFileManager.defaultManager()

public struct Configuration: Equatable {
    public static let fileName = ".swiftlint.yml"
    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 {
        case XcodeReporter.identifier:
            return XcodeReporter.self
        case JSONReporter.identifier:
            return JSONReporter.self
        case CSVReporter.identifier:
            return CSVReporter.self
        case CheckstyleReporter.identifier:
            return CheckstyleReporter.self
        default:
            fatalError("no reporter with identifier '\(reporter)' available.")
        }
    }

    public init?(disabledRules: [String] = [],
                 enabledRules: [String] = [],
                 included: [String] = [],
                 excluded: [String] = [],
                 reporter: String = "xcode",
                 rules: [Rule] = Configuration.rulesFromDict(),
                 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
        let validRuleIdentifiers = Configuration.rulesFromDict().map {
            $0.dynamicType.description.identifier
        }

        let validDisabledRules = disabledRules.filter({ validRuleIdentifiers.contains($0)})
        let invalidRules = disabledRules.filter({ !validRuleIdentifiers.contains($0) })
        if !invalidRules.isEmpty {
            for invalidRule in invalidRules {
                queuedPrintError("config error: '\(invalidRule)' is not a valid rule identifier")
            }
            let listOfValidRuleIdentifiers = validRuleIdentifiers.joinWithSeparator("\n")
            queuedPrintError("Valid rule identifiers:\n\(listOfValidRuleIdentifiers)")
        }

        // Validate that rule identifiers aren't listed multiple times
        if Set(validDisabledRules).count != validDisabledRules.count {
            let duplicateRules = validDisabledRules.reduce([String: Int]()) { accu, element in
                var accu = accu
                accu[element] = accu[element]?.successor() ?? 1
                return accu
            }.filter { $0.1 > 1 }
            queuedPrintError(duplicateRules.map { rule in
                "config error: '\(rule.0)' is listed \(rule.1) times"
            }.joinWithSeparator("\n"))
            return nil
        }
        self.disabledRules = validDisabledRules

        self.rules = rules.filter { rule in
            let id = rule.dynamicType.description.identifier
            if validDisabledRules.contains(id) { return false }
            return enabledRules.contains(id) || !(rule is OptInRule)
        }
    }

    public init?(dict: [String: AnyObject]) {
        self.init(
            disabledRules: dict["disabled_rules"] as? [String] ?? [],
            enabledRules: dict["enabled_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)
        )
    }

    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)'") }
        if path.isEmpty || !NSFileManager.defaultManager().fileExistsAtPath(fullPath) {
            if !optional { fail() }
            self.init()!
            return
        }
        do {
            let yamlContents = try NSString(contentsOfFile: fullPath,
                encoding: NSUTF8StringEncoding) as String
            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 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 = try? 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())
            }
        }

        return rules
    }

    public func lintablePathsForPath(path: String,
                                     fileManager: NSFileManager = fileManager) -> [String] {
        let pathsForPath = included.isEmpty ? fileManager.filesToLintAtPath(path) : []
        let excludedPaths = excluded.flatMap(fileManager.filesToLintAtPath)
        let includedPaths = included.flatMap(fileManager.filesToLintAtPath)
        return (pathsForPath + includedPaths).filter({ !excludedPaths.contains($0) })
    }

    public func lintableFilesForPath(path: String) -> [File] {
        return lintablePathsForPath(path).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(Configuration.fileName)

        // 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 && path != "/" {
            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)
}
<commit_msg>Make ConfigurationError marginally more useful by not squashing it in Configuration.rulesFromDict.<commit_after>//
//  Configuration.swift
//  SwiftLint
//
//  Created by <NAME> on 2015-08-23.
//  Copyright (c) 2015 Realm. All rights reserved.
//

import Foundation
import SourceKittenFramework

private let fileManager = NSFileManager.defaultManager()

public struct Configuration: Equatable {
    public static let fileName = ".swiftlint.yml"
    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 {
        case XcodeReporter.identifier:
            return XcodeReporter.self
        case JSONReporter.identifier:
            return JSONReporter.self
        case CSVReporter.identifier:
            return CSVReporter.self
        case CheckstyleReporter.identifier:
            return CheckstyleReporter.self
        default:
            fatalError("no reporter with identifier '\(reporter)' available.")
        }
    }

    public init?(disabledRules: [String] = [],
                 enabledRules: [String] = [],
                 included: [String] = [],
                 excluded: [String] = [],
                 reporter: String = "xcode",
                 rules: [Rule] = Configuration.rulesFromDict(),
                 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
        let validRuleIdentifiers = Configuration.rulesFromDict().map {
            $0.dynamicType.description.identifier
        }

        let validDisabledRules = disabledRules.filter({ validRuleIdentifiers.contains($0)})
        let invalidRules = disabledRules.filter({ !validRuleIdentifiers.contains($0) })
        if !invalidRules.isEmpty {
            for invalidRule in invalidRules {
                queuedPrintError("config error: '\(invalidRule)' is not a valid rule identifier")
            }
            let listOfValidRuleIdentifiers = validRuleIdentifiers.joinWithSeparator("\n")
            queuedPrintError("Valid rule identifiers:\n\(listOfValidRuleIdentifiers)")
        }

        // Validate that rule identifiers aren't listed multiple times
        if Set(validDisabledRules).count != validDisabledRules.count {
            let duplicateRules = validDisabledRules.reduce([String: Int]()) { accu, element in
                var accu = accu
                accu[element] = accu[element]?.successor() ?? 1
                return accu
            }.filter { $0.1 > 1 }
            queuedPrintError(duplicateRules.map { rule in
                "config error: '\(rule.0)' is listed \(rule.1) times"
            }.joinWithSeparator("\n"))
            return nil
        }
        self.disabledRules = validDisabledRules

        self.rules = rules.filter { rule in
            let id = rule.dynamicType.description.identifier
            if validDisabledRules.contains(id) { return false }
            return enabledRules.contains(id) || !(rule is OptInRule)
        }
    }

    public init?(dict: [String: AnyObject]) {
        self.init(
            disabledRules: dict["disabled_rules"] as? [String] ?? [],
            enabledRules: dict["enabled_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)
        )
    }

    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)'") }
        if path.isEmpty || !NSFileManager.defaultManager().fileExistsAtPath(fullPath) {
            if !optional { fail() }
            self.init()!
            return
        }
        do {
            let yamlContents = try NSString(contentsOfFile: fullPath,
                encoding: NSUTF8StringEncoding) as String
            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 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] {
                do {
                    let configuredRule = try ConfigurableRuleType.init(config: ruleConfig)
                    rules.append(configuredRule)
                } catch {
                    queuedPrintError("Invalid config for '\(identifier)'. Falling back to default.")
                    rules.append(rule.init())
                }
            } else {
                rules.append(rule.init())
            }
        }

        return rules
    }

    public func lintablePathsForPath(path: String,
                                     fileManager: NSFileManager = fileManager) -> [String] {
        let pathsForPath = included.isEmpty ? fileManager.filesToLintAtPath(path) : []
        let excludedPaths = excluded.flatMap(fileManager.filesToLintAtPath)
        let includedPaths = included.flatMap(fileManager.filesToLintAtPath)
        return (pathsForPath + includedPaths).filter({ !excludedPaths.contains($0) })
    }

    public func lintableFilesForPath(path: String) -> [File] {
        return lintablePathsForPath(path).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(Configuration.fileName)

        // 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 && path != "/" {
            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)
}

{'fraction_non_alphanumeric': 0.09624371487725525, 'fraction_numerical': 0.002839396628216504, 'mean_word_length': 2.166510582506087, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5211847', 'n_tokens_mistral': 4374, 'n_tokens_neox': 4231, 'n_words': 1176}