Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2b0462a
Implement ReduceIntoInsteadOfLoop rule
snofla Jun 18, 2023
abf8ea9
Cleanups
snofla Jun 18, 2023
9d90eba
Fix build
snofla Jun 19, 2023
437da08
Move models to separate file
snofla Jun 19, 2023
31363d4
Move couple of extension helper functions
snofla Jun 19, 2023
4f0e6f8
Merge commit 'ad23d08cdaa7d656a6e96d6f6cc7b0d165f0a8f2'
snofla May 10, 2025
3746b58
Implement ReduceIntoInsteadOfLoop rule
snofla Jun 18, 2023
63bed40
Cleanups
snofla Jun 18, 2023
810a508
Fix build
snofla Jun 19, 2023
a4f3b78
Move models to separate file
snofla Jun 19, 2023
4df2cb9
Move couple of extension helper functions
snofla Jun 19, 2023
06cf616
Make it work with new SwiftSyntax
snofla May 11, 2025
74a4372
Rename
snofla May 11, 2025
5016d85
Fix new SwiftSyntax forStmt
snofla May 11, 2025
86560c5
Fix new SwiftSyntax-isms
snofla May 12, 2025
27c7188
Clean up helpers
snofla May 12, 2025
7ff5b30
Support array initialisation expression
snofla May 12, 2025
40c4ff6
Regenerate built-in rules to include our rule
snofla May 12, 2025
6822901
Handle assignment expression `varName = <...>`
snofla May 12, 2025
b92bce3
Clean up
snofla May 12, 2025
5ea7e80
Clean up
snofla May 12, 2025
f91b8e9
Add triggering/non-triggering examples for the unit tests
snofla May 12, 2025
1443cb7
Auto applied fixes
snofla May 12, 2025
4a67268
Make opt-in
snofla May 12, 2025
e773996
Add credit note
snofla May 12, 2025
a42d66b
Fix ChangeLog line len
snofla May 12, 2025
9e8fbbc
Add extra trailing space
snofla May 12, 2025
6ef9351
Merge pull request #2 from snofla/snofla/use-reduce-into-1
snofla May 12, 2025
ca19f7d
Remove old files
snofla May 12, 2025
5543177
Merge pull request #3 from snofla/snofla/use-reduce-into-2
snofla May 12, 2025
21446f1
Clean up merge warnings
snofla May 13, 2025
81175c1
Merge pull request #4 from snofla/snofla/use-reduce-into-2
snofla May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@

