diff --git a/CHANGELOG.md b/CHANGELOG.md index edd8c3b113..083275624c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,12 @@ 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) + +* Add `prefer_scaled_to_fit` opt-in rule to prefer `scaledToFit()` and + `scaledToFill()` over `aspectRatio(contentMode:)` with a constant + content mode. + [DemiDevv](https://github.com/DemiDevv) + [#5713](https://github.com/realm/SwiftLint/issues/5713) * Support access level modifiers on imports in `unused_imports` rule. [SimplyDanny](https://github.com/SimplyDanny) diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 538d385ce8..04c3e4e24a 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -158,6 +158,7 @@ public let builtInRules: [any Rule.Type] = [ PreferConditionListRule.self, PreferKeyPathRule.self, PreferNimbleRule.self, + PreferScaledToFitRule.self, PreferSelfInStaticReferencesRule.self, PreferSelfTypeOverTypeOfSelfRule.self, PreferTypeCheckingRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitRule.swift new file mode 100644 index 0000000000..60a6cc38a4 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitRule.swift @@ -0,0 +1,70 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule(optIn: true) +struct PreferScaledToFitRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "prefer_scaled_to_fit", + name: "Prefer Scaled To Fit", + description: "Prefer `scaledToFit()` or `scaledToFill()` over " + + "`aspectRatio(contentMode:)` with a constant content mode", + kind: .idiomatic, + nonTriggeringExamples: [ + Example("view.aspectRatio(ratio, contentMode: .fit)"), + Example("view.aspectRatio(ratio, contentMode: .fill)"), + Example("view.aspectRatio(contentMode: contentMode)"), + Example("view.aspectRatio(contentMode: shouldFit ? .fit : .fill)"), + Example("view.scaledToFit()"), + Example("view.scaledToFill()"), + ], + triggeringExamples: [ + Example("view.↓aspectRatio(contentMode: .fit)"), + Example("view.↓aspectRatio(contentMode: .fill)"), + Example("↓aspectRatio(contentMode: .fit)"), + Example("↓aspectRatio(contentMode: .fill)"), + ] + ) +} + +private extension PreferScaledToFitRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: FunctionCallExprSyntax) { + let functionName: String + let violationPosition: AbsolutePosition + + if let memberAccess = node.calledExpression.as(MemberAccessExprSyntax.self) { + functionName = memberAccess.declName.baseName.text + violationPosition = memberAccess.declName.baseName.positionAfterSkippingLeadingTrivia + } else if let declRef = node.calledExpression.as(DeclReferenceExprSyntax.self) { + functionName = declRef.baseName.text + violationPosition = declRef.baseName.positionAfterSkippingLeadingTrivia + } else { + return + } + + guard functionName == "aspectRatio" else { + return + } + + guard + node.arguments.count == 1, + let argument = node.arguments.first, + argument.label?.text == "contentMode" + else { + return + } + + guard + let memberValue = argument.expression.as(MemberAccessExprSyntax.self), + let valueName = memberValue.declName.baseName.text as String?, + valueName == "fit" || valueName == "fill" + else { + return + } + + violations.append(violationPosition) + } + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_07.swift b/Tests/GeneratedTests/GeneratedTests_07.swift index 2637e38154..2d3b72d003 100644 --- a/Tests/GeneratedTests/GeneratedTests_07.swift +++ b/Tests/GeneratedTests/GeneratedTests_07.swift @@ -43,6 +43,12 @@ final class PreferNimbleRuleGeneratedTests: SwiftLintTestCase { } } +final class PreferScaledToFitRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferScaledToFitRule.description) + } +} + final class PreferSelfInStaticReferencesRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PreferSelfInStaticReferencesRule.description) @@ -150,9 +156,3 @@ final class RawValueForCamelCasedCodableEnumRuleGeneratedTests: SwiftLintTestCas verifyRule(RawValueForCamelCasedCodableEnumRule.description) } } - -final class ReduceBooleanRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ReduceBooleanRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_08.swift b/Tests/GeneratedTests/GeneratedTests_08.swift index 1dd9f6da6b..70443d3392 100644 --- a/Tests/GeneratedTests/GeneratedTests_08.swift +++ b/Tests/GeneratedTests/GeneratedTests_08.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class ReduceBooleanRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ReduceBooleanRule.description) + } +} + final class ReduceIntoRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ReduceIntoRule.description) @@ -150,9 +156,3 @@ final class SortedImportsRuleGeneratedTests: SwiftLintTestCase { verifyRule(SortedImportsRule.description) } } - -final class StatementPositionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(StatementPositionRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_09.swift b/Tests/GeneratedTests/GeneratedTests_09.swift index 53caca1c0c..fceabc3673 100644 --- a/Tests/GeneratedTests/GeneratedTests_09.swift +++ b/Tests/GeneratedTests/GeneratedTests_09.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class StatementPositionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(StatementPositionRule.description) + } +} + final class StaticOperatorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(StaticOperatorRule.description) @@ -150,9 +156,3 @@ final class UnneededBreakInSwitchRuleGeneratedTests: SwiftLintTestCase { verifyRule(UnneededBreakInSwitchRule.description) } } - -final class UnneededEscapingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnneededEscapingRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_10.swift b/Tests/GeneratedTests/GeneratedTests_10.swift index 49ccf753b2..a5a412c499 100644 --- a/Tests/GeneratedTests/GeneratedTests_10.swift +++ b/Tests/GeneratedTests/GeneratedTests_10.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class UnneededEscapingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnneededEscapingRule.description) + } +} + final class UnneededOverrideRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnneededOverrideRule.description) @@ -150,9 +156,3 @@ final class VoidReturnRuleGeneratedTests: SwiftLintTestCase { verifyRule(VoidReturnRule.description) } } - -final class WeakDelegateRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(WeakDelegateRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_11.swift b/Tests/GeneratedTests/GeneratedTests_11.swift index 66e2e1afe3..c444d0081c 100644 --- a/Tests/GeneratedTests/GeneratedTests_11.swift +++ b/Tests/GeneratedTests/GeneratedTests_11.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class WeakDelegateRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(WeakDelegateRule.description) + } +} + final class XCTFailMessageRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(XCTFailMessageRule.description) diff --git a/Tests/IntegrationTests/Resources/default_rule_configurations.yml b/Tests/IntegrationTests/Resources/default_rule_configurations.yml index a9722b64fe..3d93554219 100644 --- a/Tests/IntegrationTests/Resources/default_rule_configurations.yml +++ b/Tests/IntegrationTests/Resources/default_rule_configurations.yml @@ -914,6 +914,11 @@ prefer_nimble: meta: opt-in: true correctable: false +prefer_scaled_to_fit: + severity: warning + meta: + opt-in: true + correctable: false prefer_self_in_static_references: severity: warning meta: