From f7035956f86560c6ca5ee8dad98cc0537774e47f Mon Sep 17 00:00:00 2001 From: Keith Smiley Date: Fri, 21 Jul 2023 11:39:30 -0700 Subject: [PATCH] Add unneeded_override rule This rule flags functions where the only thing they do is call their super function and therefore could be omitted. For example: ```swift override func foo() { super.foo() } ``` This can get pretty complex since there are a lot of slight variations the subclasses' functions can call the superclasses' functions with, but this covers many of the cases. Ideally this would handle variable overrides too but it doesn't currently. --- .../Models/BuiltInRules.swift | 1 + .../Rules/Lint/UnneededOverrideRule.swift | 89 ++++++++++ .../Lint/UnneededOverrideRuleExamples.swift | 156 ++++++++++++++++++ Tests/GeneratedTests/GeneratedTests.swift | 6 + 4 files changed, 252 insertions(+) create mode 100644 Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift create mode 100644 Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRuleExamples.swift diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index b57937438be..f35ac42bd9d 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -205,6 +205,7 @@ public let builtInRules: [Rule.Type] = [ UnavailableConditionRule.self, UnavailableFunctionRule.self, UnhandledThrowingTaskRule.self, + UnneededOverrideRule.self, UnneededBreakInSwitchRule.self, UnneededParenthesesInClosureArgumentRule.self, UnneededSynthesizedInitializerRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift new file mode 100644 index 00000000000..a3dcbaea16b --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift @@ -0,0 +1,89 @@ +import SwiftSyntax +import SwiftSyntaxBuilder + +struct UnneededOverrideRule: ConfigurationProviderRule, SwiftSyntaxRule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "unneeded_override", + name: "Remove empty overridden functions", + description: "Remove overridden functions that don't do anything except call their super", + kind: .lint, + nonTriggeringExamples: UnneededOverrideRule.nonTriggeringExamples, + triggeringExamples: UnneededOverrideRule.triggeringExamples + ) + + func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { + Visitor(viewMode: .sourceAccurate) + } +} + +private func simplify(_ node: SyntaxProtocol) -> ExprSyntaxProtocol? { + if let expr = node.as(AwaitExprSyntax.self) { + return expr.expression + } else if let expr = node.as(TryExprSyntax.self) { + // Assume using try! / try? changes behavior + if expr.questionOrExclamationMark != nil { + return nil + } + + return expr.expression + } else if let expr = node.as(FunctionCallExprSyntax.self) { + return expr + } else if let stmt = node.as(ReturnStmtSyntax.self) { + return stmt.expression + } + + return nil +} + +private final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: FunctionDeclSyntax) { + guard let modifiers = node.modifiers, modifiers.contains(where: { $0.name.text == "override" }), + node.body?.statements.count == 1, let statement = node.body?.statements.first else { + return + } + + // Assume having @available changes behavior + if node.attributes.contains(attributeNamed: "available") { + return + } + + // Extract the function call from other expressions like try / await / return. + // If this returns a non-super calling function that will get filtered out later + var syntax: ExprSyntaxProtocol? = simplify(statement.item) + while let nestedSyntax = syntax { + if nestedSyntax.as(FunctionCallExprSyntax.self) != nil { + break + } + + syntax = simplify(nestedSyntax) + } + + let overridenFunctionName = node.identifier.text + guard let call = syntax?.as(FunctionCallExprSyntax.self), + let member = call.calledExpression.as(MemberAccessExprSyntax.self), + member.base?.as(SuperRefExprSyntax.self) != nil, + member.name.text == overridenFunctionName else { + return + } + + // Assume any change in arguments passed means behavior was changed + let expectedArguments = node.signature.input.parameterList.map { + ($0.firstName.text == "_" ? "" : $0.firstName.text, $0.secondName?.text ?? $0.firstName.text) + } + let actualArguments = call.argumentList.map { + ($0.label?.text ?? "", $0.expression.as(IdentifierExprSyntax.self)?.identifier.text ?? "") + } + + guard expectedArguments.count == actualArguments.count else { + return + } + + for (lhs, rhs) in zip(expectedArguments, actualArguments) where lhs != rhs { + return + } + + self.violations.append(node.positionAfterSkippingLeadingTrivia) + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRuleExamples.swift new file mode 100644 index 00000000000..180cdac1986 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRuleExamples.swift @@ -0,0 +1,156 @@ +extension UnneededOverrideRule { + static let nonTriggeringExamples = [ + Example(""" +class Foo { + override func bar() { + super.bar() + print("hi") + } +} +"""), + Example(""" +class Foo { + @available(*, unavailable) + override func bar() { + super.bar() + } +} +"""), + Example(""" +class Foo { + override func bar() { + super.bar() + super.bar() + } +} +"""), + Example(""" +class Foo { + override func bar() throws { + // Doing a different variation of 'try' changes behavior + try! super.bar() + } +} +"""), + Example(""" +class Foo { + override func bar() throws { + // Doing a different variation of 'try' changes behavior + try? super.bar() + } +} +"""), + Example(""" +class Foo { + override func bar() async throws { + // Doing a different variation of 'try' changes behavior + await try! super.bar() + } +} +"""), + Example(""" +class Foo { + override func bar(arg: Bool) { + // Flipping the argument changes behavior + super.bar(arg: !arg) + } +} +"""), + Example(""" +class Foo { + override func bar(_ arg: Int) { + // Changing the argument changes behavior + super.bar(arg + 1) + } +} +"""), + Example(""" +class Foo { + override func bar(_ arg: Int) { + // Not passing arguments because they have default values changes behavior + super.bar() + } +} +"""), + Example(""" +class Foo { + override func bar(arg: Int, _ arg3: Bool) { + // Calling a super function with different argument labels changes behavior + super.bar(arg2: arg, arg3: arg3) + } +} +"""), + Example(""" +class Foo { + override func bar(animated: Bool, completion: () -> Void) { + super.bar(animated: animated) { + // This likely changes behavior + } + } +} +"""), + Example(""" +class Foo { + override func bar(animated: Bool, completion: () -> Void) { + super.bar(animated: animated, completion: { + // This likely changes behavior + }) + } +} +"""), + ] + + static let triggeringExamples = [ + Example(""" +class Foo { + override func bar() { + super.bar() + } +} +"""), + Example(""" +class Foo { + override func bar() { + return super.bar() + } +} +"""), + Example(""" +class Foo { + override func bar() { + super.bar() + // comments don't affect this + } +} +"""), + Example(""" +class Foo { + override func bar() async { + await super.bar() + } +} +"""), + Example(""" +class Foo { + override func bar() throws { + try super.bar() + // comments don't affect this + } +} +"""), + Example(""" +class Foo { + override func bar(arg: Bool) throws { + try super.bar(arg: arg) + } +} +"""), + Example(""" +class Foo { + override func bar(animated: Bool, completion: () -> Void) { + super.bar(animated: animated, completion: completion) + } +} +"""), + ] +} diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index f2b1e147c7f..ad2053a90e8 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -1232,6 +1232,12 @@ class UnneededBreakInSwitchRuleGeneratedTests: SwiftLintTestCase { } } +class UnneededOverrideRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnneededOverrideRule.description) + } +} + class UnneededParenthesesInClosureArgumentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnneededParenthesesInClosureArgumentRule.description)