Missing warning or error when messing up initializer inheritance?

So I managed to instantiate an object of a subclass that had a property (in this case storing a closure) not initialized. At least I believe so, thus I would appreciate somebody taking a look at this and comment. I had the following class (abbreviated here):

class TaskFile {
    private static let ephemeralUrl = URL(fileURLWithPath: "/dev/null")

    private(set) var location: URL
    private var contents: Contents // a simple struct holding some Strings

    private init(parsingFrom url: URL) throws {
        location = url
        contents = try Contents.loadFromPackage(packageUrl: url)
    }

    convenience init(withUrl: URL) throws {
        guard url.isFileURL else { throw TaskFileError.locationIsNotFileURL }
        if url.isFileUrlToExistingFile, url != Self.ephemeralUrl {
            try self.init(parsingFrom: url)
        } else {
            let title = url.deletingPathExtension().lastPathComponent
            self.init(withNewContentsToStoreAt: url, title: title)
            try save()
        }
    }

    private init(withNewContentsToStoreAt url: URL, title: String) {
        location = url
        contents = Contents(title: title)
    }

    static func ephemeralTaskFile() -> TaskFile {
        return TaskFile(withNewContentsToStoreAt: ephemeralUrl, title: "New Task")
    }

    func save() throws {
        // just some encoding of contents and writing to location
    }

The "ephemeral TaskFile" was just needed as basically an initial value in some places of my project and I thought I was clever by hiding the init it needs to prevent TaskFiles being created that had such "bad" URLs (the URLs for instances that are created based on user input would definitely be safe).

Generally the class worked very well so far, but then I came across a unit test for another class where I would need a mock/spy TaskFile. So I did what I always do:

class SpyTaskFile: TaskFile {
    var didSaveContents: () -> Void = { }
    override func save() throws {
        try super.save()
        didSaveContents()
    }
}

To my surprise, already existing tests that now (also) used this spy would give me a crash (bad mem access) when creating an instance, right when it was save()ed, at the didSaveContents() call. Confusingly, the property was set to 0x0, even though I had provided it a harmless noop as default value.
To further investigate I removed the default value and wrote my own init for SpyTaskFile and there I saw the issue: The compiler obviously wants me to call a designated init of the superclass, but I had set them all to private. One facepalm later I fixed that by removing the first private init and changed the convenience initializer to a designated one:

    init(withUrl url: URL) throws {
        guard url.isFileURL else { throw TaskFileError.locationIsNotFileURL }
        if url.isFileUrlToExistingFile, url != Self.ephemeralUrl {
            location = url
            contents = try Contents.loadFromPackage(packageUrl: url)
        } else {
            let title = url.deletingPathExtension().lastPathComponent
            location = url
            contents = Contents(title: title)
            try save()
        }
    }

This works as expected as I was now able to call the new designated initializer from my subclass initializer, where I can set the didSaveContents property. In fact, I don't even need to spell it out anymore, providing a default value works. The only drawback was the few lines of duplicated code in the if statement.
Oh, I should also mention that weirdly enough, if I made the didSaveContents property optional, it does get properly defaulted to nil, even with the bad private/convenience design.

I'm not sure whether it's a compiler bug (i.e. even with private designated initializer the property added in the subclass should properly init via default value) or just an "oversight" that would warrant a warning/error message only?

1 Like

Yup, this is a bug, discussed here:

2 Likes

It is basically this code, mentioned in the discussion

class A {
  private init() {
  }
  convenience init(x: Int) {
    self.init()
  }
}

class B: A {
  let y: Int = 42
}

let z = B(x: 37)
print(z.y)
% swiftc -g -o some -sanitize=address some.swift
% ./some
=================================================================
==60321==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x000103c006e0 at pc 0x000100f56a5c bp 0x00016eeab5e0 sp 0x00016eeab5d8
READ of size 8 at 0x000103c006e0 thread T0
    #0 0x100f56a58 in main some.swift:14
    #1 0x18760a0dc  (<unknown module>)

0x000103c006e0 is located 0 bytes after 16-byte region [0x000103c006d0,0x000103c006e0)
allocated by thread T0 here:
    #0 0x1017ff124 in wrap_malloc+0x94 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x53124)
    #1 0x1979f54c8 in swift_slowAlloc+0x20 (libswiftCore.dylib:arm64e+0x3aa4c8)
    #2 0x1979f57b4 in swift_allocObject+0x3c (libswiftCore.dylib:arm64e+0x3aa7b4)
    #3 0x100f56fec in A.__allocating_init() some.swift
    #4 0x100f56cd4 in A.__allocating_init(x:) some.swift:5
    #5 0x100f569fc in main some.swift:13
    #6 0x18760a0dc  (<unknown module>)
1 Like

Thank you, @xwu and @paiv, that confirms my understanding of the issue. :smiley: