From b9841595a543cfa06a4cb4e53e970d45ce62600c Mon Sep 17 00:00:00 2001 From: Skoti <1700160+Skoti@users.noreply.github.com> Date: Mon, 26 Jun 2017 23:04:19 +0200 Subject: [PATCH] Separated nesting level counting for types and functions in `nesting` rule (fixes #1151). Enhanced `nesting` rule to search for nested types and functions within closures and statements. Enhanced `nesting` rule to allow for one nested type within a function even if breaking a maximum type level nesting (fixes #1151). --- CHANGELOG.md | 16 + .../SwiftDeclarationKind+SwiftLint.swift | 8 + .../Rules/Metrics/NestingRule.swift | 212 ++++++--- .../Rules/Metrics/NestingRuleExamples.swift | 415 ++++++++++++++++++ .../NestingConfiguration.swift | 29 +- Tests/LinuxMain.swift | 4 +- .../AutomaticRuleTests.generated.swift | 6 - .../CollectingRuleTests.swift | 2 - .../NestingRuleTests.swift | 377 ++++++++++++++++ .../RuleConfigurationTests.swift | 20 +- 10 files changed, 993 insertions(+), 96 deletions(-) create mode 100644 Source/SwiftLintFramework/Rules/Metrics/NestingRuleExamples.swift create mode 100644 Tests/SwiftLintFrameworkTests/NestingRuleTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index ae26a7b2b..8387680f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,19 @@ [JP Simard](https://github.com/jpsim) [#3412](https://github.com/realm/SwiftLint/issues/3412) +* Renamed `statement_level` to `function_level` in `nesting` rule configuration. + [Skoti](https://github.com/Skoti) + +* Separated `type_level` and `function_level` counting in `nesting` rule. + [Skoti](https://github.com/Skoti) + [#1151](https://github.com/realm/SwiftLint/issues/1151) + +* `function_level` in `nesting` rule defaults to 2 levels. + [Skoti](https://github.com/Skoti) + +* Added `check_nesting_in_closures_and_statements` in `nesting` rule to search for nested types and functions within closures and statements. Defaults to `true`. + [Skoti](https://github.com/Skoti) + #### Experimental * None. @@ -27,6 +40,9 @@ `Never`. [Artem Garmash](https://github.com/agarmash) [#3286](https://github.com/realm/SwiftLint/issues/3286) +* Added `always_allow_one_type_in_functions` option in `nesting` rule configuration. Defaults to `false`. This allows to nest one type within a function even if breaking the maximum `type_level`. + [Skoti](https://github.com/Skoti) + [#1151](https://github.com/realm/SwiftLint/issues/1151) #### Bug Fixes diff --git a/Source/SwiftLintFramework/Extensions/SwiftDeclarationKind+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/SwiftDeclarationKind+SwiftLint.swift index feca880e7..892368069 100644 --- a/Source/SwiftLintFramework/Extensions/SwiftDeclarationKind+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/SwiftDeclarationKind+SwiftLint.swift @@ -34,4 +34,12 @@ extension SwiftDeclarationKind { .associatedtype, .enum ] + + internal static let extensionKinds: Set = [ + .extension, + .extensionClass, + .extensionEnum, + .extensionProtocol, + .extensionStruct + ] } diff --git a/Source/SwiftLintFramework/Rules/Metrics/NestingRule.swift b/Source/SwiftLintFramework/Rules/Metrics/NestingRule.swift index 98e4c389c..7025ee28b 100644 --- a/Source/SwiftLintFramework/Rules/Metrics/NestingRule.swift +++ b/Source/SwiftLintFramework/Rules/Metrics/NestingRule.swift @@ -1,10 +1,10 @@ import SourceKittenFramework -public struct NestingRule: ASTRule, ConfigurationProviderRule, AutomaticTestableRule { +public struct NestingRule: ConfigurationProviderRule { public var configuration = NestingConfiguration(typeLevelWarning: 1, typeLevelError: nil, - statementLevelWarning: 5, - statementLevelError: nil) + functionLevelWarning: 2, + functionLevelError: nil) public init() {} @@ -12,79 +12,151 @@ public struct NestingRule: ASTRule, ConfigurationProviderRule, AutomaticTestable identifier: "nesting", name: "Nesting", description: "Types should be nested at most 1 level deep, " + - "and statements should be nested at most 5 levels deep.", - kind: .metrics, - nonTriggeringExamples: ["class", "struct", "enum"].flatMap { kind -> [Example] in - [ - Example("\(kind) Class0 { \(kind) Class1 {} }\n"), - Example(""" - func func0() { - func func1() { - func func2() { - func func3() { - func func4() { - func func5() { - } - } - } - } - } - } - """) - ] - } + [Example("enum Enum0 { enum Enum1 { case Case } }")], - triggeringExamples: ["class", "struct", "enum"].map { kind -> Example in - return Example("\(kind) A { \(kind) B { ↓\(kind) C {} } }\n") - } + [ - Example(""" - func func0() { - func func1() { - func func2() { - func func3() { - func func4() { - func func5() { - ↓func func6() { - } - } - } - } - } - } - } - """) - ] + "and functions should be nested at most 2 levels deep.", + kind: .metrics, + nonTriggeringExamples: NestingRuleExamples.nonTriggeringExamples, + triggeringExamples: NestingRuleExamples.triggeringExamples ) - public func validate(file: SwiftLintFile, kind: SwiftDeclarationKind, - dictionary: SourceKittenDictionary) -> [StyleViolation] { - return validate(file: file, kind: kind, dictionary: dictionary, level: 0) + private let omittedStructureKinds: [SwiftStructureKind] = + [.declaration(.enumcase), .declaration(.enumelement)] + + SwiftDeclarationKind.variableKinds.map { .declaration($0) } + + private struct ValidationArgs { + var typeLevel: Int = -1 + var functionLevel: Int = -1 + var previousKind: SwiftStructureKind? + var violations: [StyleViolation] = [] + + func with(previousKind: SwiftStructureKind?) -> ValidationArgs { + var args = self + args.previousKind = previousKind + return args + } } - private func validate(file: SwiftLintFile, kind: SwiftDeclarationKind, dictionary: SourceKittenDictionary, - level: Int) -> [StyleViolation] { - var violations = [StyleViolation]() - let typeKinds = SwiftDeclarationKind.typeKinds - if let offset = dictionary.offset { - let (targetName, targetLevel) = typeKinds.contains(kind) - ? ("Types", configuration.typeLevel) : ("Statements", configuration.statementLevel) - if let severity = configuration.severity(with: targetLevel, for: level) { - let threshold = configuration.threshold(with: targetLevel, for: severity) - let pluralSuffix = threshold > 1 ? "s" : "" - violations.append(StyleViolation( - ruleDescription: Self.description, - severity: severity, - location: Location(file: file, byteOffset: offset), - reason: "\(targetName) should be nested at most \(threshold) level\(pluralSuffix) deep")) + public func validate(file: SwiftLintFile) -> [StyleViolation] { + return validate(file: file, substructure: file.structureDictionary.substructure, args: ValidationArgs()) + } + + private func validate(file: SwiftLintFile, substructure: [SourceKittenDictionary], + args: ValidationArgs) -> [StyleViolation] { + return args.violations + substructure.flatMap { dictionary -> [StyleViolation] in + guard let kindString = dictionary.kind, let structureKind = SwiftStructureKind(kindString) else { + return validate(file: file, substructure: dictionary.substructure, args: args.with(previousKind: nil)) + } + guard !omittedStructureKinds.contains(structureKind) else { + return args.violations + } + switch structureKind { + case let .declaration(declarationKind): + return validate(file: file, structureKind: structureKind, + declarationKind: declarationKind, dictionary: dictionary, args: args) + case .expression, .statement: + guard configuration.checkNestingInClosuresAndStatements else { + return args.violations + } + return validate(file: file, substructure: dictionary.substructure, + args: args.with(previousKind: structureKind)) } } - violations.append(contentsOf: dictionary.substructure.compactMap { subDict in - if let kind = subDict.declarationKind { - return (kind, subDict) - } + } + + private func validate(file: SwiftLintFile, structureKind: SwiftStructureKind, declarationKind: SwiftDeclarationKind, + dictionary: SourceKittenDictionary, args: ValidationArgs) -> [StyleViolation] { + let isTypeOrExtension = SwiftDeclarationKind.typeKinds.contains(declarationKind) + || SwiftDeclarationKind.extensionKinds.contains(declarationKind) + let isFunction = SwiftDeclarationKind.functionKinds.contains(declarationKind) + + guard isTypeOrExtension || isFunction else { + return validate(file: file, substructure: dictionary.substructure, + args: args.with(previousKind: structureKind)) + } + + let currentTypeLevel = isTypeOrExtension ? args.typeLevel + 1 : args.typeLevel + let currentFunctionLevel = isFunction ? args.functionLevel + 1 : args.functionLevel + + var violations = args.violations + + if let violation = levelViolation(file: file, dictionary: dictionary, + previousKind: args.previousKind, + level: isFunction ? currentFunctionLevel : currentTypeLevel, + forFunction: isFunction) { + violations.append(violation) + } + + return validate(file: file, substructure: dictionary.substructure, + args: ValidationArgs( + typeLevel: currentTypeLevel, + functionLevel: currentFunctionLevel, + previousKind: structureKind, + violations: violations + ) + ) + } + + private func levelViolation(file: SwiftLintFile, dictionary: SourceKittenDictionary, + previousKind: SwiftStructureKind?, level: Int, forFunction: Bool) -> StyleViolation? { + guard let offset = dictionary.offset else { return nil - }.flatMap { kind, subDict in - return validate(file: file, kind: kind, dictionary: subDict, level: level + 1) - }) - return violations + } + + let targetLevel = forFunction ? configuration.functionLevel : configuration.typeLevel + var violatingSeverity: ViolationSeverity? + + if configuration.alwaysAllowOneTypeInFunctions, + case let .declaration(previousDeclarationKind)? = previousKind, + !SwiftDeclarationKind.functionKinds.contains(previousDeclarationKind) { + violatingSeverity = configuration.severity(with: targetLevel, for: level) + } else if forFunction || !configuration.alwaysAllowOneTypeInFunctions || previousKind == nil { + violatingSeverity = configuration.severity(with: targetLevel, for: level) + } else { + violatingSeverity = nil + } + + guard let severity = violatingSeverity else { + return nil + } + + let targetName = forFunction ? "Functions" : "Types" + let threshold = configuration.threshold(with: targetLevel, for: severity) + let pluralSuffix = threshold > 1 ? "s" : "" + return StyleViolation( + ruleDescription: Self.description, + severity: severity, + location: Location(file: file, byteOffset: offset), + reason: "\(targetName) should be nested at most \(threshold) level\(pluralSuffix) deep" + ) + } +} + +private enum SwiftStructureKind: Equatable { + case declaration(SwiftDeclarationKind) + case expression(SwiftExpressionKind) + case statement(StatementKind) + + init?(_ structureKind: String) { + if let declarationKind = SwiftDeclarationKind(rawValue: structureKind) { + self = .declaration(declarationKind) + } else if let expressionKind = SwiftExpressionKind(rawValue: structureKind) { + self = .expression(expressionKind) + } else if let statementKind = StatementKind(rawValue: structureKind) { + self = .statement(statementKind) + } else { + return nil + } + } + + static func == (lhs: SwiftStructureKind, rhs: SwiftStructureKind) -> Bool { + switch (lhs, rhs) { + case let (.declaration(lhsKind), .declaration(rhsKind)): + return lhsKind == rhsKind + case let (.expression(lhsKind), .expression(rhsKind)): + return lhsKind == rhsKind + case let (.statement(lhsKind), .statement(rhsKind)): + return lhsKind == rhsKind + default: + return false + } } } diff --git a/Source/SwiftLintFramework/Rules/Metrics/NestingRuleExamples.swift b/Source/SwiftLintFramework/Rules/Metrics/NestingRuleExamples.swift new file mode 100644 index 000000000..56f85055e --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Metrics/NestingRuleExamples.swift @@ -0,0 +1,415 @@ +// swiftlint:disable file_length type_body_length +internal struct NestingRuleExamples { + static let nonTriggeringExamples = nonTriggeringTypeExamples + + nonTriggeringFunctionExamples + + nonTriggeringClosureAndStatementExamples + + nonTriggeringMixedExamples + + private static let nonTriggeringTypeExamples = + ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + // default maximum type nesting level + .init(""" + \(type) Example_0 { + \(type) Example_1 {} + } + """), + + /* + all variableKinds of SwiftDeclarationKind (except .varParameter which is a function parameter) + are flattend in a file structure so limits do not change + */ + .init(""" + var example: Int { + \(type) Example_0 { + \(type) Example_1 {} + } + return 5 + } + """), + + // didSet is not present in file structure although there is such a swift declaration kind + .init(""" + var example: Int = 5 { + didSet { + \(type) Example_0 { + \(type) Example_1 {} + } + } + } + """), + + // extensions are counted as a type level + .init(""" + extension Example_0 { + \(type) Example_1 {} + } + """) + ] + } + + private static let nonTriggeringFunctionExamples: [Example] = [ + // default maximum function nesting level + .init(""" + func f_0() { + func f_1() { + func f_2() {} + } + } + """), + + /* + all variableKinds of SwiftDeclarationKind (except .varParameter which is a function parameter) + are flattend in a file structure so level limits do not change + */ + .init(""" + var example: Int { + func f_0() { + func f_1() { + func f_2() {} + } + } + return 5 + } + """), + + // didSet is not present in file structure although there is such a swift declaration kind + .init(""" + var example: Int = 5 { + didSet { + func f_0() { + func f_1() { + func f_2() {} + } + } + } + } + """), + + // extensions are counted as a type level + .init(""" + extension Example_0 { + func f_0() { + func f_1() { + func f_2() {} + } + } + } + """) + ] + + private static let nonTriggeringClosureAndStatementExamples = + ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + // swich statement example + .init(""" + switch example { + case .exampleCase: + \(type) Example_0 { + \(type) Example_1 {} + } + default: + func f_0() { + func f_1() { + func f_2() {} + } + } + } + """), + + // closure var example + .init(""" + var exampleClosure: () -> Void = { + \(type) Example_0 { + \(type) Example_1 {} + } + func f_0() { + func f_1() { + func f_2() {} + } + } + } + """), + + // function closure parameter example + .init(""" + exampleFunc(closure: { + \(type) Example_0 { + \(type) Example_1 {} + } + func f_0() { + func f_1() { + func f_2() {} + } + } + }) + """) + ] + } + + private static let nonTriggeringMixedExamples = + ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + // default maximum nesting level for both type and function (nesting order is arbitrary) + .init(""" + \(type) Example_0 { + func f_0() { + \(type) Example_1 { + func f_1() { + func f_2() {} + } + } + } + } + """), + + // default maximum nesting level for both type and function within closures and statements + .init(""" + \(type) Example_0 { + func f_0() { + switch example { + case .exampleCase: + \(type) Example_1 { + func f_1() { + func f_2() {} + } + } + default: + exampleFunc(closure: { + \(type) Example_1 { + func f_1() { + func f_2() {} + } + } + }) + } + } + } + """) + ] + } + + static let triggeringExamples = triggeringTypeExamples + + triggeringFunctionExamples + + triggeringClosureAndStatementExamples + + triggeringMixedExamples + + private static let triggeringTypeExamples = + ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + // violation of default maximum type nesting level + .init(""" + \(type) Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + """), + + /* + all variableKinds of SwiftDeclarationKind (except .varParameter which is a function parameter) + are flattend in a file structure so limits do not change + */ + .init(""" + var example: Int { + \(type) Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + return 5 + } + """), + + // didSet is not present in file structure although there is such a swift declaration kind + .init(""" + var example: Int = 5 { + didSet { + \(type) Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + } + } + """), + + // extensions are counted as a type level, violation of default maximum type nesting level + .init(""" + extension Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + """) + ] + } + + private static let triggeringFunctionExamples: [Example] = [ + // violation of default maximum function nesting level + .init(""" + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + """), + + /* + all variableKinds of SwiftDeclarationKind (except .varParameter which is a function parameter) + are flattend in a file structure so level limits do not change + */ + .init(""" + var example: Int { + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + return 5 + } + """), + + // didSet is not present in file structure although there is such a swift declaration kind + .init(""" + var example: Int = 5 { + didSet { + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + } + } + """), + + // extensions are counted as a type level, violation of default maximum function nesting level + .init(""" + extension Example_0 { + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + } + """) + ] + + private static let triggeringClosureAndStatementExamples = + ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + // swich statement example + .init(""" + switch example { + case .exampleCase: + \(type) Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + default: + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + } + """), + + // closure var example + .init(""" + var exampleClosure: () -> Void = { + \(type) Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + } + """), + + // function closure parameter example + .init(""" + exampleFunc(closure: { + \(type) Example_0 { + \(type) Example_1 {} + } + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + }) + """) + ] + } + + private static let triggeringMixedExamples = + ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + // violation of default maximum nesting level for both type and function (nesting order is arbitrary) + .init(""" + \(type) Example_0 { + func f_0() { + \(type) Example_1 { + func f_1() { + func f_2() { + ↓\(type) Example_2 {} + ↓func f_3() {} + } + } + } + } + } + """), + + // violation of default maximum nesting level for both type and function within closures and statements + .init(""" + \(type) Example_0 { + func f_0() { + switch example { + case .exampleCase: + \(type) Example_1 { + func f_1() { + func f_2() { + ↓\(type) Example_2 {} + ↓func f_3() {} + } + } + } + default: + exampleFunc(closure: { + \(type) Example_1 { + func f_1() { + func f_2() { + ↓\(type) Example_2 {} + ↓func f_3() {} + } + } + } + }) + } + } + } + """) + ] + } +} diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/NestingConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/NestingConfiguration.swift index 481c6a07b..ef27249da 100644 --- a/Source/SwiftLintFramework/Rules/RuleConfigurations/NestingConfiguration.swift +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/NestingConfiguration.swift @@ -1,18 +1,26 @@ public struct NestingConfiguration: RuleConfiguration, Equatable { public var consoleDescription: String { - return "(type_level) \(typeLevel.shortConsoleDescription), " + - "(statement_level) \(statementLevel.shortConsoleDescription)" + return "(type_level) \(typeLevel.shortConsoleDescription)" + + ", (function_level) \(functionLevel.shortConsoleDescription)" + + ", (check_nesting_in_closures_and_statements) \(checkNestingInClosuresAndStatements)" + + ", (always_allow_one_type_in_functions) \(alwaysAllowOneTypeInFunctions)" } var typeLevel: SeverityLevelsConfiguration - var statementLevel: SeverityLevelsConfiguration + var functionLevel: SeverityLevelsConfiguration + var checkNestingInClosuresAndStatements: Bool + var alwaysAllowOneTypeInFunctions: Bool public init(typeLevelWarning: Int, typeLevelError: Int?, - statementLevelWarning: Int, - statementLevelError: Int?) { - typeLevel = SeverityLevelsConfiguration(warning: typeLevelWarning, error: typeLevelError) - statementLevel = SeverityLevelsConfiguration(warning: statementLevelWarning, error: statementLevelError) + functionLevelWarning: Int, + functionLevelError: Int?, + checkNestingInClosuresAndStatements: Bool = true, + alwaysAllowOneTypeInFunctions: Bool = false) { + self.typeLevel = SeverityLevelsConfiguration(warning: typeLevelWarning, error: typeLevelError) + self.functionLevel = SeverityLevelsConfiguration(warning: functionLevelWarning, error: functionLevelError) + self.checkNestingInClosuresAndStatements = checkNestingInClosuresAndStatements + self.alwaysAllowOneTypeInFunctions = alwaysAllowOneTypeInFunctions } public mutating func apply(configuration: Any) throws { @@ -23,9 +31,12 @@ public struct NestingConfiguration: RuleConfiguration, Equatable { if let typeLevelConfiguration = configurationDict["type_level"] { try typeLevel.apply(configuration: typeLevelConfiguration) } - if let statementLevelConfiguration = configurationDict["statement_level"] { - try statementLevel.apply(configuration: statementLevelConfiguration) + if let functionLevelConfiguration = configurationDict["function_level"] { + try functionLevel.apply(configuration: functionLevelConfiguration) } + // swiftlint:disable:next line_length + checkNestingInClosuresAndStatements = configurationDict["check_nesting_in_closures_and_statements"] as? Bool ?? true + alwaysAllowOneTypeInFunctions = configurationDict["always_allow_one_type_in_functions"] as? Bool ?? false } func severity(with config: SeverityLevelsConfiguration, for level: Int) -> ViolationSeverity? { diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 83047e8f3..15d231131 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1011,7 +1011,9 @@ extension NSObjectPreferIsEqualRuleTests { extension NestingRuleTests { static var allTests: [(String, (NestingRuleTests) -> () throws -> Void)] = [ - ("testWithDefaultConfiguration", testWithDefaultConfiguration) + ("testNestingWithDefaultConfiguration", testNestingWithDefaultConfiguration), + ("testNestingWithAlwaysAllowOneTypeInFunctions", testNestingWithAlwaysAllowOneTypeInFunctions), + ("testNestingWithoutCheckNestingInClosuresAndStatements", testNestingWithoutCheckNestingInClosuresAndStatements) ] } diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift index 8c35dc67b..29ee77970 100644 --- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift +++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift @@ -432,12 +432,6 @@ class NSObjectPreferIsEqualRuleTests: XCTestCase { } } -class NestingRuleTests: XCTestCase { - func testWithDefaultConfiguration() { - verifyRule(NestingRule.description) - } -} - class NimbleOperatorRuleTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(NimbleOperatorRule.description) diff --git a/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift b/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift index ae30ba935..ef5cf5668 100644 --- a/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift @@ -1,8 +1,6 @@ @testable import SwiftLintFramework import XCTest -// swiftlint:disable nesting - class CollectingRuleTests: XCTestCase { func testCollectsIntoStorage() { struct Spec: MockCollectingRule { diff --git a/Tests/SwiftLintFrameworkTests/NestingRuleTests.swift b/Tests/SwiftLintFrameworkTests/NestingRuleTests.swift new file mode 100644 index 000000000..56e8462a2 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/NestingRuleTests.swift @@ -0,0 +1,377 @@ +import SwiftLintFramework +import XCTest + +// swiftlint:disable:next type_body_length +class NestingRuleTests: XCTestCase { + func testNestingWithDefaultConfiguration() { + verifyRule(NestingRule.description) + } + + // swiftlint:disable:next function_body_length + func testNestingWithAlwaysAllowOneTypeInFunctions() { + var nonTriggeringExamples = NestingRule.description.nonTriggeringExamples + nonTriggeringExamples.append(contentsOf: ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + .init(""" + \(type) Example_0 { + \(type) Example_1 { + func f_0() { + \(type) Example_2 {} + } + } + } + """), + + .init(""" + \(type) Example_0 { + \(type) Example_1 { + func f_0() { + \(type) Example_2 { + func f_1() { + \(type) Example_3 {} + } + } + } + } + } + """), + + .init(""" + func f_0() { + \(type) Example_0 { + \(type) Example_1 {} + } + } + """) + ] + }) + nonTriggeringExamples.append(contentsOf: ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + .init(""" + exampleFunc(closure: { + \(type) Example_0 { + \(type) Example_1 { + func f_0() { + \(type) Example_2 {} + } + } + } + func f_0() { + \(type) Example_0 { + func f_1() { + \(type) Example_1 { + func f_2() { + \(type) Example_2 {} + } + } + } + } + } + }) + """), + + .init(""" + switch example { + case .exampleCase: + \(type) Example_0 { + \(type) Example_1 { + func f_0() { + \(type) Example_2 {} + } + } + } + default: + func f_0() { + \(type) Example_0 { + func f_1() { + \(type) Example_1 { + func f_2() { + \(type) Example_2 {} + } + } + } + } + } + } + """) + ] + }) + + var triggeringExamples = ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + .init(""" + \(type) Example_0 { + \(type) Example_1 { + func f_0() { + \(type) Example_2 { + ↓\(type) Example_3 {} + } + } + } + } + """), + + .init(""" + \(type) Example_0 { + \(type) Example_1 { + func f_0() { + \(type) Example_2 { + func f_1() { + \(type) Example_3 { + ↓\(type) Example_4 {} + } + } + } + } + } + } + """), + + .init(""" + func f_0() { + \(type) Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + } + """) + ] + } + + triggeringExamples.append(contentsOf: ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + .init(""" + exampleFunc(closure: { + \(type) Example_0 { + \(type) Example_1 { + func f_0() { + \(type) Example_2 { + ↓\(type) Example_3 {} + } + } + } + } + func f_0() { + \(type) Example_0 { + func f_1() { + \(type) Example_1 { + func f_2() { + \(type) Example_2 { + ↓\(type) Example_3 {} + } + } + } + } + } + } + }) + """), + + .init(""" + switch example { + case .exampleCase: + \(type) Example_0 { + \(type) Example_1 { + func f_0() { + \(type) Example_2 { + ↓\(type) Example_3 {} + } + } + } + } + default: + func f_0() { + \(type) Example_0 { + func f_1() { + \(type) Example_1 { + func f_2() { + \(type) Example_2 { + ↓\(type) Example_3 {} + } + } + } + } + } + } + } + """) + ] + }) + + let description = RuleDescription( + identifier: NestingRule.description.identifier, + name: NestingRule.description.name, + description: NestingRule.description.description, + kind: .metrics, + nonTriggeringExamples: nonTriggeringExamples, + triggeringExamples: triggeringExamples + ) + + verifyRule(description, ruleConfiguration: ["always_allow_one_type_in_functions": true]) + } + + // swiftlint:disable:next function_body_length + func testNestingWithoutCheckNestingInClosuresAndStatements() { + var nonTriggeringExamples = NestingRule.description.nonTriggeringExamples + nonTriggeringExamples.append(contentsOf: ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + .init(""" + exampleFunc(closure: { + \(type) Example_0 { + \(type) Example_1 { + \(type) Example_2 {} + } + } + func f_0() { + func f_1() { + func f_2() { + func f_3() {} + } + } + } + }) + """), + + .init(""" + switch example { + case .exampleCase: + \(type) Example_0 { + \(type) Example_1 { + \(type) Example 2 {} + } + } + default: + func f_0() { + func f_1() { + func f_2() { + func f_3() {} + } + } + } + } + """) + ] + }) + + var triggeringExamples = ["class", "struct", "enum"].flatMap { type -> [Example] in + [ + .init(""" + \(type) Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + """), + + .init(""" + var example: Int { + \(type) Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + return 5 + } + """), + + .init(""" + var example: Int = 5 { + didSet { + \(type) Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + } + } + """), + + .init(""" + extension Example_0 { + \(type) Example_1 { + ↓\(type) Example_2 {} + } + } + """), + + .init(""" + \(type) Example_0 { + func f_0() { + \(type) Example_1 { + func f_1() { + func f_2() { + ↓\(type) Example_2 {} + ↓func f_3() {} + } + } + } + } + } + """) + ] + } + + triggeringExamples.append(contentsOf: [ + .init(""" + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + """), + + .init(""" + var example: Int { + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + return 5 + } + """), + + .init(""" + var example: Int = 5 { + didSet { + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + } + } + """), + + .init(""" + extension Example_0 { + func f_0() { + func f_1() { + func f_2() { + ↓func f_3() {} + } + } + } + } + """) + ]) + + let description = RuleDescription( + identifier: NestingRule.description.identifier, + name: NestingRule.description.name, + description: NestingRule.description.description, + kind: .metrics, + nonTriggeringExamples: nonTriggeringExamples, + triggeringExamples: triggeringExamples + ) + + verifyRule(description, ruleConfiguration: ["check_nesting_in_closures_and_statements": false]) + } +} diff --git a/Tests/SwiftLintFrameworkTests/RuleConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/RuleConfigurationTests.swift index c963cd232..a2b6a0085 100644 --- a/Tests/SwiftLintFrameworkTests/RuleConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/RuleConfigurationTests.swift @@ -70,20 +70,24 @@ class RuleConfigurationTests: XCTestCase { "type_level": [ "warning": 7, "error": 17 ], - "statement_level": [ + "function_level": [ "warning": 8, "error": 18 - ] + ], + "check_nesting_in_closures_and_statements": false, + "always_allow_one_type_in_functions": true ] as [String: Any] var nestingConfig = NestingConfiguration(typeLevelWarning: 0, typeLevelError: nil, - statementLevelWarning: 0, - statementLevelError: nil) + functionLevelWarning: 0, + functionLevelError: nil) do { try nestingConfig.apply(configuration: config) XCTAssertEqual(nestingConfig.typeLevel.warning, 7) - XCTAssertEqual(nestingConfig.statementLevel.warning, 8) + XCTAssertEqual(nestingConfig.functionLevel.warning, 8) XCTAssertEqual(nestingConfig.typeLevel.error, 17) - XCTAssertEqual(nestingConfig.statementLevel.error, 18) + XCTAssertEqual(nestingConfig.functionLevel.error, 18) + XCTAssert(nestingConfig.alwaysAllowOneTypeInFunctions) + XCTAssert(!nestingConfig.checkNestingInClosuresAndStatements) } catch { XCTFail("Failed to configure nested configurations") } @@ -93,8 +97,8 @@ class RuleConfigurationTests: XCTestCase { let config = 17 var nestingConfig = NestingConfiguration(typeLevelWarning: 0, typeLevelError: nil, - statementLevelWarning: 0, - statementLevelError: nil) + functionLevelWarning: 0, + functionLevelError: nil) checkError(ConfigurationError.unknownConfiguration) { try nestingConfig.apply(configuration: config) }