diff --git a/CHANGELOG.md b/CHANGELOG.md index ff7d461726..f86310e703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ #### Enhancements +* Add `reduce_boolean` rule to prefer simpler constructs over `reduce(Boolean)`. + [Xavier Lowmiller](https://github.com/xavierLowmiller) + [#2675](https://github.com/realm/SwiftLint/issues/2675) + * Add `nsobject_prefer_isequal` rule to warn against implementing `==` on an `NSObject` subclass as calling `isEqual` (i.e. when using the class from Objective-C) will will not use the defined `==` method. diff --git a/Rules.md b/Rules.md index c630e787cc..cbed13614a 100644 --- a/Rules.md +++ b/Rules.md @@ -114,6 +114,7 @@ * [Quick Discouraged Call](#quick-discouraged-call) * [Quick Discouraged Focused Test](#quick-discouraged-focused-test) * [Quick Discouraged Pending Test](#quick-discouraged-pending-test) +* [Reduce Boolean](#reduce-boolean) * [Redundant Discardable Let](#redundant-discardable-let) * [Redundant Nil Coalescing](#redundant-nil-coalescing) * [Redundant @objc Attribute](#redundant-@objc-attribute) @@ -15783,6 +15784,67 @@ class TotoTests: QuickSpec { +## Reduce Boolean + +Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version +--- | --- | --- | --- | --- | --- +`reduce_boolean` | Enabled | No | performance | No | 4.2.0 + +Prefer using `.allSatisfy()` or `.contains()` over `reduce(true)` or `reduce(false)` + +### Examples + +
+Non Triggering Examples + +```swift +nums.reduce(0) { $0.0 + $0.1 } +``` + +```swift +nums.reduce(0.0) { $0.0 + $0.1 } +``` + +
+
+Triggering Examples + +```swift +let allNines = nums.↓reduce(true) { $0.0 && $0.1 == 9 } +``` + +```swift +let anyNines = nums.↓reduce(false) { $0.0 || $0.1 == 9 } +``` + +```swift +let allValid = validators.↓reduce(true) { $0 && $1(input) } +``` + +```swift +let anyValid = validators.↓reduce(false) { $0 || $1(input) } +``` + +```swift +let allNines = nums.↓reduce(true, { $0.0 && $0.1 == 9 }) +``` + +```swift +let anyNines = nums.↓reduce(false, { $0.0 || $0.1 == 9 }) +``` + +```swift +let allValid = validators.↓reduce(true, { $0 && $1(input) }) +``` + +```swift +let anyValid = validators.↓reduce(false, { $0 || $1(input) }) +``` + +
+ + + ## Redundant Discardable Let Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 4c0741e9db..7d1c46de12 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -115,6 +115,7 @@ public let masterRuleList = RuleList(rules: [ QuickDiscouragedCallRule.self, QuickDiscouragedFocusedTestRule.self, QuickDiscouragedPendingTestRule.self, + ReduceBooleanRule.self, RedundantDiscardableLetRule.self, RedundantNilCoalescingRule.self, RedundantObjcAttributeRule.self, diff --git a/Source/SwiftLintFramework/Rules/Lint/NSObjectPreferIsEqualRule.swift b/Source/SwiftLintFramework/Rules/Lint/NSObjectPreferIsEqualRule.swift index 99896f2449..08fe81e935 100644 --- a/Source/SwiftLintFramework/Rules/Lint/NSObjectPreferIsEqualRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/NSObjectPreferIsEqualRule.swift @@ -60,8 +60,8 @@ public struct NSObjectPreferIsEqualRule: Rule, ConfigurationProviderRule, Automa private func areAllArguments(toMethod method: [String: SourceKitRepresentable], ofType typeName: String) -> Bool { - return method.enclosedVarParameters.reduce(true) { soFar, param -> Bool in - soFar && (param.typeName == typeName) + return method.enclosedVarParameters.allSatisfy { param in + param.typeName == typeName } } } diff --git a/Source/SwiftLintFramework/Rules/Performance/ReduceBooleanRule.swift b/Source/SwiftLintFramework/Rules/Performance/ReduceBooleanRule.swift new file mode 100644 index 0000000000..57d76dd552 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Performance/ReduceBooleanRule.swift @@ -0,0 +1,50 @@ +import SourceKittenFramework + +public struct ReduceBooleanRule: Rule, ConfigurationProviderRule, AutomaticTestableRule { + public var configuration = SeverityConfiguration(.warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "reduce_boolean", + name: "Reduce Boolean", + description: "Prefer using `.allSatisfy()` or `.contains()` over `reduce(true)` or `reduce(false)`", + kind: .performance, + minSwiftVersion: .fourDotTwo, + nonTriggeringExamples: [ + "nums.reduce(0) { $0.0 + $0.1 }", + "nums.reduce(0.0) { $0.0 + $0.1 }" + ], + triggeringExamples: [ + "let allNines = nums.↓reduce(true) { $0.0 && $0.1 == 9 }", + "let anyNines = nums.↓reduce(false) { $0.0 || $0.1 == 9 }", + "let allValid = validators.↓reduce(true) { $0 && $1(input) }", + "let anyValid = validators.↓reduce(false) { $0 || $1(input) }", + "let allNines = nums.↓reduce(true, { $0.0 && $0.1 == 9 })", + "let anyNines = nums.↓reduce(false, { $0.0 || $0.1 == 9 })", + "let allValid = validators.↓reduce(true, { $0 && $1(input) })", + "let anyValid = validators.↓reduce(false, { $0 || $1(input) })" + ] + ) + + public func validate(file: File) -> [StyleViolation] { + let pattern = "\\breduce\\((true|false)" + return file + .match(pattern: pattern, with: [.identifier, .keyword]) + .map { range in + let reason: String + if file.contents[Range(range, in: file.contents)!].contains("true") { + reason = "Use `allSatisfy` instead" + } else { + reason = "Use `contains` instead" + } + + return StyleViolation( + ruleDescription: type(of: self).description, + severity: configuration.severity, + location: Location(file: file, characterOffset: range.location), + reason: reason + ) + } + } +} diff --git a/Source/SwiftLintFramework/Rules/Style/UnneededParenthesesInClosureArgumentRule.swift b/Source/SwiftLintFramework/Rules/Style/UnneededParenthesesInClosureArgumentRule.swift index c7be19b853..a691137a2c 100644 --- a/Source/SwiftLintFramework/Rules/Style/UnneededParenthesesInClosureArgumentRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/UnneededParenthesesInClosureArgumentRule.swift @@ -83,11 +83,7 @@ public struct UnneededParenthesesInClosureArgumentRule: ConfigurationProviderRul } let parametersTokens = file.syntaxMap.tokens(inByteRange: parametersByteRange) - let parametersAreValid = parametersTokens.reduce(true) { isValid, token in - guard isValid else { - return false - } - + let parametersAreValid = parametersTokens.allSatisfy { token in let kind = SyntaxKind(rawValue: token.type) if kind == .identifier { return true diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index cf4783ce4f..9098d5cb4a 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 4A9A3A3A1DC1D75F00DF5183 /* HTMLReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A9A3A391DC1D75F00DF5183 /* HTMLReporter.swift */; }; 4DB7815E1CAD72BA00BC4723 /* LegacyCGGeometryFunctionsRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB7815C1CAD690100BC4723 /* LegacyCGGeometryFunctionsRule.swift */; }; 4DCB8E7F1CBE494E0070FCF0 /* RegexHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCB8E7D1CBE43640070FCF0 /* RegexHelpers.swift */; }; + 4E342B4C2215C793008E4EF8 /* ReduceBooleanRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E342B4A2215C6DF008E4EF8 /* ReduceBooleanRule.swift */; }; 57ED827B1CF656E3002B3513 /* JUnitReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57ED82791CF65183002B3513 /* JUnitReporter.swift */; }; 584B0D3A2112BA78002F7E25 /* SonarQubeReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B0D392112BA78002F7E25 /* SonarQubeReporter.swift */; }; 584B0D3C2112E8FB002F7E25 /* CannedSonarQubeReporterOutput.json in Resources */ = {isa = PBXBuildFile; fileRef = 584B0D3B2112E8FB002F7E25 /* CannedSonarQubeReporterOutput.json */; }; @@ -539,6 +540,7 @@ 4A9A3A391DC1D75F00DF5183 /* HTMLReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLReporter.swift; sourceTree = ""; }; 4DB7815C1CAD690100BC4723 /* LegacyCGGeometryFunctionsRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCGGeometryFunctionsRule.swift; sourceTree = ""; }; 4DCB8E7D1CBE43640070FCF0 /* RegexHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegexHelpers.swift; sourceTree = ""; }; + 4E342B4A2215C6DF008E4EF8 /* ReduceBooleanRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReduceBooleanRule.swift; sourceTree = ""; }; 5499CA961A2394B700783309 /* Components.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Components.plist; sourceTree = ""; }; 5499CA971A2394B700783309 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 57ED82791CF65183002B3513 /* JUnitReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JUnitReporter.swift; sourceTree = ""; }; @@ -1033,6 +1035,7 @@ D42D2B371E09CC0D00CD7A2E /* FirstWhereRule.swift */, D414D6AD21D22FF500960935 /* LastWhereRule.swift */, 429644B41FB0A99E00D75128 /* SortedFirstLastRule.swift */, + 4E342B4A2215C6DF008E4EF8 /* ReduceBooleanRule.swift */, ); path = Performance; sourceTree = ""; @@ -1762,7 +1765,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which sourcery >/dev/null; then\n make Source/SwiftLintFramework/Models/MasterRuleList.swift\nelse\n echo \"Sourcery not found, install with 'brew install sourcery'\"\nfi"; + shellScript = "if which sourcery >/dev/null; then\n make Source/SwiftLintFramework/Models/MasterRuleList.swift\nelse\n echo \"Sourcery not found, install with 'brew install sourcery'\"\nfi\n"; showEnvVarsInLog = 0; }; 6CF24C421FC99616008CB0B1 /* Update Tests/LinuxMain.swift */ = { @@ -1779,7 +1782,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which sourcery >/dev/null; then\n make Tests/LinuxMain.swift\nelse\n echo \"Sourcery not found, install with 'brew install sourcery'\"\nfi"; + shellScript = "if which sourcery >/dev/null; then\n make Tests/LinuxMain.swift\nelse\n echo \"Sourcery not found, install with 'brew install sourcery'\"\nfi\n"; showEnvVarsInLog = 0; }; C2265FAB1A4B86AC00158358 /* Check Xcode Version */ = { @@ -1871,6 +1874,7 @@ E88198571BEA953300333A11 /* ForceCastRule.swift in Sources */, D44AD2761C0AA5350048F7B0 /* LegacyConstructorRule.swift in Sources */, D41985E721F85014003BE2B7 /* NSLocalizedStringKeyRule.swift in Sources */, + 4E342B4C2215C793008E4EF8 /* ReduceBooleanRule.swift in Sources */, D286EC021E02DF6F0003CF72 /* SortedImportsRule.swift in Sources */, D40E041C1F46E3B30043BC4E /* SuperfluousDisableCommandRule.swift in Sources */, 82F614F42106015100D23904 /* MultilineArgumentsBracketsRule.swift in Sources */, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 608905f692..bd6c16645a 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -984,6 +984,12 @@ extension QuickDiscouragedPendingTestRuleTests { ] } +extension ReduceBooleanRuleTests { + static var allTests: [(String, (ReduceBooleanRuleTests) -> () throws -> Void)] = [ + ("testWithDefaultConfiguration", testWithDefaultConfiguration) + ] +} + extension RedundantDiscardableLetRuleTests { static var allTests: [(String, (RedundantDiscardableLetRuleTests) -> () throws -> Void)] = [ ("testWithDefaultConfiguration", testWithDefaultConfiguration) @@ -1593,6 +1599,7 @@ XCTMain([ testCase(QuickDiscouragedCallRuleTests.allTests), testCase(QuickDiscouragedFocusedTestRuleTests.allTests), testCase(QuickDiscouragedPendingTestRuleTests.allTests), + testCase(ReduceBooleanRuleTests.allTests), testCase(RedundantDiscardableLetRuleTests.allTests), testCase(RedundantNilCoalescingRuleTests.allTests), testCase(RedundantObjcAttributeRuleTests.allTests), diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift index 564688be19..d2ab101b03 100644 --- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift +++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift @@ -510,6 +510,12 @@ class QuickDiscouragedPendingTestRuleTests: XCTestCase { } } +class ReduceBooleanRuleTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(ReduceBooleanRule.description) + } +} + class RedundantDiscardableLetRuleTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantDiscardableLetRule.description)