diff --git a/.swiftlint.yml b/.swiftlint.yml
index 6f0bd6043e..78e0b69dd4 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -27,6 +27,7 @@ opt_in_rules:
- file_header
- file_name
- first_where
+ - flatmap_over_map_reduce
- identical_operands
- joined_default_parameter
- legacy_random
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e84800628..a651a4a635 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,11 @@
[nvanfleet](https://github.com/nvanfleet)
[#2866](https://github.com/realm/SwiftLint/issues/2866)
+* Add `flatmap_over_map_reduce` opt-in rule to prefer
+ using `flatMap` over `map { ... }.reduce([], +)`.
+ [Marcelo Fabri](https://github.com/marcelofabri)
+ [#2883](https://github.com/realm/SwiftLint/issues/2883)
+
#### Bug Fixes
* None.
diff --git a/Rules.md b/Rules.md
index 302546b45e..33a48cbb47 100644
--- a/Rules.md
+++ b/Rules.md
@@ -54,6 +54,7 @@
* [File Name](#file-name)
* [File Types Order](#file-types-order)
* [First Where](#first-where)
+* [FlatMap over map and reduce](#flatmap-over-map-and-reduce)
* [For Where](#for-where)
* [Force Cast](#force-cast)
* [Force Try](#force-try)
@@ -8429,6 +8430,39 @@ if let pause = timeTracker.pauses.filter("beginDate < %@", beginDate).first { pr
+## FlatMap over map and reduce
+
+Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version
+--- | --- | --- | --- | --- | ---
+`flatmap_over_map_reduce` | Disabled | No | performance | No | 3.0.0
+
+Prefer `flatMap` over `map` followed by `reduce([], +)`.
+
+### Examples
+
+
+Non Triggering Examples
+
+```swift
+let foo = bar.map { $0.count }.reduce(0, +)
+```
+
+```swift
+let foo = bar.flatMap { $0.array }
+```
+
+
+
+Triggering Examples
+
+```swift
+let foo = ↓bar.map { $0.array }.reduce([], +)
+```
+
+
+
+
+
## For Where
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 b0ef7db703..b34c694029 100644
--- a/Source/SwiftLintFramework/Models/MasterRuleList.swift
+++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift
@@ -55,6 +55,7 @@ public let masterRuleList = RuleList(rules: [
FileNameRule.self,
FileTypesOrderRule.self,
FirstWhereRule.self,
+ FlatMapOverMapReduceRule.self,
ForWhereRule.self,
ForceCastRule.self,
ForceTryRule.self,
diff --git a/Source/SwiftLintFramework/Rules/Performance/FlatMapOverMapReduceRule.swift b/Source/SwiftLintFramework/Rules/Performance/FlatMapOverMapReduceRule.swift
new file mode 100644
index 0000000000..5f7f5636ca
--- /dev/null
+++ b/Source/SwiftLintFramework/Rules/Performance/FlatMapOverMapReduceRule.swift
@@ -0,0 +1,27 @@
+import SourceKittenFramework
+
+public struct FlatMapOverMapReduceRule: CallPairRule, OptInRule, ConfigurationProviderRule, AutomaticTestableRule {
+ public var configuration = SeverityConfiguration(.warning)
+
+ public init() {}
+
+ public static let description = RuleDescription(
+ identifier: "flatmap_over_map_reduce",
+ name: "FlatMap over map and reduce",
+ description: "Prefer `flatMap` over `map` followed by `reduce([], +)`.",
+ kind: .performance,
+ nonTriggeringExamples: [
+ "let foo = bar.map { $0.count }.reduce(0, +)",
+ "let foo = bar.flatMap { $0.array }"
+ ],
+ triggeringExamples: [
+ "let foo = ↓bar.map { $0.array }.reduce([], +)"
+ ]
+ )
+
+ public func validate(file: File) -> [StyleViolation] {
+ let pattern = "[\\}\\)]\\s*\\.reduce\\s*\\(\\[\\s*\\],\\s*\\+\\s*\\)"
+ return validate(file: file, pattern: pattern, patternSyntaxKinds: [.identifier],
+ callNameSuffix: ".map", severity: configuration.severity)
+ }
+}
diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj
index 239b92ec97..ed7ef78094 100644
--- a/SwiftLint.xcodeproj/project.pbxproj
+++ b/SwiftLint.xcodeproj/project.pbxproj
@@ -282,6 +282,7 @@
D45255C81F0932F8003C9B56 /* RuleDescription+Examples.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45255C71F0932F8003C9B56 /* RuleDescription+Examples.swift */; };
D462021F1E15F52D0027AAD1 /* NumberSeparatorRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = D462021E1E15F52D0027AAD1 /* NumberSeparatorRuleExamples.swift */; };
D46252541DF63FB200BE2CA1 /* NumberSeparatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46252531DF63FB200BE2CA1 /* NumberSeparatorRule.swift */; };
+ D466B620233D229F0068190B /* FlatMapOverMapReduceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D466B61F233D229F0068190B /* FlatMapOverMapReduceRule.swift */; };
D46A317F1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46A317E1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift */; };
D46E041D1DE3712C00728374 /* TrailingCommaRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46E041C1DE3712C00728374 /* TrailingCommaRule.swift */; };
D47079A71DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47079A61DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift */; };
@@ -779,6 +780,7 @@
D45255C71F0932F8003C9B56 /* RuleDescription+Examples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RuleDescription+Examples.swift"; sourceTree = ""; };
D462021E1E15F52D0027AAD1 /* NumberSeparatorRuleExamples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberSeparatorRuleExamples.swift; sourceTree = ""; };
D46252531DF63FB200BE2CA1 /* NumberSeparatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberSeparatorRule.swift; sourceTree = ""; };
+ D466B61F233D229F0068190B /* FlatMapOverMapReduceRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlatMapOverMapReduceRule.swift; sourceTree = ""; };
D46A317E1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnneededParenthesesInClosureArgumentRule.swift; sourceTree = ""; };
D46E041C1DE3712C00728374 /* TrailingCommaRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingCommaRule.swift; sourceTree = ""; };
D47079A61DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyParenthesesWithTrailingClosureRule.swift; sourceTree = ""; };
@@ -1088,6 +1090,7 @@
E847F0A81BFBBABD00EA9363 /* EmptyCountRule.swift */,
740DF1AF203F5AFC0081F694 /* EmptyStringRule.swift */,
D42D2B371E09CC0D00CD7A2E /* FirstWhereRule.swift */,
+ D466B61F233D229F0068190B /* FlatMapOverMapReduceRule.swift */,
D414D6AD21D22FF500960935 /* LastWhereRule.swift */,
756C0777222EA49400A111F4 /* ReduceIntoRule.swift */,
429644B41FB0A99E00D75128 /* SortedFirstLastRule.swift */,
@@ -2197,6 +2200,7 @@
4A9A3A3A1DC1D75F00DF5183 /* HTMLReporter.swift in Sources */,
D40F83881DE9179200524C62 /* TrailingCommaConfiguration.swift in Sources */,
827169B31F488181003FB9AF /* ExplicitEnumRawValueRule.swift in Sources */,
+ D466B620233D229F0068190B /* FlatMapOverMapReduceRule.swift in Sources */,
D41985E921FAB62F003BE2B7 /* DeploymentTargetRule.swift in Sources */,
62FE5D32200CABDD00F68793 /* DiscouragedOptionalCollectionExamples.swift in Sources */,
D49896F12026B36C00814A83 /* RedundantSetAccessControlRule.swift in Sources */,
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index ba9c7a8dd1..5d48007bfd 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -528,6 +528,12 @@ extension FirstWhereRuleTests {
]
}
+extension FlatMapOverMapReduceRuleTests {
+ static var allTests: [(String, (FlatMapOverMapReduceRuleTests) -> () throws -> Void)] = [
+ ("testWithDefaultConfiguration", testWithDefaultConfiguration)
+ ]
+}
+
extension ForWhereRuleTests {
static var allTests: [(String, (ForWhereRuleTests) -> () throws -> Void)] = [
("testWithDefaultConfiguration", testWithDefaultConfiguration)
@@ -1624,6 +1630,7 @@ XCTMain([
testCase(FileNameRuleTests.allTests),
testCase(FileTypesOrderRuleTests.allTests),
testCase(FirstWhereRuleTests.allTests),
+ testCase(FlatMapOverMapReduceRuleTests.allTests),
testCase(ForWhereRuleTests.allTests),
testCase(ForceCastRuleTests.allTests),
testCase(ForceTryRuleTests.allTests),
diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift
index 52d34728b5..19a4d02c32 100644
--- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift
+++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift
@@ -228,6 +228,12 @@ class FirstWhereRuleTests: XCTestCase {
}
}
+class FlatMapOverMapReduceRuleTests: XCTestCase {
+ func testWithDefaultConfiguration() {
+ verifyRule(FlatMapOverMapReduceRule.description)
+ }
+}
+
class ForWhereRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(ForWhereRule.description)