Calling private methods from extensions in separate files

I think the alternative would be something like what other languages call protected: visible via limited interactions with the type. This could include subclasses, which is the typical usage, but could also include extensions in other files. So the property wouldn't be visible to consumers of the type, even internally, but would be to any subtype or extension. Personally, I would find this very useful to maximize the cleanliness of projects. Currently I'm forced to either allow things to be inappropriately visible internally or put everything in one file, including rather large subclasses.

4 Likes

For this particular aspect the difference between fileprivate and private could have been used..

1 Like

I understand there are different approaches that different people might want to promote, but the key issue is that the only way for the implementer of a type to keep control of its private details (while spreading those details across multiple implementation files) is some scheme that requires the implementer to grant access to the additional files.

So, protected could be problematic if anyone could subclass the base class in order get access to its private details.

I don't see exactly how fileprivate helps that much. Maybe something like private("additionalFileName.swift") could be a way of granting access.

This is basically a security issue.

Since the only way to access something that is internal is to be compiled into the same module (@testable import notwithstanding, and that requires that the module being imported opt-in to testable imports), why wouldn't you trust those internal clients?

You mention "security issue" below, but if there's a major risk from declarations being too visible within the same module, i.e. if a component can't trust the code given to the same compiler invocation, isn't that illustrative of a much more significant breakdown in the code review or build process? And if it really is that sensitive, then maybe it should be isolated in its own module?

The recurring desire to finely audit access control of the code within one's own module strikes me more as adversarial toward extremely uncommon hypothetical scenarios rather than serving a practical purpose for everyday Swift usage.

7 Likes

It's not really about trust or security in the security sense. It really does happen, though, that other people not familiar with the design of your class might add some kind of accessor to derive something from a private property, for example — and they get it wrong, or introduce a cross-type dependency you don't know about.

My feeling exactly. I'd love to have a lightweight module system, such as hierarchical sub-modules defined within Swift itself, that provide boundaries around which inadvertent misuse of private details can be prevented.

IME, Swift's access control* is such a delight to use that the old type-based system feels ancient no matter what languages I switched to. I can't quite put it into words, but Becca's word strongly resonate with me:

* No, not you private. You're stealing fileprivate's place again... :expressionless:


Digression aside, I'm not sure I ever have a problem with one part of the project accessing other parts because of them being internal. I do get somewhat paranoid in the beginning, but nothing ever comes out of it, even as the projects grow in size.

Though all these talks about security and trust make me think I might be missing out on some fun people are having. I mean, access control is such an easy thing to bypass that they're usually more about convenience than concerns.

7 Likes

In my experience, internal or public are almost always the right access modifiers. Most Swift projects I've seen use these two modifiers extensively. The ability to access one type's properties and methods from within another type has never become an issue for me.

I only use fileprivate or private if there's absolutely no reason to access a property or method outside of the type/file (i.e. @State properties in SwiftUI).

I’ve found that local Swift packages are a good way to organize modules hierarchically.

2 Likes

A different non-file based approach could be a more stricter form of extension that is being granted.

struct Thing {
    vetted extensions Foo, Bar // preliminary new syntax
    private var x = 0
    private func y() {}
}

// same file as Thing
extension Thing { // normal extension
    func yy() {
        x = 1 // Error, x is private
    }
}

// same or different file
vetted extension Thing(Foo) { // preliminary new syntax
    named extensions Baz // preliminary new syntax
    private var w = 1 // variables allowed
    private func z() {
        x = 1 // ok
    }
}

// same or different file
vetted extension Thing(Baz) { // preliminary new syntax
    private func t() {
        x = 1 // ok
        w = 1 // ok
    }
}

// same or different file
vetted extension Thing(AttemtToBreachSecurity) { // Error, not allowed
}

real-life analogy would be: not everyone can extend my house, only those whom I granted that permission. And if the vetted builder wants so they can hire a subcontractor.

The added benefit is an ability to add new variables in these vetted extensions.

