import Foundation internal extension Configuration { class RulesWrapper { // MARK: - Properties private static var isOptInRuleCache: [String: Bool] = [:] private static let isOptInRuleCacheLock = NSLock() let allRulesWrapped: [ConfigurationRuleWrapper] internal let mode: RulesMode private let aliasResolver: (String) -> String private var invalidRuleIdsWarnedAbout: Set = [] private var customRulesIdentifiers: Set { Set(allRulesWrapped.customRules?.customRuleIdentifiers ?? []) } private var validRuleIdentifiers: Set { let regularRuleIdentifiers = allRulesWrapped.map { type(of: $0.rule).identifier } return Set(regularRuleIdentifiers + customRulesIdentifiers) } private var cachedResultingRules: [any Rule]? private let resultingRulesLock = NSLock() /// All rules enabled in this configuration, /// derived from rule mode (only / optIn - disabled) & existing rules var resultingRules: [any Rule] { // Lock for thread-safety (that's also why this is not a lazy var) resultingRulesLock.lock() defer { resultingRulesLock.unlock() } // Return existing value if it's available if let cachedResultingRules { return cachedResultingRules } // Calculate value let customRulesFilter: (RegexConfiguration) -> (Bool) var resultingRules = [any Rule]() switch mode { case .allCommandLine: customRulesFilter = { _ in true } resultingRules = allRulesWrapped.map(\.rule) case let .onlyConfiguration(onlyRulesRuleIdentifiers), let .onlyCommandLine(onlyRulesRuleIdentifiers): customRulesFilter = { onlyRulesRuleIdentifiers.contains($0.identifier) } let onlyRulesRuleIdentifiers = validate(ruleIds: onlyRulesRuleIdentifiers, valid: validRuleIdentifiers) resultingRules = allRulesWrapped.filter { tuple in onlyRulesRuleIdentifiers.contains(type(of: tuple.rule).identifier) }.map(\.rule) if !resultingRules.contains(where: { $0 is CustomRules }), !customRulesIdentifiers.isDisjoint(with: onlyRulesRuleIdentifiers), let customRules = allRulesWrapped.customRules { resultingRules.append(customRules) } case var .defaultConfiguration(disabledRuleIdentifiers, optInRuleIdentifiers): customRulesFilter = { !disabledRuleIdentifiers.contains($0.identifier) } disabledRuleIdentifiers = validate(ruleIds: disabledRuleIdentifiers, valid: validRuleIdentifiers) optInRuleIdentifiers = validate(optInRuleIds: optInRuleIdentifiers, valid: validRuleIdentifiers) resultingRules = allRulesWrapped.filter { tuple in let id = type(of: tuple.rule).identifier return !disabledRuleIdentifiers.contains(id) && (!(tuple.rule is any OptInRule) || optInRuleIdentifiers.contains(id)) }.map(\.rule) } // Filter custom rules if var customRulesRule = (resultingRules.first { $0 is CustomRules }) as? CustomRules { customRulesRule.configuration.customRuleConfigurations = customRulesRule.configuration.customRuleConfigurations.filter(customRulesFilter) resultingRules = resultingRules.filter { !($0 is CustomRules) } + [customRulesRule] } // Sort by name resultingRules = resultingRules.sorted { type(of: $0).identifier < type(of: $1).identifier } // Store & return cachedResultingRules = resultingRules return resultingRules } lazy var disabledRuleIdentifiers: [String] = { switch mode { case let .defaultConfiguration(disabled, _): return validate(ruleIds: disabled, valid: validRuleIdentifiers, silent: true) .sorted(by: <) case let .onlyConfiguration(onlyRules), let .onlyCommandLine(onlyRules): return validate( ruleIds: Set(allRulesWrapped .map { type(of: $0.rule).identifier } .filter { !onlyRules.contains($0) }), valid: validRuleIdentifiers, silent: true ).sorted(by: <) case .allCommandLine: return [] } }() // MARK: - Initializers init( mode: RulesMode, allRulesWrapped: [ConfigurationRuleWrapper], aliasResolver: @escaping (String) -> String, originatesFromMergingProcess: Bool = false ) { self.allRulesWrapped = allRulesWrapped self.aliasResolver = aliasResolver let mode = mode.applied(aliasResolver: aliasResolver) // If this instance originates from a merging process, some custom rules may be treated as not activated // Otherwise, custom rules should be treated as implicitly activated self.mode = originatesFromMergingProcess ? mode : mode.activateCustomRuleIdentifiers(allRulesWrapped: allRulesWrapped) } // MARK: - Methods: Validation private func validate(optInRuleIds: Set, valid: Set) -> Set { validate(ruleIds: optInRuleIds, valid: valid.union([RuleIdentifier.all.stringRepresentation])) } private func validate(ruleIds: Set, valid: Set, silent: Bool = false) -> Set { // Process invalid rule identifiers if !silent { let invalidRuleIdentifiers = ruleIds.subtracting(valid) if !invalidRuleIdentifiers.isEmpty { for invalidRuleIdentifier in invalidRuleIdentifiers.subtracting(invalidRuleIdsWarnedAbout) { invalidRuleIdsWarnedAbout.insert(invalidRuleIdentifier) queuedPrintError( "warning: '\(invalidRuleIdentifier)' is not a valid rule identifier" ) } queuedPrintError( "Valid rule identifiers:\n\(valid.sorted().joined(separator: "\n"))" ) } } // Return valid rule identifiers return ruleIds.intersection(valid) } // MARK: Merging func merged(with child: RulesWrapper) -> RulesWrapper { // Merge allRulesWrapped let newAllRulesWrapped = mergedAllRulesWrapped(with: child) // Merge mode let validRuleIdentifiers = self.validRuleIdentifiers.union(child.validRuleIdentifiers) let newMode: RulesMode switch child.mode { case let .defaultConfiguration(childDisabled, childOptIn): newMode = mergeDefaultMode( newAllRulesWrapped: newAllRulesWrapped, child: child, childDisabled: childDisabled, childOptIn: childOptIn, validRuleIdentifiers: validRuleIdentifiers ) case let .onlyConfiguration(childOnlyRules): // Use the child only rules, unless the parent is onlyRule switch mode { case let .onlyCommandLine(onlyRules): newMode = .onlyCommandLine(onlyRules) default: newMode = .onlyConfiguration(childOnlyRules) } case let .onlyCommandLine(onlyRules): // Always use the only rule newMode = .onlyCommandLine(onlyRules) case .allCommandLine: // Always use .allEnabled mode newMode = .allCommandLine } // Assemble & return merged rules return RulesWrapper( mode: newMode, allRulesWrapped: mergedCustomRules(newAllRulesWrapped: newAllRulesWrapped, with: child), aliasResolver: { child.aliasResolver(self.aliasResolver($0)) }, originatesFromMergingProcess: true ) } private func mergedAllRulesWrapped(with child: RulesWrapper) -> [ConfigurationRuleWrapper] { let mainConfigSet = Set(allRulesWrapped.map(HashableConfigurationRuleWrapperWrapper.init)) let childConfigSet = Set(child.allRulesWrapped.map(HashableConfigurationRuleWrapperWrapper.init)) let childConfigRulesWithConfig = childConfigSet .filter(\.configurationRuleWrapper.initializedWithNonEmptyConfiguration) let rulesUniqueToChildConfig = childConfigSet.subtracting(mainConfigSet) return childConfigRulesWithConfig // Include, if rule is configured in child .union(rulesUniqueToChildConfig) // Include, if rule is in child config only .union(mainConfigSet) // Use configurations from parent for remaining rules .map(\.configurationRuleWrapper) } private func mergedCustomRules( newAllRulesWrapped: [ConfigurationRuleWrapper], with child: RulesWrapper ) -> [ConfigurationRuleWrapper] { guard let parentCustomRulesRule = allRulesWrapped.customRules, let childCustomRulesRule = child.allRulesWrapped.customRules else { // Merging is only needed if both parent & child have a custom rules rule return newAllRulesWrapped } // Create new custom rules rule, prioritizing child custom rules var configuration = CustomRulesConfiguration() configuration.customRuleConfigurations = childCustomRulesRule.configuration.customRuleConfigurations + parentCustomRulesRule.configuration.customRuleConfigurations.filter { parentConfig in !childCustomRulesRule.configuration.customRuleConfigurations.contains { childConfig in childConfig.identifier == parentConfig.identifier } } var newCustomRulesRule = CustomRules() newCustomRulesRule.configuration = configuration return newAllRulesWrapped.filter { !($0.rule is CustomRules) } + [(newCustomRulesRule, true)] } private func mergeDefaultMode( newAllRulesWrapped: [ConfigurationRuleWrapper], child: RulesWrapper, childDisabled: Set, childOptIn: Set, validRuleIdentifiers: Set ) -> RulesMode { let childDisabled = child.validate(ruleIds: childDisabled, valid: validRuleIdentifiers) let childOptIn = child.validate(optInRuleIds: childOptIn, valid: validRuleIdentifiers) switch mode { // Switch parent's mode. Child is in default mode. case var .defaultConfiguration(disabled, optIn): disabled = validate(ruleIds: disabled, valid: validRuleIdentifiers) optIn = child.validate(optInRuleIds: optIn, valid: validRuleIdentifiers) // Only use parent disabled / optIn if child config doesn't tell the opposite return .defaultConfiguration( disabled: Set(childDisabled).union(Set(disabled.filter { !childOptIn.contains($0) })), optIn: Set(childOptIn).union(Set(optIn.filter { !childDisabled.contains($0) })) .filter { isOptInRule($0, allRulesWrapped: newAllRulesWrapped) } ) case var .onlyConfiguration(onlyRules): // Also add identifiers of child custom rules iff the custom_rules rule is enabled // (parent custom rules are already added) if (onlyRules.contains { $0 == CustomRules.identifier }) { if let childCustomRulesRule = child.allRulesWrapped.customRules { onlyRules = onlyRules.union( Set( childCustomRulesRule.customRuleIdentifiers ) ) } } onlyRules = validate(ruleIds: onlyRules, valid: validRuleIdentifiers) // Allow parent only rules that weren't disabled via the child config // & opt-ins from the child config return .onlyConfiguration(Set( childOptIn.union(onlyRules).filter { !childDisabled.contains($0) } )) case let .onlyCommandLine(onlyRules): // Like .allEnabled, rules can be disabled in a child config return .onlyCommandLine(onlyRules.filter { !childDisabled.contains($0) }) case .allCommandLine: // Opt-in to every rule that isn't disabled via child config return .defaultConfiguration( disabled: childDisabled .filter { !isOptInRule($0, allRulesWrapped: newAllRulesWrapped) }, optIn: Set(newAllRulesWrapped.map { type(of: $0.rule).identifier } .filter { !childDisabled.contains($0) && isOptInRule($0, allRulesWrapped: newAllRulesWrapped) } ) ) } } // MARK: Helpers private func isOptInRule( _ identifier: String, allRulesWrapped: [ConfigurationRuleWrapper] ) -> Bool { let cachedIsOptInRule = Self.isOptInRuleCacheLock.withLock { Self.isOptInRuleCache[identifier] } if let cachedIsOptInRule { return cachedIsOptInRule } let isOptInRule = allRulesWrapped .first { type(of: $0.rule).identifier == identifier }?.rule is any OptInRule Self.isOptInRuleCacheLock.withLock { Self.isOptInRuleCache[identifier] = isOptInRule } return isOptInRule } } } extension [ConfigurationRuleWrapper] { var customRules: CustomRules? { first { $0.rule is CustomRules }?.rule as? CustomRules } }