* Add new `allowed_numbers` option to the `no_magic_numbers` rule.
[Martin Redington](https://github.com/mildm8nnered)
* Add new `reduce_into_instead_of_loop` opt-in rule that triggers when a
`for ... in <sequence>` is used to update a sequence where a `reduce(into:)` is
preferred.
[Alfons Hoogervorst](https://github.com/snofla/SwiftLint)

### Bug Fixes

Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ public let builtInRules: [any Rule.Type] = [
QuickDiscouragedPendingTestRule.self,
RawValueForCamelCasedCodableEnumRule.self,
ReduceBooleanRule.self,
ReduceIntoInsteadOfLoopRule.self,
ReduceIntoRule.self,
RedundantDiscardableLetRule.self,
RedundantNilCoalescingRule.self,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
struct ReduceIntoInsteadOfLoopRuleExamples {
static let nonTriggeringExamples: [Example] = [
Example("""
let result: [SomeType] = someSequence.reduce(into: [], { result, eachN in
result.insert(eachN)
})
"""),
Example("""
let result: Set<SomeType> = someSequence.reduce(into: []], { result, eachN in
result.insert(eachN)
})
"""),
Example("""
let result: [SomeType1: SomeType2] = someSequence.reduce(into: [:], { result, eachN in
result[SomeType1Value] = SomeType2Value
})
"""),
]

static let triggeringExamples: [Example] =
triggeringArrayExamples
+ triggeringSetExamples
+ triggeringDictionaryExamples
}

extension ReduceIntoInsteadOfLoopRuleExamples {
private static let triggeringDictionaryExamples: [Example] = [
Example("""
var result: Dictionary<SomeType1, SomeType2> = [:]
for eachN in someSequence {
↓result[SomeType1Value] = SomeType2Value + eachN
}
"""),
Example("""
var result: Dictionary<SomeType1, SomeType2> = [:]
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
Example("""
var result: Dictionary<SomeType1, SomeType2> = .init()
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
Example("""
var result = Dictionary<SomeType1, SomeType2>()
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
]

private static let triggeringSetExamples: [Example] = [
Example("""
var result: Set<SomeType> = []
for eachN in someSequence {
↓result = result + [eachN]
}
"""),
Example("""
var result: Set<SomeType> = []
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
Example("""
var result: Set<SomeType> = .init()
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
Example("""
var result = Set<SomeType>()
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
]

private static let triggeringArrayExamples: [Example] = [
Example("""
var result: [SomeType] = []
for eachN in someSequence {
↓result[5] = eachN
}
"""),
Example("""
var result: [SomeType] = []
for eachN in someSequence {
↓result = result + [eachN]
}
"""),
Example("""
var result: [SomeType] = []
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
Example("""
var result: [SomeType] = .init()
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
Example("""
var result = Array<SomeType>()
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
Example("""
var result = [SomeType]()
for eachN in someSequence {
↓result.someMethod(eachN)
}
"""),
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SwiftSyntax

struct ReduceIntoInsteadOfLoopRuleHelpers { }

extension VariableDeclSyntax {
/// Binding is a var: "`var` someVar = <...>"
var isVar: Bool {
self.bindingSpecifier.tokenKind == .keyword(.var)
}

var identifier: String? {
guard let identifierPattern = self.firstPatternOf(IdentifierPatternSyntax.self),
case .identifier(let name) = identifierPattern.identifier.tokenKind else {
return nil
}
return name
}

/// Returns the first binding with a `pattern` of type
/// `type`.
func firstPatternOf<T: PatternSyntaxProtocol>(_ type: T.Type) -> T? {
let result = self.bindings.first { patternBinding in
patternBinding.pattern.as(type) != nil
}
return result?.pattern.as(type)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import SwiftSyntax

struct ReduceIntoInsteadOfLoopRuleModels {}

internal extension ReduceIntoInsteadOfLoopRule {
struct ReferencedVariable: Hashable {
let name: String
let position: AbsolutePosition
let kind: Kind

func hash(into hasher: inout Hasher) {
hasher.combine(self.name)
hasher.combine(self.position.utf8Offset)
hasher.combine(self.kind)
}
}

enum Kind: Hashable {
case method(name: String, arguments: Int)
case assignment(subscript: Bool)

func hash(into hasher: inout Hasher) {
switch self {
case let .method(name, arguments):
hasher.combine("method")
hasher.combine(name)
hasher.combine(arguments)
case let .assignment(`subscript`):
hasher.combine("assignment")
hasher.combine(`subscript`)
}
}
}

struct CollectionType: Hashable {
let name: String
let genericArguments: Int

static let types: [Self] = [
.set,
.array,
.dictionary,
]

static let array = Self(name: "Array", genericArguments: 1)
static let set = Self(name: "Set", genericArguments: 1)
static let dictionary = Self(name: "Dictionary", genericArguments: 2)

static let names: [String: Self] = {
Self.types.reduce(into: [String: Self]()) { partialResult, collectionType in
partialResult[collectionType.name] = collectionType
}
}()
}
}
6 changes: 6 additions & 0 deletions Tests/GeneratedTests/GeneratedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,12 @@ final class ReduceBooleanRuleGeneratedTests: SwiftLintTestCase {
}
}

final class ReduceIntoInsteadOfLoopRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(ReduceIntoInsteadOfLoopRule.description)
}
}

final class ReduceIntoRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(ReduceIntoRule.description)
Expand Down
5 changes: 5 additions & 0 deletions Tests/IntegrationTests/default_rule_configurations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,11 @@ reduce_into:
meta:
opt-in: true
correctable: false
reduce_into_instead_of_loop:
severity: warning
meta:
opt-in: true
correctable: false
redundant_discardable_let:
severity: warning
ignore_swiftui_view_bodies: false
Expand Down