Super call enforcement

Hello Swift evolution community,

This is the first draft of my proposal "Super call enforcement". I look forward to hearing your thoughts.

Thank you.


Super call enforcement

Introduction

Introduce a new attribute to enforce that an overridden function should call the super method in its body.

Swift-evolution thread: Super call enforcement

Motivation

It is quite common to override a method in order to add additional functionality to it, rather than to completely replace it. For example - one might override UIViewController.viewDidLoad() to add additional setup code to it:

class MyViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    // Setup some views
  }
}

On top of that, the superclass method may also sometimes require a call to itself in an override for the overall functionality to work correctly.

At present, there is no way to communiate this need other than adding it to the documentation and even experienced developers sometimes overlook this small detail and later run into various issues at runtime. In Objective-C, one can annotate a superclass method with the objc_requires_super attribute and the compiler emits a warning when an overridden method is missing a call to the super method in its body.

However, there is no such attribute in Swift. There are some sub-optimal solutions to this problem, for example:

class C1 {
  final func foo() {
    bar()
  }
  func bar() {}
}

class C2: C1 {
  override func bar() {
    // It's okay to elide the super.bar() because 'foo()' calls 'bar()'
  }
}

However, this has a couple of problems:

  1. It doesn't work when you have a subclass of C2.
  2. If your class is public, then you now have an additional API method in your interface that you have to document as something that no one should ever call directly.
  3. The user of the API method loses control over when super is called.

Proposed solution

Introduce a new @requiresSuper attribute to control super calls in overridden methods, which will be equivalent to the objc_requires_super attribute in Clang.

When a @requiresSuper attribute is present on a method, any overrides of that method should call the super method in their body. If the super call is missing, the compiler will emit an error. The error can be suppressed by inserting the super call and the compiler places no restrictions on where or how many times the call appears.

class C1 {
  @requiresSuper 
  func foo() {}
}

class C2: C1 {
  override func foo() {} // error: method override is missing 'super.foo()' call
}

class C3: C1 {
  override func foo() { // Okay
    super.foo()
  }
}

An optional message can also be specified on the @requiresSuper attribute to provide any additional information, which will be shown with the error message:

class C1 {
  @requiresSuper("Call super as the final step in your implementation")
  func foo() {}
}

class C2: C1 {
  override func foo() {} // error: method override is missing 'super.foo()' call: Call super as the final step in your implementation
}

Any overrides of a method annotated with @requiresSuper will also implicitly inherit the attribute, to make sure that all the overrides in a subclass chain call back to the super method. This will be suppressed if the override is marked with @ignoresSuper or if the override is in a final class:

class C1 {
  @requiresSuper
  func foo() {}
}

class C2: C1 {
  // Implicitly inherits '@requiresSuper' from 'C1.foo'
  override func foo() {
    super.foo()
    // Do some other stuff
  }
}

class C3: C2 {
  override func foo() {} // error: method override is missing 'super.foo()' call
}

class C4: C2 {
  @ignoresSuper
  override func foo() {} // Okay
}

In some scenarios, the lack of super call could be intentional. So, a new @ignoresSuper attribute is also introduced. This will provide an alternate way to suppress the error if necessary:

class C1 {
  @requiresSuper 
  func foo() {}
}

class C3: C1 {
  @ignoresSuper
  override func foo() {} // Also okay 
}

Finally, a @requiresSuper attribute will also be implicitly added for imported Clang decls which have been annotated with objc_requires_super. This enables errors for any Objective-C method with such an attribute, such as UIViewController.viewDidLoad():

import UIKit

class MyViewController1: UIViewController {
  override func viewDidLoad() { } // error: method override is missing 'super.viewDidLoad()' call
}

class MyViewController2: UIViewController {
  override func viewDidLoad() { super.viewDidLoad() } // Okay
}

class MyViewController3: UIViewController {
  @ignoresSuper 
  override func viewDidLoad() { } // Also okay
}

Source compatibility

This is an additive change, so it does not break any existing Swift code in general. However, there is a possibility of breaking existing Swift code if a method which overrides an ObjC method annotated with objc_requires_super skips the super call in its body. This would likely be quite rare in practice though, because any such code would likely not behave correctly without calling super anyway.

Effect on ABI stability

This change has no effect on the ABI.

Effect on API resilience

