From 80ffb8e014a3f011276cdf30c1c6a48a28c1570c Mon Sep 17 00:00:00 2001 From: Alexander Ikonomou Date: Thu, 22 Aug 2024 23:17:32 +0200 Subject: [PATCH 1/2] Add PreferScaledToFitAndScaledToFill rule with triggering and non-triggering examples. --- .../PreferScaledToFitAndScaledToFill.swift | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift new file mode 100644 index 0000000000..1cbd3dba2a --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift @@ -0,0 +1,45 @@ +import SwiftSyntax + +@SwiftSyntaxRule +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()` to `aspectRatio(contentMode: .fit)` and `scaledToFill` to `aspectRatio(contentMode: .fill)`", + 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)"), + ] + ) +} + +private extension PreferNimbleRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: FunctionCallExprSyntax) { + if let expr = node.calledExpression.as(DeclReferenceExprSyntax.self), + expr.baseName.text.starts(with: "XCTAssert") { + violations.append(node.positionAfterSkippingLeadingTrivia) + } + } + } +} From b8f0409b50e50e9fc57918c16ef597d2b4ca3d6c Mon Sep 17 00:00:00 2001 From: Alexander Ikonomou Date: Mon, 26 Aug 2024 23:09:12 +0200 Subject: [PATCH 2/2] Add examples for corrections. --- .../Models/BuiltInRules.swift | 1 + .../PreferScaledToFitAndScaledToFill.swift | 61 ++++++++++++++++--- .../default_rule_configurations.yml | 2 + 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 86105239e6..2ff45a9647 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, PreferZeroOverExplicitInitRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift index 1cbd3dba2a..32eaca459a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferScaledToFitAndScaledToFill.swift @@ -1,13 +1,13 @@ import SwiftSyntax -@SwiftSyntaxRule +@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()` to `aspectRatio(contentMode: .fit)` and `scaledToFill` to `aspectRatio(contentMode: .fill)`", + description: "Prefer `scaledToFit` and `scaledToFill` to `aspectRatio`", kind: .idiomatic, nonTriggeringExamples: [ Example(""" @@ -25,21 +25,68 @@ struct PreferScaledToFitAndScaledToFill: Rule { """), ], triggeringExamples: [ - Example("↓view.aspectRatio(contentMode: .fit)"), - Example("↓view.aspectRatio(contentMode: .fill)"), + 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 PreferNimbleRule { +private extension PreferScaledToFitAndScaledToFill { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: FunctionCallExprSyntax) { - if let expr = node.calledExpression.as(DeclReferenceExprSyntax.self), - expr.baseName.text.starts(with: "XCTAssert") { + 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 3edd735e5a..9bf4619c89 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -404,6 +404,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: