diff --git a/CHANGELOG.md b/CHANGELOG.md index 8758375069..430d243d60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,11 @@ ### Enhancements -* None. - +* Add `ignore_coding_keys` parameter to `nesting` rule. Setting this to true prevents + `CodingKey` enums from violating the rule + [braker1nine](https://github.com/braker1nine) + [#5641](https://github.com/realm/SwiftLint/issues/5641) + ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift index 7eb1af8163..dde85c9a30 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift @@ -46,7 +46,7 @@ private extension NestingRule { private var levels = Levels() override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { - validate(forFunction: false, triggeringToken: node.actorKeyword) + validate(forFunction: false, definesCodingKeys: false, triggeringToken: node.actorKeyword) return .visitChildren } @@ -55,7 +55,7 @@ private extension NestingRule { } override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - validate(forFunction: false, triggeringToken: node.classKeyword) + validate(forFunction: false, definesCodingKeys: false, triggeringToken: node.classKeyword) return .visitChildren } @@ -64,7 +64,7 @@ private extension NestingRule { } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - validate(forFunction: false, triggeringToken: node.enumKeyword) + validate(forFunction: false, definesCodingKeys: node.definesCodingKeys, triggeringToken: node.enumKeyword) return .visitChildren } @@ -73,7 +73,7 @@ private extension NestingRule { } override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { - validate(forFunction: false, triggeringToken: node.extensionKeyword) + validate(forFunction: false, definesCodingKeys: false, triggeringToken: node.extensionKeyword) return .visitChildren } @@ -82,7 +82,7 @@ private extension NestingRule { } override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - validate(forFunction: true, triggeringToken: node.funcKeyword) + validate(forFunction: true, definesCodingKeys: false, triggeringToken: node.funcKeyword) return .visitChildren } @@ -91,7 +91,7 @@ private extension NestingRule { } override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { - validate(forFunction: false, triggeringToken: node.protocolKeyword) + validate(forFunction: false, definesCodingKeys: false, triggeringToken: node.protocolKeyword) return .visitChildren } @@ -100,7 +100,7 @@ private extension NestingRule { } override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - validate(forFunction: false, triggeringToken: node.structKeyword) + validate(forFunction: false, definesCodingKeys: false, triggeringToken: node.structKeyword) return .visitChildren } @@ -113,7 +113,7 @@ private extension NestingRule { if configuration.ignoreTypealiasesAndAssociatedtypes { return } - validate(forFunction: false, triggeringToken: node.typealiasKeyword) + validate(forFunction: false, definesCodingKeys: false, triggeringToken: node.typealiasKeyword) levels.pop() } @@ -121,7 +121,7 @@ private extension NestingRule { if configuration.ignoreTypealiasesAndAssociatedtypes { return } - validate(forFunction: false, triggeringToken: node.associatedtypeKeyword) + validate(forFunction: false, definesCodingKeys: false, triggeringToken: node.associatedtypeKeyword) levels.pop() } @@ -141,7 +141,7 @@ private extension NestingRule { } // MARK: - - private func validate(forFunction: Bool, triggeringToken: TokenSyntax) { + private func validate(forFunction: Bool, definesCodingKeys: Bool, triggeringToken: TokenSyntax) { let inFunction = levels.lastIsFunction levels.push(forFunction) @@ -152,6 +152,12 @@ private extension NestingRule { if configuration.alwaysAllowOneTypeInFunctions && inFunction && !forFunction { return } + + // if current defines coding keys and we're ignoring coding keys, then skip nesting rule + if configuration.ignoreCodingKeys && definesCodingKeys { + return + } + guard let severity = configuration.severity(with: targetLevel, for: level) else { return } let targetName = forFunction ? "Functions" : "Types" diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift index 750b1793ef..2600c45ab6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift @@ -15,6 +15,8 @@ struct NestingConfiguration: RuleConfiguration { private(set) var alwaysAllowOneTypeInFunctions = false @ConfigurationElement(key: "ignore_typealiases_and_associatedtypes") private(set) var ignoreTypealiasesAndAssociatedtypes = false + @ConfigurationElement(key: "ignore_coding_keys") + private(set) var ignoreCodingKeys = false func severity(with config: Severity, for level: Int) -> ViolationSeverity? { if let error = config.error, level > error { diff --git a/Source/SwiftLintCore/Extensions/EnumDeclSyntax+SwiftLint.swift b/Source/SwiftLintCore/Extensions/EnumDeclSyntax+SwiftLint.swift new file mode 100644 index 0000000000..a43524bf86 --- /dev/null +++ b/Source/SwiftLintCore/Extensions/EnumDeclSyntax+SwiftLint.swift @@ -0,0 +1,38 @@ +import SwiftSyntax + +public extension EnumDeclSyntax { + /// True if this enum supports raw values + var supportsRawValues: Bool { + guard let inheritedTypeCollection = inheritanceClause?.inheritedTypes else { + return false + } + + let rawValueTypes: Set = [ + "Int", "Int8", "Int16", "Int32", "Int64", + "UInt", "UInt8", "UInt16", "UInt32", "UInt64", + "Double", "Float", "Float80", "Decimal", "NSNumber", + "NSDecimalNumber", "NSInteger", "String", "CGFloat", + ] + + return inheritedTypeCollection.contains { element in + guard let identifier = element.type.as(IdentifierTypeSyntax.self)?.name.text else { + return false + } + + return rawValueTypes.contains(identifier) + } + } + + /// True if this enum is a `CodingKey`. For that, it has to be named `CodingKeys` and must conform + /// to the `CodingKey` protocol. + var definesCodingKeys: Bool { + guard let inheritedTypeCollection = inheritanceClause?.inheritedTypes, + name.text == "CodingKeys" else { + return false + } + + return inheritedTypeCollection.contains { element in + element.type.as(IdentifierTypeSyntax.self)?.name.text == "CodingKey" + } + } +} diff --git a/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift b/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift index af40515e4b..5c2af44692 100644 --- a/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift @@ -169,30 +169,6 @@ public extension VariableDeclSyntax { } } -public extension EnumDeclSyntax { - /// True if this enum supports raw values - var supportsRawValues: Bool { - guard let inheritedTypeCollection = inheritanceClause?.inheritedTypes else { - return false - } - - let rawValueTypes: Set = [ - "Int", "Int8", "Int16", "Int32", "Int64", - "UInt", "UInt8", "UInt16", "UInt32", "UInt64", - "Double", "Float", "Float80", "Decimal", "NSNumber", - "NSDecimalNumber", "NSInteger", "String", "CGFloat", - ] - - return inheritedTypeCollection.contains { element in - guard let identifier = element.type.as(IdentifierTypeSyntax.self)?.name.text else { - return false - } - - return rawValueTypes.contains(identifier) - } - } -} - public extension FunctionDeclSyntax { var isIBAction: Bool { attributes.contains(attributeNamed: "IBAction") diff --git a/Tests/BuiltInRulesTests/NestingRuleTests.swift b/Tests/BuiltInRulesTests/NestingRuleTests.swift index b41082edb4..aaeeedd848 100644 --- a/Tests/BuiltInRulesTests/NestingRuleTests.swift +++ b/Tests/BuiltInRulesTests/NestingRuleTests.swift @@ -555,4 +555,48 @@ final class NestingRuleTests: SwiftLintTestCase { verifyRule(description, ruleConfiguration: ["ignore_typealiases_and_associatedtypes": true]) } + + func testNestingWithoutCodingKeys() { + var nonTriggeringExamples = NestingRule.description.nonTriggeringExamples + nonTriggeringExamples.append(contentsOf: [ + .init(""" + struct Outer { + struct Inner { + enum CodingKeys: String, CodingKey { + case id + } + } + } + """ + ), + ]) + + var triggeringExamples = NestingRule.description.triggeringExamples + triggeringExamples.append(contentsOf: [ + .init(""" + struct Outer { + struct Inner { + ↓enum Example: String, CodingKey { + case id + } + } + } + """), + .init(""" + struct Outer { + enum CodingKeys: String, CodingKey { + case id + ↓struct S {} + } + } + """), + ]) + + let description = NestingRule.description.with( + nonTriggeringExamples: nonTriggeringExamples, + triggeringExamples: triggeringExamples + ) + + verifyRule(description, ruleConfiguration: ["ignore_coding_keys": true ]) + } } diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 981d667680..ea2300ef3b 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -692,6 +692,7 @@ nesting: check_nesting_in_closures_and_statements: true always_allow_one_type_in_functions: false ignore_typealiases_and_associatedtypes: false + ignore_coding_keys: false meta: opt-in: false correctable: false