Adding @requiresSuper to any existing API method can break code if an override of that method does not already call super in its body. Removing the attribute does not have any effect. Adding or removing @ignoresSuper also has no effect, because it only applies to the API method itself and not any overrides of it.

Alternatives considered

  • Do not diagnose missing 'super' calls.
  • Diagnose missing 'super' calls but also enforce its placement: The ordering of a 'super' call (first, middle, last, etc) is not syntactical requirement. Adding such a requirement will complicate API design and usage and will also not be possible to prove statically in some scenarios. For simplicitly, it would be better to only dictate that a superclass method is called at some point, rather than also dictating its order and how many times its called.

Future Directions

We could also make the super call check the default and get rid of the @requiresSuper attribute. This would be source-breaking in practice, but can be mitigated by downgrading the error to a warning in a compatibility mode.

20 Likes

What's the reason this is part of "Future Directions" and not part of this proposal? :sweat_smile: Implementation complexity? Majority of cases in practice (from ObjC) don't end up requiring flow-sensitive analysis? Something else?

2 Likes

I felt a syntactic check is more than enough, but found the idea interesting so kept it in future directions. I don’t know if it’s something that should necessarily be done though (either now or in future). And yeah, I think implementation-wise it might be a bit more complicated as well :-)

1 Like

I totally agree this is a frequent issue, however, I'm not sure the proposed approach is the best one.

I feel like the @requiresSuper annotation wouldn't bring much benefit – if you'd like to make sure super is called, you could make the method final and call a different non-final method in the super implementation like this:

// Module A
open class A {
    final func foo() {
        // something important here that must be executed
        additionalFoo()
    }
    func additionalFoo() {}
}

// Module B
class B : A {
    override func additionalFoo() {  // instead of overriding foo
    }
}

This way your logic in A.foo always gets executed and you don't need to worry about users forgetting a super call.

(It would look even nicer if Swift had protected access level, but that is different topic)

The objc_requires_super situation is a bit different, since you can't declare a final method in Objective-C, so there is no apparent way to prevent the code without a super call from compiling.

As for the @ignoresSuper annotation, this seems to be the default option which doesn't need a specific attribute: when you choose to make a class open and a method non-final, it should be your responsibility as a developer of the base class to make sure it works as expected when an overriding method doesn't call super.

Maybe a SourceKit annotation for some frequent cases of forgotten super like the UIViewController methods, or just anything that inherits from NSObject, would be a better solution?

Adding another 2 new annotations to generate/suppress the contrived warning, just to make sure to call super method in overriding body, it doesn't seem much necessary.

Swift already have lots of attribute/annotation and tons of _hiddenExperiment ones in development, it would be better to tackle this problem through other ways.

-1 for this pitch for now

3 Likes

I’m not sure I understand your objection. Are you just staying there’s already enough warnings and you think there is a higher bar for adding additional warnings than this particular issue reaches?

I think it could work in some cases (such as if you have total control over the source code both as a callee and caller), but for other cases it might be too restrictive. The nice thing about the attribute is that it’s opt-in and does not require any further changes, which I think is a more flexible approach.

An alternative would be to always warn in overrides when the super call is missing, but I wonder if that’s going to create a lot of churn (in scenarios where the missing call is intentional).

I’ve used UIKit for convenience but there are many frameworks that use this (like PSPDFKit) and many Swift developers have previously asked for something like this, so I think making it generally available would be useful.

However, if we really want to limit it, one compromise might be to make @requiresSuper a private attribute so it’s only applied for imported Clang decls, but leave the @ignoresSuper as is because I think there needs to be a way to silence the warning that doesn’t involve adding the super call.

Do we need to provide a way to silence this warning? We don't necessarily offer a way to silence every warning, and misusing an API in a way that the author has explicitly marked as invalid seems like a good case for a warning that can't be silenced.

3 Likes

This resolves the problem in a way that creates a different problem elsewhere. Now you have an extra method in the class's public API that you have to document that no-one should ever call directly. Which is pretty much equivalent to our current situation of documenting that you must call up to super in your override of foo.

3 Likes

IIRC, the only Swift-provided warnings which cannot be removed by a local refactoring are deprecation warnings. There has been several reports of annoyance on this both here and on <bugs.swift.org>.

1 Like

Purely a syntactic thing: in case we decide to go ahead with this, an alternate spelling to consider would be @warn([requiresSuper])/@ignore(warning: [requiresSuper]) or something similar which generalizes in case we want to have similar warnings in the future. I know there are deliberate reasons why you can't turn on/off individual warnings but since this pitch is already going against the grain in that respect, might as well use a more general spelling instead of adding new attributes each time.

