Introduction
It’d be nice to have compiler warnings and errors for redundant access control modifiers when a member's declared access level exceeds its enclosing type's effective access level. In Swift's access control model, a member cannot be more accessible than its enclosing type, making such declarations misleading and potentially confusing. This proposal aims to improve code clarity and help developers better understand the actual accessibility of their APIs.
Motivation
Swift's access control system follows a fundamental rule: no entity can be more accessible than its enclosing context. For example, a public
property of an internal
struct is effectively internal
, regardless of the explicit public
modifier. However, the compiler currently accepts such declarations without any warnings, leading to several problems:
1. Misleading Code and False Expectations
Consider this example:
struct User: Decodable {
struct Name: Decodable {
let title: String
let first: String
let last: String
}
public let name: Name
let email: String
public func foo() {
print("Hello World!")
}
}
Here, the User
struct has the default internal
access level. The public
modifiers on name
and foo()
suggest these members are publicly accessible, but they are effectively internal
. This creates false expectations for developers reading or maintaining the code.
2. Code Review Confusion
When reviewing pull requests, team members might assume that marking a member as public
makes it part of the public API. They may spend time discussing API design implications for members that aren't actually publicly accessible.
3. Maintenance Burden
As codebases evolve, developers might change an outer type's access level without realizing that inner members have redundant modifiers. This creates visual noise and makes it harder to understand the true access boundaries of a module.
4. Learning Curve for New Swift Developers
Newcomers to Swift might not understand why their public
member isn't accessible from another module, leading to confusion about how access control works. A compiler diagnostic would provide immediate feedback and education about Swift's access control rules.
5. API Evolution Pitfalls
When developers later decide to make an internal type public, they might overlook members that already have public
modifiers, assuming they're already correctly configured. This can lead to unintended API surface exposure.
Currently, developers must rely on manual code review, third-party linters, or trial-and-error to catch these issues. The compiler has all the information needed to detect these redundant modifiers but provides no feedback.
Proposed Solution
It’d nice to add compiler diagnostics when a member's explicitly declared access level exceeds its enclosing type's effective access level. The diagnostic should:
1. Emit a Warning (Initially)
For Swift 6.x compatibility, start with a warning:
internal struct User {
public let name: Name // Warning: 'public' modifier is redundant;
// 'name' is effectively 'internal' because
// its enclosing type 'User' is 'internal'
public func foo() {} // Warning: 'public' modifier is redundant;
// 'foo()' is effectively 'internal' because
// its enclosing type 'User' is 'internal'
}
2. Provide Fix-It Suggestions
The compiler should offer automatic fix-its:
- Option 1: Remove the redundant modifier (most common case)
- Option 2: Suggest making the enclosing type match the intended access level
3. Handle Nested Types
The diagnostic should work correctly with nested types at any depth:
internal class Outer {
public class Middle { // Warning: redundant 'public'
public func bar() {} // Warning: redundant 'public' (limited by Outer)
}
}
4. Consider File-Private and Private Edge Cases
The diagnostic should be intelligent about fileprivate
and private
:
fileprivate struct Config {
internal let value: String // Warning: redundant 'internal'
public let name: String // Warning: redundant 'public'
}
5. Future Path to Error
In a future Swift version (e.g., Swift 7), this could become a compile-time error, similar to how other redundant or contradictory declarations are handled.
Example of Improved Code:
Before (with redundant modifiers):
struct User: Decodable {
public let name: Name
public func foo() {
print("Hello World!")
}
}
After (clean and accurate):
struct User: Decodable {
let name: Name
func foo() {
print("Hello World!")
}
}
Or, if public access was truly intended:
public struct User: Decodable {
public let name: Name
public func foo() {
print("Hello World!")
}
}
This change would make Swift's access control more transparent, improve code quality, and provide better learning experiences for developers at all skill levels.