diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b4945fef..eaec196e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,11 @@ ### Enhancements -* None. +* Add `legacy_uigraphics_functions` rule to encourage the use of modern + UIGraphicsImageRenderer instead of legacy UIGraphics{Begin|End}ImageContext. + The modern replacement is safer, cleaner, Retina-aware and more performant. + [Dimitri Dupuis-Latour](https://github.com/DimDL) + [#6268](https://github.com/realm/SwiftLint/issues/6268) ### Bug Fixes diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 30f78d9dc9..dd1fa40311 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -107,6 +107,7 @@ public let builtInRules: [any Rule.Type] = [ LegacyNSGeometryFunctionsRule.self, LegacyObjcTypeRule.self, LegacyRandomRule.self, + LegacyUIGraphicsFunctionsRule.self, LetVarWhitespaceRule.self, LineLengthRule.self, LiteralExpressionEndIndentationRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyUIGraphicsFunctionsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyUIGraphicsFunctionsRule.swift new file mode 100644 index 0000000000..e2c5218182 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyUIGraphicsFunctionsRule.swift @@ -0,0 +1,82 @@ +import SwiftSyntax + +@SwiftSyntaxRule +struct LegacyUIGraphicsFunctionsRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "legacy_uigraphics_functions", + name: "Legacy UIGraphics Functions", + description: "Prefer using `UIGraphicsImageRenderer` over legacy functions", + rationale: "The modern replacement is safer, cleaner, Retina-aware and more performant", + kind: .idiomatic, + nonTriggeringExamples: [ + Example(""" + let renderer = UIGraphicsImageRenderer(size: bounds.size) + let screenshot = renderer.image { _ in + myUIView.drawHierarchy(in: bounds, afterScreenUpdates: true) + } + """), + + Example(""" + let renderer = UIGraphicsImageRenderer(size: newSize) + let combined = renderer.image { _ in + background.draw(in: CGRect(origin: .zero, size: newSize)) + watermark.draw(in: CGRect(origin: .zero, size: watermarkSize)) + } + """), + + Example(""" + let format = UIGraphicsImageRendererFormat() + format.scale = 1.0 + format.opaque = true + + let renderer = UIGraphicsImageRenderer(size: newSize, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: newSize)) + } + """), + ], + triggeringExamples: [ + Example(""" + ↓UIGraphicsBeginImageContext(newSize) + myUIView.drawHierarchy(in: bounds, afterScreenUpdates: false) + let optionalScreenshot = ↓UIGraphicsGetImageFromCurrentImageContext() + ↓UIGraphicsEndImageContext() + """), + + Example(""" + ↓UIGraphicsBeginImageContext(newSize) + background.draw(in: CGRect(origin: .zero, size: newSize)) + watermark.draw(in: CGRect(origin: .zero, size: watermarkSize)) + let optionalOutput = ↓UIGraphicsGetImageFromCurrentImageContext() + ↓UIGraphicsEndImageContext() + """), + + Example(""" + ↓UIGraphicsBeginImageContextWithOptions(newSize, true, 1.0) + image.draw(in: CGRect(origin: .zero, size: newSize)) + let optionalOutput = ↓UIGraphicsGetImageFromCurrentImageContext() + ↓UIGraphicsEndImageContext() + """), + ] + ) +} + +private extension LegacyUIGraphicsFunctionsRule { + final class Visitor: ViolationsSyntaxVisitor { + private static let legacyUIGraphicsFunctions: Set = [ + "UIGraphicsBeginImageContext", + "UIGraphicsBeginImageContextWithOptions", + "UIGraphicsGetImageFromCurrentImageContext", + "UIGraphicsEndImageContext", + ] + + override func visitPost(_ node: FunctionCallExprSyntax) { + if let function = node.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text, + Self.legacyUIGraphicsFunctions.contains(function) { + violations.append(node.positionAfterSkippingLeadingTrivia) + } + } + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_05.swift b/Tests/GeneratedTests/GeneratedTests_05.swift index 26a5a1a72a..20801bb8eb 100644 --- a/Tests/GeneratedTests/GeneratedTests_05.swift +++ b/Tests/GeneratedTests/GeneratedTests_05.swift @@ -37,6 +37,12 @@ final class LegacyRandomRuleGeneratedTests: SwiftLintTestCase { } } +final class LegacyUIGraphicsFunctionsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyUIGraphicsFunctionsRule.description) + } +} + final class LetVarWhitespaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LetVarWhitespaceRule.description) @@ -150,9 +156,3 @@ final class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase { verifyRule(NSObjectPreferIsEqualRule.description) } } - -final class NestingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NestingRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_06.swift b/Tests/GeneratedTests/GeneratedTests_06.swift index 1f586a1f32..be794e08bd 100644 --- a/Tests/GeneratedTests/GeneratedTests_06.swift +++ b/Tests/GeneratedTests/GeneratedTests_06.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class NestingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NestingRule.description) + } +} + final class NimbleOperatorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NimbleOperatorRule.description) @@ -150,9 +156,3 @@ final class PreferKeyPathRuleGeneratedTests: SwiftLintTestCase { verifyRule(PreferKeyPathRule.description) } } - -final class PreferNimbleRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PreferNimbleRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_07.swift b/Tests/GeneratedTests/GeneratedTests_07.swift index a27cb25e98..cee0783997 100644 --- a/Tests/GeneratedTests/GeneratedTests_07.swift +++ b/Tests/GeneratedTests/GeneratedTests_07.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class PreferNimbleRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferNimbleRule.description) + } +} + final class PreferSelfInStaticReferencesRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PreferSelfInStaticReferencesRule.description) @@ -150,9 +156,3 @@ final class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase { verifyRule(RedundantSelfInClosureRule.description) } } - -final class RedundantSendableRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantSendableRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_08.swift b/Tests/GeneratedTests/GeneratedTests_08.swift index 068ffab49e..404c7ed757 100644 --- a/Tests/GeneratedTests/GeneratedTests_08.swift +++ b/Tests/GeneratedTests/GeneratedTests_08.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class RedundantSendableRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantSendableRule.description) + } +} + final class RedundantSetAccessControlRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantSetAccessControlRule.description) @@ -150,9 +156,3 @@ final class SwitchCaseAlignmentRuleGeneratedTests: SwiftLintTestCase { verifyRule(SwitchCaseAlignmentRule.description) } } - -final class SwitchCaseOnNewlineRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SwitchCaseOnNewlineRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_09.swift b/Tests/GeneratedTests/GeneratedTests_09.swift index 1d8dc7614d..0fedfd4eae 100644 --- a/Tests/GeneratedTests/GeneratedTests_09.swift +++ b/Tests/GeneratedTests/GeneratedTests_09.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class SwitchCaseOnNewlineRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SwitchCaseOnNewlineRule.description) + } +} + final class SyntacticSugarRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SyntacticSugarRule.description) @@ -150,9 +156,3 @@ final class UnusedControlFlowLabelRuleGeneratedTests: SwiftLintTestCase { verifyRule(UnusedControlFlowLabelRule.description) } } - -final class UnusedDeclarationRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedDeclarationRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_10.swift b/Tests/GeneratedTests/GeneratedTests_10.swift index a5ce334fc5..648dde1162 100644 --- a/Tests/GeneratedTests/GeneratedTests_10.swift +++ b/Tests/GeneratedTests/GeneratedTests_10.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class UnusedDeclarationRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedDeclarationRule.description) + } +} + final class UnusedEnumeratedRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedEnumeratedRule.description) diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index e324f818ff..a3bd5e2161 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -607,6 +607,11 @@ legacy_random: meta: opt-in: false correctable: false +legacy_uigraphics_functions: + severity: warning + meta: + opt-in: false + correctable: false let_var_whitespace: severity: warning meta: