Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift
JP Simard 3eb3772022 Compile with -strict-concurrency=complete (#5320)
* Compile with `-strict-concurrency=complete`

Only in Bazel for now, because this is considered an unsafe flag in
SwiftPM which would lead to warnings for downstream consumers of
SwiftLint using SwiftPM.

Some imports of SwiftSyntax need the `@preconcurrency` annotation until
https://github.com/apple/swift-syntax/pull/2322 is available in a
release.

The following SwiftLint libraries have `-strict-concurrency=complete`
applied:

* SwiftLintCoreMacros
* SwiftLintBuiltInRules
* SwiftLintExtraRules

The following SwiftLint libraries don't have the flag applied and need
to be migrated:

* SwiftLintCore
* swiftlint (CLI target)

So really the rules and macros are now being compiled with
`-strict-concurrency=complete`, but the core infrastructure of SwiftLint
is not.

Still, given that Swift 6 will eventually make these warnings errors by
default, it's good to prevent issues from creeping in earlier rather
than later.

* Add CI job to build with strict concurrency
2023-11-01 15:20:40 +00:00

182 lines
6.2 KiB
Swift

import SwiftSyntax
@SwiftSyntaxRule
struct ReduceIntoRule: OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "reduce_into",
name: "Reduce into",
description: "Prefer `reduce(into:_:)` over `reduce(_:_:)` for copy-on-write types",
kind: .performance,
nonTriggeringExamples: [
Example("""
let foo = values.reduce(into: "abc") { $0 += "\\($1)" }
"""),
Example("""
values.reduce(into: Array<Int>()) { result, value in
result.append(value)
}
"""),
Example("""
let rows = violations.enumerated().reduce(into: "") { rows, indexAndViolation in
rows.append(generateSingleRow(for: indexAndViolation.1, at: indexAndViolation.0 + 1))
}
"""),
Example("""
zip(group, group.dropFirst()).reduce(into: []) { result, pair in
result.append(pair.0 + pair.1)
}
"""),
Example("""
let foo = values.reduce(into: [String: Int]()) { result, value in
result["\\(value)"] = value
}
"""),
Example("""
let foo = values.reduce(into: Dictionary<String, Int>.init()) { result, value in
result["\\(value)"] = value
}
"""),
Example("""
let foo = values.reduce(into: [Int](repeating: 0, count: 10)) { result, value in
result.append(value)
}
"""),
Example("""
let foo = values.reduce(MyClass()) { result, value in
result.handleValue(value)
return result
}
""")
],
triggeringExamples: [
Example("""
let bar = values.↓reduce("abc") { $0 + "\\($1)" }
"""),
Example("""
values.↓reduce(Array<Int>()) { result, value in
result += [value]
}
"""),
Example("""
[1, 2, 3].↓reduce(Set<Int>()) { acc, value in
var result = acc
result.insert(value)
return result
}
"""),
Example("""
let rows = violations.enumerated().↓reduce("") { rows, indexAndViolation in
return rows + generateSingleRow(for: indexAndViolation.1, at: indexAndViolation.0 + 1)
}
"""),
Example("""
zip(group, group.dropFirst()).↓reduce([]) { result, pair in
result + [pair.0 + pair.1]
}
"""),
Example("""
let foo = values.↓reduce([String: Int]()) { result, value in
var result = result
result["\\(value)"] = value
return result
}
"""),
Example("""
let bar = values.↓reduce(Dictionary<String, Int>.init()) { result, value in
var result = result
result["\\(value)"] = value
return result
}
"""),
Example("""
let bar = values.↓reduce([Int](repeating: 0, count: 10)) { result, value in
return result + [value]
}
"""),
Example("""
extension Data {
var hexString: String {
return ↓reduce("") { (output, byte) -> String in
output + String(format: "%02x", byte)
}
}
}
""")
]
)
}
private extension ReduceIntoRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard let name = node.nameToken,
name.text == "reduce",
node.arguments.count == 2 || (node.arguments.count == 1 && node.trailingClosure != nil),
let firstArgument = node.arguments.first,
// would otherwise equal "into"
firstArgument.label == nil,
firstArgument.expression.isCopyOnWriteType else {
return
}
violations.append(name.positionAfterSkippingLeadingTrivia)
}
}
}
private extension FunctionCallExprSyntax {
var nameToken: TokenSyntax? {
if let expr = calledExpression.as(MemberAccessExprSyntax.self) {
return expr.declName.baseName
} else if let expr = calledExpression.as(DeclReferenceExprSyntax.self) {
return expr.baseName
}
return nil
}
}
private extension ExprSyntax {
var isCopyOnWriteType: Bool {
if self.is(StringLiteralExprSyntax.self) ||
self.is(DictionaryExprSyntax.self) ||
self.is(ArrayExprSyntax.self) {
return true
}
if let expr = self.as(FunctionCallExprSyntax.self) {
if let identifierExpr = expr.calledExpression.identifierExpr {
return identifierExpr.isCopyOnWriteType
} else if let memberAccesExpr = expr.calledExpression.as(MemberAccessExprSyntax.self),
memberAccesExpr.declName.baseName.text == "init",
let identifierExpr = memberAccesExpr.base?.identifierExpr {
return identifierExpr.isCopyOnWriteType
} else if expr.calledExpression.isCopyOnWriteType {
return true
}
}
return false
}
var identifierExpr: DeclReferenceExprSyntax? {
if let identifierExpr = self.as(DeclReferenceExprSyntax.self) {
return identifierExpr
} else if let specializeExpr = self.as(GenericSpecializationExprSyntax.self) {
return specializeExpr.expression.identifierExpr
}
return nil
}
}
private extension DeclReferenceExprSyntax {
private static let copyOnWriteTypes: Set = ["Array", "Dictionary", "Set"]
var isCopyOnWriteType: Bool {
Self.copyOnWriteTypes.contains(baseName.text)
}
}