This also applies even if the ignore part of the pitch is rejected; it would be good to have a consistent spelling for attribute-based warnings.

That would also make it easier for older compilers to work with newer swiftinterfaces; we can simply have them (ahem) ignore unknown warnings.

3 Likes

This can tend to just push the problem around. If the intention of the “requires super” is to get a critical path completed, that’s fine, but if the intention is to guarantee any intermediary all subclass overrides are also completed, this does nothing. Indeed, you’ve just shifted the problem from the first method into the overridable separate method, which now can fail to call super itself, meaning any intermediary subclasses would fail.

Additionally, there’s no way to stop the “assistant” method from being called by others, which extends the problem further: you’re making a new method a user can call in place of the “main” method, get all the subclasses’ effects while not getting the superclasses, and there’s no way in Access Control in Swift to stop it. There’s no private(call) attribute.

1 Like

There is a significant difference though, consider the example with UIViewController.viewDidLoad():

viewDidLoad() is getting called from inside of the UIKit framework, and UIKit assumes you call super which does something important, and if you don't call super, UIKit might behave unexpectedly because its assumptions were violated.

The approach with an additional method resolves this issue – as a developer of a framework you have the guarantee that the code you're calling is always executed no matter what the client code does. In this case you don't actually need to document it, the compiler ensures the validity of your assumptions.

True, but only if the original method cannot be made private/internal (if it's called from outside of its module). Otherwise the public interface remains the same.

This largely depends on the specifics of the base class, but in general you won't break anything (most importantly, the internal assumptions of the framework) if you call this additional method directly.

There are a couple alternatives that already exist.

SwiftLint already has a rule for this and warns about missing calls to super. I don't know how it knows what overrides require calls to super but it already warns about the common cases in UIKit.

Type ahead. When typing an override in Xcode it inserts the call to super along with the call that you select.

These two existing tools are enough for me.

I've only come across this issue a few times in recent years. In a few cases other developers wrote the bad code and SwiftLint reported it. In Xcode the template files for XCTest Unit tests have calls to setup() and teardown() already included but don't have calls to super. I'm guessing that calls to super are not required but SwiftLint calls them out so I've added them.

I suppose we could also make this an “opt-out” behaviour rather than “opt-in”, but I saw a fair amount of warnings in the standard library with this mode, so I was wondering if the churn is worth it. If so, then we could alternatively make it work like some other attributes like @discardableResult (compiler always emits warning about unused result, but you can silence it by explicitly ignoring the result or by adding the attribute), so in this case the compiler will always warn about missing super call and to silence the warning, you could either explicitly write the super call or add @ignoresSuper.

I think not providing another escape hatch (i.e. the attribute) would be too controversial and/or restrictive.

1 Like

I'd like to see this justified a bit more thoroughly in the proposal text. If I'm an API author and I've marked my method as @requiresSuper, it seems at least moderately surprising that a client is able to just... ignore that annotation at their whim (in fact, I think there's a decent case for making a missing super call to an @requiresSuper method an error rather than a warning).

Otherwise, I think we should maybe use a different spelling for the attribute. "@recommendsSuper" is perhaps somewhat weak, but "requires" seems like a stronger promise is being made than what the compiler actually enforces.

2 Likes

IMHO, there is 2 use cases for marking an method with @requiresSuper.

You have a class that implements a bunch of logic and don't want subclass to mess with it. In such case, I think the API design is just flawed. You can instead make your method final and call an hook for subclass inside it. (this where of course not possible in Obj-C as final concept does not exists here)

The second use case, is for methods that are designed to be overridden (hook without any useful code in the root class, like viewWillLoad, and friends, …), and want to make sure client don't forget to call super, to make sure the call propagate to all parent classes. In such case, the client may own the whole class hierarchy and may want to ignore the super call for legit reason.

1 Like

@discardableResult

The attribute should also specify if the super call comes first or last or anywhere.

Didn't we already had this conversation at least 3 times in the past ?

I mean, it always start with a simple proposal, then we debate about adding more advanced features like super call auto generation, we end up with an never ending debate about if the super call should be at start or at end, can be omitted, etc. and then proposal is bury for 6 months before someone else restart it.

6 Likes