realm / realm/SwiftLint

The `redundant_type_annotation` rule does more harm than good

Open
#3,750 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

discussion
Dominant language
Swift
Stars
19.7k
Forks
2.3k
Avg merge
1d 1h
Merged PRs (30d)
11

Description

New Issue Checklist
Describe the bug

A clear and concise description of what the bug is.

Complete output when running SwiftLint, including the stack trace and command used
$ swiftlint lint
Environment
  • SwiftLint version: 0.45.0
  • Installation method used: brew install swiftlint
  • Paste your configuration file:
opt_in_rules:
  - redundant_type_annotation
  • Are you using nested configurations?
    No.

  • Which Xcode version are you using (check xcodebuild -version)?

    Xcode 12.5.1
    Build version 12E507
    
  • Do you have a sample that shows the issue? Run echo "[string here]" | swiftlint lint --no-cache --use-stdin --enable-all-rules
    to quickly test if your example is really demonstrating the issue. If your example is more
    complex, you can use swiftlint lint --path [file here] --no-cache --enable-all-rules.

struct Foo {}

struct SomeType{
    // This performs a wrong fix:
    public var foo: Foo = Foo()
    // This performs a fix, but really shouldn't:
    public var bar: Bool = true
    public var baz: Int = 42
    public var blee: String = "blee"
}
Expected result
struct Foo {}

struct SomeType {
    public var foo: Foo = .init()
    public var bar: Bool = true
    public var baz: Int = 42
    public var blee: String = "blee"
}
Actual result
struct Foo {}

struct SomeType {
    public var foo = Foo()
    public var bar = true
    public var baz: Int = 42
    public var blee: String = "blee"
}

The "fix" made the code less consistent: Some properties now have an explicit type, others don't. As a reader of that code you now have to parse each line, adapting to explicitly typed vs. implicitly inferred syntaxes on a line-by-line basis. And the selection seems arbitrary, too, given that both, 42 and "blee" could just as well have been type-inferred to Int and String. Not to mention Double, [T], Set<T>, [T: U], …

"But I think it just looks prettier" is never a good reason for picking a less correct/robust approach.

To make things worse initializing properties of a type via type inference (especially for public ones) is generally a bad practice and code smell and should be avoided. (Yes, Apple does it too in their sample code. But Apple's sample code was never been known for being any good anyway. Remember AppDelegate?)

The reason for this is that type-inference in public APIs can very easily lead to API breakage that is very hard to even detect, but can very easily break a user's code.

(Just to be clear: the exact change applied by this rule as seen above is not harmful in itself regarding API breakage, but it makes it needlessly hard/impossible to stay consistent in one's efforts of defensively providing explicit types for all instance properties. So it's not helping either.)

Why inferred property types are to be avoided (in public, if not all APIs):

This right here is an unfortunately all too common anti-pattern:

import ThirdParty

class Foo {
    public var theThing = ThirdParty.createTheThing()

    func useTheThing() {
        self.theThing.doTheThing()
    }
}

What you should be doing instead is

public var theThing: Thing = ThirdParty.createTheThing()

The argument for avoiding type inference on exported properties (apart from introducing cognitive overhead due to users having to now manually look up the value's type, which might require a proper IDE/LSP) is the following:

By inferring the type of one's property from the return type of a function the foreign function's signature implicitly becomes part of one's own API.
This is particularly problematic when assigning public properties from an external library's public function: your public API is now an implicit superset of your own public API and the referenced subset of the external library's public API (which in turn might be making use of type inference and thus be vulnerable to the same breakage).

Let's say ThirdParty was initially (at the time when you added it as a dependency) implemented like this:

public class Thing {
    // ...
    func doTheThing() { … }
}
public func createTheThing() -> Thing { … }

but then at some point changed into this:

public protocol ThingProtocol {
    func doTheThing() { … }
}
public class Thing: ThingProtocol {
    // ...
    func doTheThing() { … }
}
public func createTheThing() -> ThingProtocol { … }

At this point your own code would still work just fine and you wouldn't be suspicious of any breakage.
But your API's users might have needed to explicitly type the value of Foo.theThing:

let thing: Thing = foo.theThing

Their code now doesn't compile anymore, as theThing is now inferred as ThingProtocol.

As such one should strive to always provide explicit types on (public) properties. Even if having redundant_type_annotation remove such explicit types did not actually introduce such a stability vulnerability it would at the very least introduce syntactic inconsistencies and further contribute to the spread of this anti-pattern.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the behavior with the provided Swift sample and swiftlint lint --no-cache --use-stdin --enable-all-rules, focusing on the redundant_type_annotation rule and its autocorrection. Compare the result with the expected output, including whether explicit property types are preserved consistently, and add coverage for the reported cases before confirming the corrected lint behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.