Add ability to filter rules for `generate-docs` subcommand (#4195)

Added rules filtering options, like in the `rules` subcommand.

<img width="880" alt="generate-docs--help" src="https://user-images.githubusercontent.com/7829589/189372666-2f5aba62-57a1-49dc-9155-c60f9e085984.png">

To achieve reuse with `rules` subcommand:
- extracted filtering logic into `RulesFilter` (and write tests)
- extracted filtering command line parameters into `RulesFilterOptions: ParsableArguments`
This commit is contained in:
Vasiliy Kattouf 2022-10-05 00:45:52 +06:00 committed by GitHub
parent c70510c3ea
commit 129a55fb74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 313 additions and 46 deletions

13
BUILD
View File

@ -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(

View File

@ -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)

View File

@ -41,6 +41,12 @@ let package = Package(
"SwiftyTextTable",
]
),
.testTarget(
name: "CLITests",
dependencies: [
"swiftlint"
]
),
.target(
name: "SwiftLintFramework",
dependencies: frameworkDependencies

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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] {
[]
}
}

View File

@ -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