diff --git a/BUILD b/BUILD index c3c505ac4..2b5783ce0 100644 --- a/BUILD +++ b/BUILD @@ -31,8 +31,9 @@ swift_library( }), ) -swift_binary( - name = "swiftlint", +swift_library( + name = "swiftlint.library", + module_name = "swiftlint", srcs = glob(["Source/swiftlint/**/*.swift"]), visibility = ["//visibility:public"], deps = [ @@ -43,6 +44,14 @@ swift_binary( ], ) +swift_binary( + name = "swiftlint", + visibility = ["//visibility:public"], + deps = [ + ":swiftlint.library", + ], +) + # Linting filegroup( diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c652afde..5f6b23cef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ [JP Simard](https://github.com/jpims) [#4031](https://github.com/realm/SwiftLint/issues/4031) +* Add ability to filter rules for `generate-docs` subcommand. + [kattouf](https://github.com/kattouf) + * Add new `excludes_trivial_init` configuration for `missing_docs` rule to exclude initializers without any parameters. [Marcelo Fabri](https://github.com/marcelofabri) diff --git a/Package.swift b/Package.swift index 63086a85c..343c485ce 100644 --- a/Package.swift +++ b/Package.swift @@ -41,6 +41,12 @@ let package = Package( "SwiftyTextTable", ] ), + .testTarget( + name: "CLITests", + dependencies: [ + "swiftlint" + ] + ), .target( name: "SwiftLintFramework", dependencies: frameworkDependencies diff --git a/Source/swiftlint/Commands/Common/RulesFilter.ExcludingOptions+RulesFilterOptions.swift b/Source/swiftlint/Commands/Common/RulesFilter.ExcludingOptions+RulesFilterOptions.swift new file mode 100644 index 000000000..192bf6895 --- /dev/null +++ b/Source/swiftlint/Commands/Common/RulesFilter.ExcludingOptions+RulesFilterOptions.swift @@ -0,0 +1,20 @@ +extension RulesFilter.ExcludingOptions { + static func excludingOptions(byCommandLineOptions rulesFilterOptions: RulesFilterOptions) -> Self { + var excludingOptions: Self = [] + + switch rulesFilterOptions.ruleEnablement { + case .enabled: + excludingOptions.insert(.disabled) + case .disabled: + excludingOptions.insert(.enabled) + case .none: + break + } + + if rulesFilterOptions.correctable { + excludingOptions.insert(.uncorrectable) + } + + return excludingOptions + } +} diff --git a/Source/swiftlint/Commands/Common/RulesFilterOptions.swift b/Source/swiftlint/Commands/Common/RulesFilterOptions.swift new file mode 100644 index 000000000..0a409fb63 --- /dev/null +++ b/Source/swiftlint/Commands/Common/RulesFilterOptions.swift @@ -0,0 +1,20 @@ +import ArgumentParser + +enum RuleEnablementOptions: String, EnumerableFlag { + case enabled, disabled + + static func name(for value: RuleEnablementOptions) -> NameSpecification { + return .shortAndLong + } + + static func help(for value: RuleEnablementOptions) -> ArgumentHelp? { + return "Only show \(value.rawValue) rules" + } +} + +struct RulesFilterOptions: ParsableArguments { + @Flag(exclusivity: .exclusive) + var ruleEnablement: RuleEnablementOptions? + @Flag(name: .shortAndLong, help: "Only display correctable rules") + var correctable = false +} diff --git a/Source/swiftlint/Commands/GenerateDocs.swift b/Source/swiftlint/Commands/GenerateDocs.swift index 69fa474c2..82b6d4f89 100644 --- a/Source/swiftlint/Commands/GenerateDocs.swift +++ b/Source/swiftlint/Commands/GenerateDocs.swift @@ -4,13 +4,22 @@ import SwiftLintFramework extension SwiftLint { struct GenerateDocs: ParsableCommand { - static let configuration = CommandConfiguration(abstract: "Generates markdown documentation for all rules") + static let configuration = CommandConfiguration( + abstract: "Generates markdown documentation for selected group of rules" + ) @Option(help: "The directory where the documentation should be saved") var path = "rule_docs" + @Option(help: "The path to a SwiftLint configuration file") + var config: String? + @OptionGroup var rulesFilterOptions: RulesFilterOptions func run() throws { - try RuleListDocumentation(primaryRuleList) + let configuration = Configuration(configurationFiles: [config].compactMap({ $0 })) + let rulesFilter = RulesFilter(enabledRules: configuration.rules) + let rules = rulesFilter.getRules(excluding: .excludingOptions(byCommandLineOptions: rulesFilterOptions)) + + try RuleListDocumentation(rules) .write(to: URL(fileURLWithPath: path, isDirectory: true)) ExitHelper.successfullyExit() } diff --git a/Source/swiftlint/Commands/Rules.swift b/Source/swiftlint/Commands/Rules.swift index 517403b34..dfaa9d52a 100644 --- a/Source/swiftlint/Commands/Rules.swift +++ b/Source/swiftlint/Commands/Rules.swift @@ -10,28 +10,13 @@ import Foundation import SwiftLintFramework import SwiftyTextTable -enum RuleEnablementOptions: String, EnumerableFlag { - case enabled, disabled - - static func name(for value: RuleEnablementOptions) -> NameSpecification { - return .shortAndLong - } - - static func help(for value: RuleEnablementOptions) -> ArgumentHelp? { - return "Only show \(value.rawValue) rules" - } -} - extension SwiftLint { struct Rules: ParsableCommand { static let configuration = CommandConfiguration(abstract: "Display the list of rules and their identifiers") @Option(help: "The path to a SwiftLint configuration file") var config: String? - @Flag(exclusivity: .exclusive) - var ruleEnablement: RuleEnablementOptions? - @Flag(name: .shortAndLong, help: "Only display correctable rules") - var correctable = false + @OptionGroup var rulesFilterOptions: RulesFilterOptions @Flag(name: .shortAndLong, help: "Display full configuration details") var verbose = false @Argument(help: "The rule identifier to display description for") @@ -48,35 +33,12 @@ extension SwiftLint { } let configuration = Configuration(configurationFiles: [config].compactMap({ $0 })) - let rules = ruleList(configuration: configuration) + let rulesFilter = RulesFilter(enabledRules: configuration.rules) + let rules = rulesFilter.getRules(excluding: .excludingOptions(byCommandLineOptions: rulesFilterOptions)) let table = TextTable(ruleList: rules, configuration: configuration, verbose: verbose) print(table.render()) ExitHelper.successfullyExit() } - - private func ruleList(configuration: Configuration) -> RuleList { - guard ruleEnablement != nil || correctable else { - return primaryRuleList - } - - let filtered: [Rule.Type] = primaryRuleList.list.compactMap { ruleID, ruleType in - let configuredRule = configuration.rules.first { rule in - return type(of: rule).description.identifier == ruleID - } - - if ruleEnablement == .enabled && configuredRule == nil { - return nil - } else if ruleEnablement == .disabled && configuredRule != nil { - return nil - } else if correctable && !(configuredRule is CorrectableRule) { - return nil - } - - return ruleType - } - - return RuleList(rules: filtered) - } } } diff --git a/Source/swiftlint/Helpers/RulesFilter.swift b/Source/swiftlint/Helpers/RulesFilter.swift new file mode 100644 index 000000000..faabf62e8 --- /dev/null +++ b/Source/swiftlint/Helpers/RulesFilter.swift @@ -0,0 +1,48 @@ +import SwiftLintFramework + +extension RulesFilter { + struct ExcludingOptions: OptionSet { + let rawValue: Int + + static let enabled = ExcludingOptions(rawValue: 1 << 0) + static let disabled = ExcludingOptions(rawValue: 1 << 1) + static let uncorrectable = ExcludingOptions(rawValue: 1 << 2) + } +} + +class RulesFilter { + private let allRules: RuleList + private let enabledRules: [Rule] + + init(allRules: RuleList = primaryRuleList, enabledRules: [Rule]) { + self.allRules = allRules + self.enabledRules = enabledRules + } + + func getRules(excluding excludingOptions: ExcludingOptions) -> RuleList { + if excludingOptions.isEmpty { + return allRules + } + + let filtered: [Rule.Type] = allRules.list.compactMap { ruleID, ruleType in + let enabledRule = enabledRules.first { rule in + type(of: rule).description.identifier == ruleID + } + let isRuleEnabled = enabledRule != nil + + if excludingOptions.contains(.enabled) && isRuleEnabled { + return nil + } + if excludingOptions.contains(.disabled) && !isRuleEnabled { + return nil + } + if excludingOptions.contains(.uncorrectable) && !(ruleType is CorrectableRule.Type) { + return nil + } + + return ruleType + } + + return RuleList(rules: filtered) + } +} diff --git a/Tests/BUILD b/Tests/BUILD index ef87c2965..441ab5bff 100644 --- a/Tests/BUILD +++ b/Tests/BUILD @@ -2,6 +2,22 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library", "swift_test") exports_files(["BUILD"]) +swift_library( + name = "CLITests.library", + testonly = True, + srcs = glob(["CLITests/**/*.swift"]), + module_name = "CLITests", + deps = [ + "//:swiftlint.library", + ], +) + +swift_test( + name = "CLITests", + visibility = ["//visibility:public"], + deps = [":CLITests.library"], +) + filegroup( name = "SwiftLintFrameworkTestsSources", srcs = glob( diff --git a/Tests/CLITests/RulesFilterTests.swift b/Tests/CLITests/RulesFilterTests.swift new file mode 100644 index 000000000..b3494cbda --- /dev/null +++ b/Tests/CLITests/RulesFilterTests.swift @@ -0,0 +1,174 @@ +@testable import swiftlint +import SwiftLintFramework +import XCTest + +final class RulesFilterTests: XCTestCase { + func testRulesFilterExcludesEnabledRules() { + let allRules = RuleList( + rules: [ + RuleMock1.self, + RuleMock2.self, + CorrectableRuleMock.self + ] + ) + let enabledRules: [Rule] = [ + RuleMock1(), + CorrectableRuleMock() + ] + let rulesFilter = RulesFilter( + allRules: allRules, + enabledRules: enabledRules + ) + + let filteredRules = rulesFilter.getRules(excluding: [.enabled]) + + XCTAssertEqual( + Set(filteredRules.list.keys), + Set([RuleMock2.description.identifier]) + ) + } + + func testRulesFilterExcludesDisabledRules() { + let allRules = RuleList( + rules: [ + RuleMock1.self, + RuleMock2.self, + CorrectableRuleMock.self + ] + ) + let enabledRules: [Rule] = [ + RuleMock1(), + CorrectableRuleMock() + ] + let rulesFilter = RulesFilter( + allRules: allRules, + enabledRules: enabledRules + ) + + let filteredRules = rulesFilter.getRules(excluding: [.disabled]) + + XCTAssertEqual( + Set(filteredRules.list.keys), + Set([RuleMock1.description.identifier, CorrectableRuleMock.description.identifier]) + ) + } + + func testRulesFilterExcludesUncorrectableRules() { + let allRules = RuleList( + rules: [ + RuleMock1.self, + RuleMock2.self, + CorrectableRuleMock.self + ] + ) + let enabledRules: [Rule] = [ + RuleMock1(), + CorrectableRuleMock() + ] + let rulesFilter = RulesFilter( + allRules: allRules, + enabledRules: enabledRules + ) + + let filteredRules = rulesFilter.getRules(excluding: [.uncorrectable]) + + XCTAssertEqual( + Set(filteredRules.list.keys), + Set([CorrectableRuleMock.description.identifier]) + ) + } + + func testRulesFilterExcludesUncorrectableDisabledRules() { + let allRules = RuleList( + rules: [ + RuleMock1.self, + RuleMock2.self, + CorrectableRuleMock.self + ] + ) + let enabledRules: [Rule] = [ + RuleMock1(), + CorrectableRuleMock() + ] + let rulesFilter = RulesFilter( + allRules: allRules, + enabledRules: enabledRules + ) + + let filteredRules = rulesFilter.getRules(excluding: [.disabled, .uncorrectable]) + + XCTAssertEqual( + Set(filteredRules.list.keys), + Set([CorrectableRuleMock.description.identifier]) + ) + } + + func testRulesFilterExcludesUncorrectableEnabledRules() { + let allRules = RuleList( + rules: [ + RuleMock1.self, + RuleMock2.self, + CorrectableRuleMock.self + ] + ) + let enabledRules: [Rule] = [ + RuleMock1() + ] + let rulesFilter = RulesFilter( + allRules: allRules, + enabledRules: enabledRules + ) + + let filteredRules = rulesFilter.getRules(excluding: [.enabled, .uncorrectable]) + + XCTAssertEqual( + Set(filteredRules.list.keys), + Set([CorrectableRuleMock.description.identifier]) + ) + } +} + +// MARK: - Mocks + +private struct RuleMock1: Rule { + var configurationDescription: String { return "N/A" } + static let description = RuleDescription(identifier: "RuleMock1", name: "", + description: "", kind: .style) + + init() {} + init(configuration: Any) throws { self.init() } + + func validate(file: SwiftLintFile) -> [StyleViolation] { + return [] + } +} + +private struct RuleMock2: Rule { + var configurationDescription: String { return "N/A" } + static let description = RuleDescription(identifier: "RuleMock2", name: "", + description: "", kind: .style) + + init() {} + init(configuration: Any) throws { self.init() } + + func validate(file: SwiftLintFile) -> [StyleViolation] { + return [] + } +} + +private struct CorrectableRuleMock: CorrectableRule { + var configurationDescription: String { return "N/A" } + static let description = RuleDescription(identifier: "CorrectableRuleMock", name: "", + description: "", kind: .style) + + init() {} + init(configuration: Any) throws { self.init() } + + func validate(file: SwiftLintFile) -> [StyleViolation] { + return [] + } + + func correct(file: SwiftLintFile) -> [Correction] { + [] + } +} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e4083e712..245df12a4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -39,7 +39,7 @@ jobs: sw_vers xcodebuild -version displayName: Version Informations - - script: xcodebuild -scheme swiftlint test -destination "platform=macOS" OTHER_SWIFT_FLAGS="-D DISABLE_FOCUSED_EXAMPLES" + - script: xcodebuild -scheme swiftlint test -destination "platform=macOS" OTHER_SWIFT_FLAGS="\$(inherited) -D DISABLE_FOCUSED_EXAMPLES" displayName: xcodebuild test - job: SwiftPM