Skip to content

Commit d0539eb

Browse files
DimDLSimplyDanny
andcommitted
Add new legacy_uigraphics_function rule
Co-authored-by: Danny Mösch <danny.moesch@icloud.com>
1 parent f242859 commit d0539eb

12 files changed

Lines changed: 137 additions & 42 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,21 @@
5656

5757
* Add new default `invisible_character` rule that detects invisible characters
5858
like zero-width space (U+200B), zero-width non-joiner (U+200C),
59-
and FEFF formatting character (U+FEFF) in string literals, which can cause hard-to-debug issues.
59+
and FEFF formatting character (U+FEFF) in string literals, which can cause
60+
hard-to-debug issues.
6061
[kapitoshka438](https://github.com/kapitoshka438)
6162
[#6045](https://github.com/realm/SwiftLint/issues/6045)
6263

6364
* Add `variable_shadowing` rule that flags when a variable declaration shadows
6465
an identifier from an outer scope.
6566
[nadeemnali](https://github.com/nadeemnali)
6667
[#6228](https://github.com/realm/SwiftLint/issues/6228)
68+
69+
* Add `legacy_uigraphics_functions` rule to encourage the use of modern
70+
`UIGraphicsImageRenderer` instead of the legacy `UIGraphics{Begin|End}ImageContext`.
71+
The modern replacement is safer, cleaner, Retina-aware and more performant.
72+
[Dimitri Dupuis-Latour](https://github.com/DimDL)
73+
[#6268](https://github.com/realm/SwiftLint/issues/6268)
6774

6875
### Bug Fixes
6976

Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public let builtInRules: [any Rule.Type] = [
110110
LegacyNSGeometryFunctionsRule.self,
111111
LegacyObjcTypeRule.self,
112112
LegacyRandomRule.self,
113+
LegacyUIGraphicsFunctionRule.self,
113114
LetVarWhitespaceRule.self,
114115
LineLengthRule.self,
115116
LiteralExpressionEndIndentationRule.self,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import SwiftSyntax
2+
3+
@SwiftSyntaxRule(optIn: true)
4+
struct LegacyUIGraphicsFunctionRule: Rule {
5+
var configuration = SeverityConfiguration<Self>(.warning)
6+
7+
static let description = RuleDescription(
8+
identifier: "legacy_uigraphics_function",
9+
name: "Legacy UIGraphics Function",
10+
description: "Prefer using `UIGraphicsImageRenderer` over legacy functions",
11+
rationale: "The modern replacement is safer, cleaner, Retina-aware and more performant.",
12+
kind: .idiomatic,
13+
nonTriggeringExamples: [
14+
Example("""
15+
let renderer = UIGraphicsImageRenderer(size: bounds.size)
16+
let screenshot = renderer.image { _ in
17+
myUIView.drawHierarchy(in: bounds, afterScreenUpdates: true)
18+
}
19+
"""),
20+
21+
Example("""
22+
let renderer = UIGraphicsImageRenderer(size: newSize)
23+
let combined = renderer.image { _ in
24+
background.draw(in: CGRect(origin: .zero, size: newSize))
25+
watermark.draw(in: CGRect(origin: .zero, size: watermarkSize))
26+
}
27+
"""),
28+
29+
Example("""
30+
UIGraphicsImageRenderer(size: newSize, format: UIGraphicsImageRendererFormat()).image { _ in
31+
image.draw(in: CGRect(origin: .zero, size: newSize))
32+
}
33+
"""),
34+
],
35+
triggeringExamples: [
36+
Example("""
37+
↓UIGraphicsBeginImageContext(newSize)
38+
myUIView.drawHierarchy(in: bounds, afterScreenUpdates: false)
39+
let optionalScreenshot = ↓UIGraphicsGetImageFromCurrentImageContext()
40+
↓UIGraphicsEndImageContext()
41+
"""),
42+
43+
Example("""
44+
↓UIGraphicsBeginImageContext(newSize)
45+
background.draw(in: CGRect(origin: .zero, size: newSize))
46+
watermark.draw(in: CGRect(origin: .zero, size: watermarkSize))
47+
let optionalOutput = ↓UIGraphicsGetImageFromCurrentImageContext()
48+
↓UIGraphicsEndImageContext()
49+
"""),
50+
51+
Example("""
52+
↓UIGraphicsBeginImageContextWithOptions(newSize, true, 1.0)
53+
image.draw(in: CGRect(origin: .zero, size: newSize))
54+
let optionalOutput = ↓UIGraphicsGetImageFromCurrentImageContext()
55+
↓UIGraphicsEndImageContext()
56+
"""),
57+
]
58+
)
59+
}
60+
61+
private extension LegacyUIGraphicsFunctionRule {
62+
final class Visitor: LegacyFunctionVisitor<ConfigurationType> {
63+
private static let legacyUIGraphicsFunctions: [String: LegacyFunctionRewriteStrategy] = [
64+
"UIGraphicsBeginImageContext": .noRewrite,
65+
"UIGraphicsBeginImageContextWithOptions": .noRewrite,
66+
"UIGraphicsGetImageFromCurrentImageContext": .noRewrite,
67+
"UIGraphicsEndImageContext": .noRewrite,
68+
]
69+
70+
init(configuration: ConfigurationType, file: SwiftLintFile) {
71+
super.init(configuration: configuration, file: file, legacyFunctions: Self.legacyUIGraphicsFunctions)
72+
}
73+
}
74+
}

Source/SwiftLintCore/Visitors/LegacyFunctionVisitor+Rewriter.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ open class LegacyFunctionVisitor<Configuration: RuleConfiguration>: ViolationsSy
2727
}
2828

2929
/// Strategy to apply when rewriting a legacy function call.
30-
public enum LegacyFunctionRewriteStrategy: Sendable {
30+
public enum LegacyFunctionRewriteStrategy: Equatable, Sendable {
31+
/// Don't rewrite the function call, just report it as a violation.
32+
case noRewrite
3133
/// Replace with equality check between the two arguments.
3234
case equal
3335
/// Replace with property access with name ``name`` on the argument.
@@ -39,6 +41,7 @@ public enum LegacyFunctionRewriteStrategy: Sendable {
3941

4042
fileprivate var expectedInitialArguments: Int {
4143
switch self {
44+
case .noRewrite: -1
4245
case .equal: 2
4346
case .property: 1
4447
case .function(name: _, argumentLabels: let argumentLabels, reversed: _): argumentLabels.count + 1
@@ -84,7 +87,7 @@ open class LegacyFunctionRewriter<Configuration: RuleConfiguration>: ViolationsS
8487
.map { $0.isEmpty ? "\($1)" : "\($0): \($1)" }
8588
.joined(separator: ", ")
8689
expr = "\(arguments[0]).\(raw: functionName)(\(raw: params))"
87-
case .none:
90+
case .none, .noRewrite:
8891
return super.visit(node)
8992
}
9093

@@ -97,11 +100,10 @@ open class LegacyFunctionRewriter<Configuration: RuleConfiguration>: ViolationsS
97100
private extension FunctionCallExprSyntax {
98101
func isLegacyFunctionExpression(legacyFunctions: [String: LegacyFunctionRewriteStrategy]) -> Bool {
99102
guard let calledExpression = calledExpression.as(DeclReferenceExprSyntax.self),
100-
let rewriteStrategy = legacyFunctions[calledExpression.baseName.text],
101-
arguments.count == rewriteStrategy.expectedInitialArguments else {
103+
let rewriteStrategy = legacyFunctions[calledExpression.baseName.text] else {
102104
return false
103105
}
104-
return true
106+
return rewriteStrategy == .noRewrite || arguments.count == rewriteStrategy.expectedInitialArguments
105107
}
106108
}
107109

Tests/GeneratedTests/GeneratedTests_05.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ final class LegacyRandomRuleGeneratedTests: SwiftLintTestCase {
5555
}
5656
}
5757

58+
final class LegacyUIGraphicsFunctionRuleGeneratedTests: SwiftLintTestCase {
59+
func testWithDefaultConfiguration() {
60+
verifyRule(LegacyUIGraphicsFunctionRule.description)
61+
}
62+
}
63+
5864
final class LetVarWhitespaceRuleGeneratedTests: SwiftLintTestCase {
5965
func testWithDefaultConfiguration() {
6066
verifyRule(LetVarWhitespaceRule.description)
@@ -150,9 +156,3 @@ final class MultipleClosuresWithTrailingClosureRuleGeneratedTests: SwiftLintTest
150156
verifyRule(MultipleClosuresWithTrailingClosureRule.description)
151157
}
152158
}
153-
154-
final class NSLocalizedStringKeyRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(NSLocalizedStringKeyRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_06.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class NSLocalizedStringKeyRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(NSLocalizedStringKeyRule.description)
13+
}
14+
}
15+
1016
final class NSLocalizedStringRequireBundleRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(NSLocalizedStringRequireBundleRule.description)
@@ -150,9 +156,3 @@ final class OverrideInExtensionRuleGeneratedTests: SwiftLintTestCase {
150156
verifyRule(OverrideInExtensionRule.description)
151157
}
152158
}
153-
154-
final class PatternMatchingKeywordsRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(PatternMatchingKeywordsRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_07.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class PatternMatchingKeywordsRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(PatternMatchingKeywordsRule.description)
13+
}
14+
}
15+
1016
final class PeriodSpacingRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(PeriodSpacingRule.description)
@@ -150,9 +156,3 @@ final class ReduceBooleanRuleGeneratedTests: SwiftLintTestCase {
150156
verifyRule(ReduceBooleanRule.description)
151157
}
152158
}
153-
154-
final class ReduceIntoRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(ReduceIntoRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_08.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class ReduceIntoRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(ReduceIntoRule.description)
13+
}
14+
}
15+
1016
final class RedundantDiscardableLetRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(RedundantDiscardableLetRule.description)
@@ -150,9 +156,3 @@ final class StatementPositionRuleGeneratedTests: SwiftLintTestCase {
150156
verifyRule(StatementPositionRule.description)
151157
}
152158
}
153-
154-
final class StaticOperatorRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(StaticOperatorRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_09.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class StaticOperatorRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(StaticOperatorRule.description)
13+
}
14+
}
15+
1016
final class StaticOverFinalClassRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(StaticOverFinalClassRule.description)
@@ -150,9 +156,3 @@ final class UnneededEscapingRuleGeneratedTests: SwiftLintTestCase {
150156
verifyRule(UnneededEscapingRule.description)
151157
}
152158
}
153-
154-
final class UnneededOverrideRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(UnneededOverrideRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_10.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class UnneededOverrideRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(UnneededOverrideRule.description)
13+
}
14+
}
15+
1016
final class UnneededParenthesesInClosureArgumentRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(UnneededParenthesesInClosureArgumentRule.description)
@@ -150,9 +156,3 @@ final class WeakDelegateRuleGeneratedTests: SwiftLintTestCase {
150156
verifyRule(WeakDelegateRule.description)
151157
}
152158
}
153-
154-
final class XCTFailMessageRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(XCTFailMessageRule.description)
157-
}
158-
}

0 commit comments

Comments
 (0)