The problem with this, and any other attempt to allow scoped multifile access to a member, is that anyone can just add a public version of the member in one of your allowed extensions. It's no more "secure" than the internal version, but half as convenient.

struct Thing {
    vetted extensions Foo, Bar // preliminary new syntax
    private var x = 0
    private func y() {}
}

// same file as Thing
extension Thing { // normal extension
    mutating func yy() {
        x = 1 // Error, x is private
    }
}

// same or different file
vetted extension Thing(Foo) { // preliminary new syntax
    named extensions Baz // preliminary new syntax
    private var w = 1 // variables allowed
    private mutating func z() {
        x = 1 // ok
    }
    // This is going to be legal no matter how you attempt to specify
    // who can do what.
    public var pubX: Int {
        get { x }
        set { x = newValue }
    }
}

Any added safety by using private is completely illusory if you have any method of accessing a private member from another file.

And the only security you get from single file private is that you only have to vet one file to make sure no one is exposing things they shouldn't be, instead of every other file in the project.

4 Likes

I hope there is only a small minority of programmers who actually need such kind of "safety" — that sounds more like fighting colleagues working in the same codebase than productive work.
Still, even when it is futile to say "you can't call that method here", there is merit in being able to express that you probably should not use some function you could access.
For example, take set of routines which handle updates in UIKit: There are methods which you should never call, but because you need the ability to override, it is impossible to make them internal.

1 Like

I can see your point about (the lack of) security. The natural conclusion of that reasoning is that as we realistically can only set user's permissions on a file basis (e.g. in a git repository, etc) - file shall be the unit of security. Got you.

(Still, an ability to add variables to your types scattered across different files would be beneficial (for some of us). Currently I can scatter extension methods across files, but variables need to go to the single "main" file - that's not scaleable. I admit this is a totally different issue, so I want to dismiss this particular angle from this thread, it belongs elsewhere.)

If protected were visible only to a type's extensions, subclasses, and conformances within the module (i.e. like internal) then would this alleviate the security issues while allowing classes to be broken up over files?

If this happens frequently (does it?) it deserves a special treatment in the language (similar to how the opposite case is handled - via final)

There's no practical difference between that and using internal. Any one of those same module extensions can expose any protected member publicly.

Come on, there is an obvious practical difference, internal makes the member accessible directly from any other type, protected makes it only available from extensions of the original type.

There's definitely a difference (duh), but I'm not sure if such difference is practical. I'd rather see folder-level access control than a type-based one. Though I'm not sure how folder-level access would work with the current compilation model.

4 Likes

I can reasonably easy bypass my microwave protection and run it with its door open. Does it mean microwaves should not have door protection? :)

Just don't treat private/protected access levels to be bulletproof. By means of another analogy they are more like a locked door made of glass - won't stop a motivated person but will stop 99% opportunity thieves. wide open door - public access. closed unlocked door - protected access. closed locked door - private access.

1 Like

What unmotivated person would need these kinds of protections in the first place? I don't believe they exist. I'll go further, they categorically do not exist. If they're writing code internal to your module, then they're writing code internal to your module. Either you trust them to extend your types correctly, or you don't. If you don't trust them to have full access to a type outside of an extension then you also should not trust them inside of an extension. This whole idea is a solution in search of a problem.

1 Like

A typical use case is a private method that's only made internal to be callable from different files to keep individual file sizes manageable. This private method is not intended to be used other than in certain other methods. This method is not meant to be used even my me, the author, and naturally a few months from now I will forget about it, or disregard my own past guideline and call it anyway because it is trivial. Or someone else on the team does, which is even more probable. private/internal levels would raise the bar, make it a bit more complex to misuse and help preventing a foot-gun. The doc comment would be a poor man unchecked solution to this problem. I believe the problem is real, otherwise we would not see this issue raised now and again with the frequency it appears.

3 Likes

Well, a good reason to split your codebase into modules. Once you do that, using internal will seem natural.

1 Like