diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 121c9e6032..4f3e10c793 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -150,6 +150,7 @@ public let builtInRules: [any Rule.Type] = [ PeriodSpacingRule.self, PreferKeyPathRule.self, PreferNimbleRule.self, + PreferScaledToFitAndScaledToFill.self, PreferSelfInStaticReferencesRule.self, PreferSelfTypeOverTypeOfSelfRule.self, PreferTypeCheckingRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift new file mode 100644 index 0000000000..32eaca459a --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift @@ -0,0 +1,92 @@ +import SwiftSyntax + +@SwiftSyntaxRule(explicitRewriter: true) +struct PreferScaledToFitAndScaledToFill: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "prefer_scaled_to_fit_and_scaled_to_fill", + name: "Prefer `scaledToFit()` and `scaledToFill()`", + description: "Prefer `scaledToFit` and `scaledToFill` to `aspectRatio`", + kind: .idiomatic, + nonTriggeringExamples: [ + Example(""" + let ratio = CGSize(width: 1, height: 1) + view.aspectRatio(ratio, contentMode: .fit) + view.aspectRatio(ratio, contentMode: .fill) + """), + Example(""" + let contentMode = ContentMode.fit + view.aspectRatio(contentMode: contentMode) + """), + Example(""" + let shouldFit = true + view.aspectRatio(contentMode: shouldFit ? .fit : .fill) + """), + ], + triggeringExamples: [ + Example("view.↓aspectRatio(contentMode: .fit)"), + Example("view.↓aspectRatio(contentMode: .fill)"), + Example("↓aspectRatio(contentMode: .fit)"), + Example("↓aspectRatio(contentMode: .fill)"), + ], + corrections: [ + Example("view.↓aspectRatio(contentMode: .fit)"): Example("view.scaledToFit()"), + Example("view.↓aspectRatio(contentMode: .fill)"): Example("view.scaledToFill()"), + Example("↓aspectRatio(contentMode: .fit)"): Example("scaledToFit()"), + Example("↓aspectRatio(contentMode: .fill)"): Example("scaledToFill()"), + ] + ) +} + +private extension PreferScaledToFitAndScaledToFill { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: FunctionCallExprSyntax) { + if node.hasViolation { + violations.append(node.positionAfterSkippingLeadingTrivia) + } + // if let expr = node.calledExpression.as(DeclReferenceExprSyntax.self), + // expr.baseName.text.starts(with: "XCTAssert") { + // violations.append(node.positionAfterSkippingLeadingTrivia) + // } + } + } + + final class Rewriter: ViolationsSyntaxRewriter { + override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax { + if node.hasViolation { + return ExprSyntax(stringLiteral: "A is C") + .with(\.leadingTrivia, node.leadingTrivia) + .with(\.trailingTrivia, node.trailingTrivia) + } + + return ExprSyntax(stringLiteral: "A is B") + .with(\.leadingTrivia, node.leadingTrivia) + .with(\.trailingTrivia, node.trailingTrivia) + } + } +} + +private extension FunctionCallExprSyntax { + var hasViolation: Bool { + name == "aspectRatio" + && argumentNames == ["contentMode"] + && argumentIsFitOrFill + } + + var name: String? { + guard let expr = calledExpression.as(DeclReferenceExprSyntax.self) else { + return nil + } + + return expr.baseName.text + } + + var argumentNames: [String?] { + arguments.map(\.label?.text) + } + + var argumentIsFitOrFill: Bool { + true + } +} diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 88e14517c1..c499ac0104 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -407,6 +407,8 @@ prefer_key_path: restrict_to_standard_functions: true prefer_nimble: severity: warning +prefer_scaled_to_fit_and_scaled_to_fill: + severity: warning prefer_self_in_static_references: severity: warning prefer_self_type_over_type_of_self: