Introduce a "rule registry" concept

This will allow for registering rules that aren't compiled as part of
SwiftLintFramework.

Specifically this will allow us to split the built-in and extra rules
into separate modules, leading to faster incremental compilation when
working on rules since the rest of the framework won't need to be
rebuilt on every compilation.
This commit is contained in:
JP Simard 2022-11-08 11:49:12 -05:00
parent 3f52acd0a2
commit 165172e0fa
16 changed files with 63 additions and 26 deletions

View File

@ -3,6 +3,3 @@
let builtInRules: [Rule.Type] = [
{% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
{% endfor %}]
/// The rule list containing all available rules built into SwiftLint as well as native custom rules.
public let primaryRuleList = RuleList(rules: builtInRules + extraRules())

View File

@ -30,11 +30,11 @@ VERSION_STRING=$(shell ./tools/get-version)
all: build
sourcery: Source/SwiftLintFramework/Models/PrimaryRuleList.swift Source/SwiftLintFramework/Models/ReportersList.swift Tests/GeneratedTests/GeneratedTests.swift
sourcery: Source/SwiftLintFramework/Models/BuiltInRules.swift Source/SwiftLintFramework/Models/ReportersList.swift Tests/GeneratedTests/GeneratedTests.swift
Source/SwiftLintFramework/Models/PrimaryRuleList.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/PrimaryRuleList.stencil
./tools/sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/PrimaryRuleList.stencil --output .sourcery
mv .sourcery/PrimaryRuleList.generated.swift Source/SwiftLintFramework/Models/PrimaryRuleList.swift
Source/SwiftLintFramework/Models/BuiltInRules.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/BuiltInRules.stencil
./tools/sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintFramework/Models/BuiltInRules.swift
Source/SwiftLintFramework/Models/ReportersList.swift: Source/SwiftLintFramework/Reporters/*.swift .sourcery/ReportersList.stencil
./tools/sourcery --sources Source/SwiftLintFramework/Reporters --templates .sourcery/ReportersList.stencil --output .sourcery

View File

@ -39,7 +39,7 @@ extension Configuration {
/// - parameter cachePath: The location of the persisted cache on disk.
public init(
dict: [String: Any],
ruleList: RuleList = primaryRuleList,
ruleList: RuleList = RuleRegistry.shared.list,
enableAllRules: Bool = false,
cachePath: String? = nil
) throws {

View File

@ -64,7 +64,7 @@ public extension Configuration {
let effectiveOptInRules: [String]
if optInRules.contains(RuleIdentifier.all.stringRepresentation) {
let allOptInRules = primaryRuleList.list.compactMap { ruleID, ruleType in
let allOptInRules = RuleRegistry.shared.list.list.compactMap { ruleID, ruleType in
ruleType is OptInRule.Type && !(ruleType is AnalyzerRule.Type) ? ruleID : nil
}
effectiveOptInRules = Array(Set(allOptInRules + optInRules))

View File

@ -231,6 +231,3 @@ let builtInRules: [Rule.Type] = [
XCTSpecificMatcherRule.self,
YodaConditionRule.self
]
/// The rule list containing all available rules built into SwiftLint as well as native custom rules.
public let primaryRuleList = RuleList(rules: builtInRules + extraRules())

View File

@ -121,7 +121,7 @@ public struct Configuration {
public init(
rulesMode: RulesMode = .default(disabled: [], optIn: []),
allRulesWrapped: [ConfigurationRuleWrapper]? = nil,
ruleList: RuleList = primaryRuleList,
ruleList: RuleList = RuleRegistry.shared.list,
fileGraph: FileGraph? = nil,
includedPaths: [String] = [],
excludedPaths: [String] = [],

View File

@ -330,7 +330,7 @@ public struct CollectedLinter {
let allCustomIdentifiers =
(configuration.rules.first { $0 is CustomRules } as? CustomRules)?
.configuration.customRuleConfigurations.map { RuleIdentifier($0.identifier) } ?? []
let allRuleIdentifiers = primaryRuleList.allValidIdentifiers().map { RuleIdentifier($0) }
let allRuleIdentifiers = RuleRegistry.shared.list.allValidIdentifiers().map { RuleIdentifier($0) }
let allValidIdentifiers = Set(allCustomIdentifiers + allRuleIdentifiers + [.all])
let superfluousRuleIdentifier = RuleIdentifier(SuperfluousDisableCommandRule.description.identifier)

View File

@ -0,0 +1,41 @@
private let _registerAllRulesOnceImpl: Void = {
RuleRegistry.shared.register(rules: builtInRules + extraRules())
}()
/// Container to register and look up SwiftLint rules.
public final class RuleRegistry {
private var registeredRules = [Rule.Type]()
/// Shared rule registry instance.
public static let shared = RuleRegistry()
/// Rule list associated with this registry. Lazily created, and
/// immutable once looked up.
///
/// - note: Adding registering more rules after this was first
/// accessed will not work.
public private(set) lazy var list = RuleList(rules: registeredRules)
private init() {}
/// Register rules.
///
/// - parameter rules: The rules to register.
public func register(rules: [Rule.Type]) {
registeredRules.append(contentsOf: rules)
}
/// Look up a rule for a given ID.
///
/// - parameter id: The ID for the rule to look up.
///
/// - returns: The rule matching the specified ID, if one was found.
public func rule(forID id: String) -> Rule.Type? {
return list.list[id]
}
/// Register all rules. Should only be called once before any SwiftLint code is executed.
public static func registerAllRulesOnce() {
_ = _registerAllRulesOnceImpl
}
}

View File

@ -56,7 +56,7 @@ private extension TextTable {
continue
}
let rule = primaryRuleList.list[ruleIdentifier]
let rule = RuleRegistry.shared.rule(forID: ruleIdentifier)
let violations = ruleIdentifiersToViolationsMap[ruleIdentifier]
let numberOfWarnings = violations?.filter { $0.severity == .warning }.count ?? 0
let numberOfErrors = violations?.filter { $0.severity == .error }.count ?? 0

View File

@ -14,7 +14,7 @@ extension SwiftLint {
func run() throws {
var subPage = ""
if let ruleID {
if primaryRuleList.list[ruleID] == nil {
if RuleRegistry.shared.rule(forID: ruleID) == nil {
queuedPrintError("There is no rule named '\(ruleID)'. Opening rule directory instead.")
subPage = "rule-directory.html"
} else {

View File

@ -26,7 +26,7 @@ extension SwiftLint {
func run() throws {
if let ruleID {
guard let rule = primaryRuleList.list[ruleID] else {
guard let rule = RuleRegistry.shared.rule(forID: ruleID) else {
throw SwiftLintError.usageError(description: "No rule with identifier: \(ruleID)")
}

View File

@ -1,5 +1,6 @@
import ArgumentParser
import Foundation
import SwiftLintFramework
@main
struct SwiftLint: AsyncParsableCommand {
@ -8,6 +9,8 @@ struct SwiftLint: AsyncParsableCommand {
FileManager.default.changeCurrentDirectoryPath(directory)
}
RuleRegistry.registerAllRulesOnce()
return CommandConfiguration(
commandName: "swiftlint",
abstract: "A tool to enforce Swift style and conventions.",

View File

@ -15,7 +15,7 @@ class RulesFilter {
private let allRules: RuleList
private let enabledRules: [Rule]
init(allRules: RuleList = primaryRuleList, enabledRules: [Rule]) {
init(allRules: RuleList = RuleRegistry.shared.list, enabledRules: [Rule]) {
self.allRules = allRules
self.enabledRules = enabledRules
}

View File

@ -33,7 +33,6 @@ extension ConfigurationTests {
func testWarningThresholdMerging() {
func configuration(forWarningThreshold warningThreshold: Int?) -> Configuration {
return Configuration(
ruleList: primaryRuleList,
warningThreshold: warningThreshold,
reporter: XcodeReporter.identifier
)

View File

@ -6,7 +6,7 @@ import XCTest
// swiftlint:disable file_length
private let optInRules = primaryRuleList.list.filter({ $0.1.init() is OptInRule }).map({ $0.0 })
private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is OptInRule }).map({ $0.0 })
class ConfigurationTests: XCTestCase {
// MARK: Setup & Teardown
@ -74,12 +74,11 @@ class ConfigurationTests: XCTestCase {
func testEnableAllRulesConfiguration() throws {
let configuration = try Configuration(
dict: [:],
ruleList: primaryRuleList,
enableAllRules: true,
cachePath: nil
)
XCTAssertEqual(configuration.rules.count, primaryRuleList.list.count)
XCTAssertEqual(configuration.rules.count, RuleRegistry.shared.list.list.count)
}
func testOnlyRules() throws {
@ -146,7 +145,7 @@ class ConfigurationTests: XCTestCase {
XCTAssertEqual(disabledConfig.rulesWrapper.disabledRuleIdentifiers,
["nesting", "todo"],
"initializing Configuration with valid rules in Dictionary should succeed")
let expectedIdentifiers = Set(primaryRuleList.list.keys
let expectedIdentifiers = Set(RuleRegistry.shared.list.list.keys
.filter({ !(["nesting", "todo"] + optInRules).contains($0) }))
let configuredIdentifiers = Set(disabledConfig.rules.map {
type(of: $0).description.identifier
@ -163,7 +162,7 @@ class ConfigurationTests: XCTestCase {
XCTAssertEqual(configuration.rulesWrapper.disabledRuleIdentifiers,
[validRule],
"initializing Configuration with valid rules in YAML string should succeed")
let expectedIdentifiers = Set(primaryRuleList.list.keys
let expectedIdentifiers = Set(RuleRegistry.shared.list.list.keys
.filter({ !([validRule] + optInRules).contains($0) }))
let configuredIdentifiers = Set(configuration.rules.map {
type(of: $0).description.identifier

View File

@ -48,7 +48,7 @@ public extension String {
}
}
public let allRuleIdentifiers = Set(primaryRuleList.list.keys)
public let allRuleIdentifiers = Set(RuleRegistry.shared.list.list.keys)
public extension Configuration {
func applyingConfiguration(from example: Example) -> Configuration {
@ -272,7 +272,7 @@ public func makeConfig(_ ruleConfiguration: Any?, _ identifier: String,
let identifiers: Set<String> = skipDisableCommandTests ? [identifier]
: [identifier, superfluousDisableCommandRuleIdentifier]
if let ruleConfiguration, let ruleType = primaryRuleList.list[identifier] {
if let ruleConfiguration, let ruleType = RuleRegistry.shared.rule(forID: identifier) {
// The caller has provided a custom configuration for the rule under test
return (try? ruleType.init(configuration: ruleConfiguration)).flatMap { configuredRule in
let rules = skipDisableCommandTests ? [configuredRule] : [configuredRule, SuperfluousDisableCommandRule()]
@ -330,6 +330,7 @@ public extension XCTestCase {
testShebang: Bool = true,
file: StaticString = #file,
line: UInt = #line) {
RuleRegistry.registerAllRulesOnce()
guard ruleDescription.minSwiftVersion <= .current else {
return
}