Skip to content

Commit be453cf

Browse files
authored
[cxx-interop] Make imported enum members inherit the access of their parent (#84522)
This patch relaxes the access check for members of imported inherited nested enums. Without it, those members become impossible to inherit, which largely defeats the purpose of importing them in the first place. rdar://160803362
1 parent cb4cb70 commit be453cf

6 files changed

+304
-20
lines changed

lib/AST/DeclContext.cpp

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,14 +1499,32 @@ bool AccessScope::allowsPrivateAccess(const DeclContext *useDC, const DeclContex
14991499
auto clangDecl = sourceNTD->getDecl()->getClangDecl();
15001500

15011501
if (isa_and_nonnull<clang::EnumDecl>(clangDecl)) {
1502-
// Consider: class C { private: enum class E { M }; };
1503-
// If sourceDC is a C++ enum (e.g, E), then we are looking at one of its
1504-
// members (e.g., E.M). If this is the case, then we should consider
1505-
// the SWIFT_PRIVATE_FILEID of its enclosing class decl (e.g., C), if any.
1506-
// Doing so allows the nested private enum's members to inherit the access
1507-
// of the nested enum type itself.
1508-
clangDecl = dyn_cast<clang::CXXRecordDecl>(clangDecl->getDeclContext());
1509-
sourceDC = sourceNTD->getDeclContext();
1502+
// C/C++ enums are an odd case for access checking because they can only
1503+
// contain variants as members, and those variants cannot be assigned
1504+
// access specifiers. Instead, those variants should simply inherit the
1505+
// access of their parent enum, if any. For instance:
1506+
//
1507+
// class Base { protected: enum class E { M }; };
1508+
// class Derived : public Base {};
1509+
//
1510+
// In C++, E::M should be accessible within Base and Derived; we should
1511+
// follow suit in Swift.
1512+
//
1513+
// When we check the access of something like E.M, clangDecl will point to
1514+
// enum class E (the DeclContext of M), and if we've gotten this far, we
1515+
// can infer that E was accessible in useDC, so its members should be too.
1516+
//
1517+
// This is technically unsound (i.e., encapsulation-breaking), because it
1518+
// is possible to extend imported Clang enums private in Swift extensions.
1519+
// With this hack, those private members would be accessible everywhere
1520+
// even though they shouldn't. But right here, when we're checking the E
1521+
// of E.M, there's no way to tell whether the M is something we imported
1522+
// from Clang (which we should allow) versus something we defined in Swift
1523+
// (which we should disallow), without over-complicating the access check.
1524+
// To limit the encapsulate the breakage, we do an additional check to
1525+
// ensure we're checking an imported enum that is *nested* in a Clang
1526+
// record (which is the only way this enum can be imported as private).
1527+
return isa_and_nonnull<clang::CXXRecordDecl>(clangDecl->getDeclContext());
15101528
}
15111529

15121530
if (!isa_and_nonnull<clang::CXXRecordDecl>(clangDecl))

test/Interop/Cxx/class/access/access-inversion-executable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ AccessInversionTestSuite.test("usePrivateRec") {
2828
}
2929

3030
AccessInversionTestSuite.test("usePrivateEnum") {
31-
let e = Leaky.AliasToPrivateEnum(rawValue: 0)!
31+
let e = Leaky.AliasToPrivateEnum(rawValue: 0)
3232
expectEqual(e.rawValue, 0)
3333
}
3434

test/Interop/Cxx/class/access/access-inversion-typechecker.swift

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,10 @@ func usePrivateEnum(a: inout Leaky.AliasToPrivateEnum) -> Leaky.AliasToPrivateEn
208208
// Constructing and reading PrivateEnum
209209
//
210210

211-
// TODO: nested enum members are not being imported (#54905)
212-
// let _ = Leaky.privateEnumMember
213-
let rv0 = Leaky.AliasToPrivateEnum(rawValue: 0)!
214-
let _ = Leaky.PrivateEnum(rawValue: 0)!
211+
let _ = Leaky.privateEnumMember // FIXME: nested enum members are not being imported (#54905)
212+
// expected-error@-1 {{type 'Leaky' has no member 'privateEnumMember'}}
213+
let rv0 = Leaky.AliasToPrivateEnum(rawValue: 0)
214+
let _ = Leaky.PrivateEnum(rawValue: 0)
215215
// expected-error@-1 {{'PrivateEnum' is inaccessible due to 'private' protection level}}
216216

217217
let _ = rv0.rawValue
@@ -276,16 +276,13 @@ func usePrivateEnumClass(a: inout Leaky.AliasToPrivateEnumClass) -> Leaky.AliasT
276276
// Constructing and reading PrivateEnumClass
277277
//
278278

279-
// NOTE: private enum class members are not accessible even if we can access
280-
// instances of the private enum class via
279+
// NOTE: private enum class members are accessible if we can access
280+
// their parent enum class decl
281281

282282
let _ = Leaky.AliasToPrivateEnumClass.privateEnumClassMember
283-
// expected-error@-1 {{'privateEnumClassMember' is inaccessible due to 'private' protection level}}
284283
let _ = Leaky.PrivateEnumClass.privateEnumClassMember
285284
// expected-error@-1 {{'PrivateEnumClass' is inaccessible due to 'private' protection level}}
286-
let _: Leaky.AliasToPrivateEnum = .privateEnumClassMember
287-
// expected-error@-1 {{type 'Leaky.AliasToPrivateEnum' (aka 'Leaky.PrivateEnum') has no member 'privateEnumClassMember'}}
288-
// TODO: ^this is not really the right error message
285+
let _: Leaky.AliasToPrivateEnumClass = .privateEnumClassMember
289286

290287
let rv0 = Leaky.AliasToPrivateEnumClass(rawValue: 0)!
291288
let _ = Leaky.PrivateEnumClass(rawValue: 0)!
@@ -298,7 +295,6 @@ func usePrivateEnumClass(a: inout Leaky.AliasToPrivateEnumClass) -> Leaky.AliasT
298295

299296
switch rv0 {
300297
case .privateEnumClassMember:
301-
// expected-error@-1 {{'privateEnumClassMember' is inaccessible due to 'private' protection level}}
302298
doSomething()
303299
default:
304300
doSomething()
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// RUN: split-file %s %t
2+
3+
// RUN: %target-build-swift -module-name main %t/base.swift -I %t/Inputs -o %t/base -Xfrontend -cxx-interoperability-mode=default -enable-experimental-feature ImportNonPublicCxxMembers
4+
// RUN: %target-codesign %t/base
5+
// RUN: %target-run %t/base
6+
7+
// RUN: %target-build-swift -module-name main %t/not-base.swift -I %t/Inputs -o %t/not-base -Xfrontend -cxx-interoperability-mode=default -enable-experimental-feature ImportNonPublicCxxMembers
8+
// RUN: %target-codesign %t/not-base
9+
// RUN: %target-run %t/not-base
10+
11+
// RUN: %target-build-swift -module-name main %t/derived.swift -I %t/Inputs -o %t/derived -Xfrontend -cxx-interoperability-mode=default -enable-experimental-feature ImportNonPublicCxxMembers
12+
// RUN: %target-codesign %t/derived
13+
// RUN: %target-run %t/derived
14+
15+
// RUN: %target-build-swift -module-name main %t/not-derived.swift -I %t/Inputs -o %t/not-derived -Xfrontend -cxx-interoperability-mode=default -enable-experimental-feature ImportNonPublicCxxMembers
16+
// RUN: %target-codesign %t/not-derived
17+
// RUN: %target-run %t/not-derived
18+
19+
// REQUIRES: executable_test
20+
// REQUIRES: swift_feature_ImportNonPublicCxxMembers
21+
22+
//--- Inputs/module.modulemap
23+
module CxxModule {
24+
requires cplusplus
25+
header "cxx-header.h"
26+
}
27+
28+
//--- Inputs/cxx-header.h
29+
#pragma once
30+
31+
class __attribute__((__swift_attr__("private_fileid:main/base.swift")))
32+
Base {
33+
protected:
34+
enum class Enum { Foo, Bar };
35+
public:
36+
Enum makeEnum(void) const { return Enum::Bar; }
37+
};
38+
39+
class __attribute__((__swift_attr__("private_fileid:main/derived.swift")))
40+
Derived : public Base {};
41+
42+
//--- base.swift
43+
import StdlibUnittest
44+
import CxxModule
45+
46+
var Suite = TestSuite("BlessedForBase")
47+
extension Base {
48+
static func makeEnums() {
49+
let e1: Enum = Enum.Bar
50+
let e2: Enum = Enum.Foo
51+
expectEqual(e1, Enum.Bar)
52+
expectNotEqual(e1, e2)
53+
switch e2 {
54+
case .Bar:
55+
expectTrue(false, "foo is bar")
56+
case .Foo:
57+
expectTrue(true, "foo is foo")
58+
}
59+
}
60+
}
61+
Suite.test("Use private nested enum in base class") { Base.makeEnums() }
62+
runAllTests()
63+
64+
//--- not-base.swift
65+
import StdlibUnittest
66+
import CxxModule
67+
68+
var Suite = TestSuite("NotBlessedForBase")
69+
Suite.test("Use private nested enum in base class") {
70+
let b = Base()
71+
let e1 = b.makeEnum()
72+
let e2 = b.makeEnum()
73+
expectEqual(e1, e2)
74+
}
75+
runAllTests()
76+
77+
78+
//--- derived.swift
79+
import StdlibUnittest
80+
import CxxModule
81+
82+
var Suite = TestSuite("BlessedForDerived")
83+
extension Derived {
84+
static func makeEnums() {
85+
let e1: Enum = Enum.Bar
86+
let e2: Enum = Enum.Foo
87+
expectEqual(e1, Enum.Bar)
88+
expectNotEqual(e1, e2)
89+
switch e2 {
90+
case .Bar:
91+
expectTrue(false, "foo is bar")
92+
case .Foo:
93+
expectTrue(true, "foo is foo")
94+
}
95+
}
96+
}
97+
Suite.test("Use private nested enum inherited from base class") { Derived.makeEnums() }
98+
runAllTests()
99+
100+
//--- not-derived.swift
101+
import StdlibUnittest
102+
import CxxModule
103+
104+
var Suite = TestSuite("NotBlessedForDerive")
105+
Suite.test("Use private nested enum inherited from base class") {
106+
let d = Derived()
107+
let e1 = d.makeEnum()
108+
let e2 = d.makeEnum()
109+
expectEqual(e1, e2)
110+
}
111+
runAllTests()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// RUN: split-file %s %t
2+
3+
// RUN: %target-swift-ide-test -print-module -module-to-print=CxxModule -print-access -I %t/Inputs -source-filename=x -enable-experimental-cxx-interop -enable-experimental-feature ImportNonPublicCxxMembers | %FileCheck %s
4+
5+
// REQUIRES: swift_feature_ImportNonPublicCxxMembers
6+
7+
//--- Inputs/module.modulemap
8+
module CxxModule {
9+
requires cplusplus
10+
header "cxx-header.h"
11+
}
12+
13+
//--- Inputs/cxx-header.h
14+
#pragma once
15+
16+
class __attribute__((__swift_attr__("private_fileid:main/base.swift")))
17+
Base {
18+
protected:
19+
enum class Enum { Foo, Bar };
20+
public:
21+
Enum makeEnum(void) const { return Enum::Bar; }
22+
};
23+
24+
class __attribute__((__swift_attr__("private_fileid:main/derived.swift")))
25+
Derived : public Base {};
26+
27+
// CHECK: public struct Base {
28+
// CHECK-NEXT: public init()
29+
// CHECK-NEXT: private enum Enum : [[ENUM_UNDERLYING_TYPE:Int32|UInt32]] {
30+
// CHECK-NEXT: private init?(rawValue: [[ENUM_UNDERLYING_TYPE]])
31+
// CHECK-NEXT: private var rawValue: [[ENUM_UNDERLYING_TYPE]] { get }
32+
// CHECK-NEXT: private typealias RawValue = [[ENUM_UNDERLYING_TYPE]]
33+
// CHECK-NEXT: case Foo
34+
// CHECK-NEXT: case Bar
35+
// CHECK-NEXT: }
36+
// CHECK-NEXT: public func makeEnum() -> Base.Enum
37+
// CHECK-NEXT: }
38+
39+
// CHECK-NEXT: public struct Derived {
40+
// CHECK-NEXT: public init()
41+
// CHECK-NEXT: private typealias Enum = Base.Enum
42+
// CHECK-NEXT: public func makeEnum() -> Base.Enum
43+
// CHECK-NEXT: }
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// RUN: split-file %s %t
2+
3+
// RUN: %target-swift-frontend -typecheck -verify -I %t/Inputs -cxx-interoperability-mode=default -enable-experimental-feature ImportNonPublicCxxMembers -module-name main %t/base.swift
4+
// RUN: %target-swift-frontend -typecheck -verify -I %t/Inputs -cxx-interoperability-mode=default -enable-experimental-feature ImportNonPublicCxxMembers -module-name main %t/derived.swift
5+
6+
// REQUIRES: swift_feature_ImportNonPublicCxxMembers
7+
8+
//--- Inputs/module.modulemap
9+
module CxxModule {
10+
requires cplusplus
11+
header "cxx-header.h"
12+
}
13+
14+
//--- Inputs/cxx-header.h
15+
#pragma once
16+
17+
class __attribute__((__swift_attr__("private_fileid:main/base.swift")))
18+
Base {
19+
protected:
20+
enum class Enum { Foo, Bar };
21+
public:
22+
Enum makeEnum(void) const { return Enum::Bar; }
23+
};
24+
25+
class __attribute__((__swift_attr__("private_fileid:main/derived.swift")))
26+
Derived : public Base {};
27+
28+
//--- base.swift
29+
import CxxModule
30+
31+
extension Base {
32+
private static func inside(e: Enum) {
33+
let _: Enum = Enum.Bar
34+
let _: Base.Enum = Base.Enum.Bar
35+
let _: Enum = Base.Enum.Foo
36+
let _: Base.Enum = Enum.Foo
37+
38+
switch e {
39+
case .Bar: return
40+
case .Foo: return
41+
}
42+
}
43+
}
44+
45+
func switches() {
46+
let b = Base()
47+
let d = Derived()
48+
switch b.makeEnum() {
49+
default: return // this is OK
50+
}
51+
switch d.makeEnum() {
52+
default: return // this is OK
53+
}
54+
switch b.makeEnum() {
55+
case Base.Enum.Bar: return // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
56+
case Base.Enum.Foo: return // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
57+
}
58+
switch b.makeEnum() {
59+
case .Bar: return // OK as long as the user does not refer to Enum itself
60+
case .Foo: return // (NOTE: arguably this should not be allowed)
61+
}
62+
switch d.makeEnum() {
63+
case Derived.Enum.Bar: return // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
64+
case Derived.Enum.Foo: return // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
65+
}
66+
switch d.makeEnum() {
67+
case .Bar: return // OK as long as the user does not refer to Enum itself
68+
case .Foo: return // (NOTE: arguably this should not be allowed)
69+
}
70+
}
71+
72+
func outside() {
73+
let b = Base()
74+
let d = Derived()
75+
let _ = b.makeEnum() // This is OK as long as we don't name the type
76+
let _ = d.makeEnum() // This is OK as long as we don't name the type
77+
78+
let _: Derived.Enum = b.makeEnum() // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
79+
let _: Derived.Enum = d.makeEnum() // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
80+
81+
// The types in the base and derived types should be considered the same type
82+
var x = b.makeEnum()
83+
x = d.makeEnum()
84+
var y = d.makeEnum()
85+
y = b.makeEnum()
86+
}
87+
88+
//--- derived.swift
89+
import CxxModule
90+
91+
extension Base {
92+
private static func inside(e: Enum) { // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
93+
let _: Enum = Enum.Bar // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
94+
// expected-error@-1 {{'Enum' is inaccessible due to 'private' protection level}}
95+
}
96+
}
97+
98+
extension Derived {
99+
private static func inside(e: Enum) {
100+
let b = Base()
101+
let _: Enum = Enum.Bar
102+
let _: Derived.Enum = Derived.Enum.Bar
103+
let _: Enum = b.makeEnum()
104+
105+
// It would be nice to make these work but they do not
106+
let _: Base.Enum = Base.Enum.Bar // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
107+
// expected-error@-1 {{'Enum' is inaccessible due to 'private' protection level}}
108+
let _: Enum = Base.Enum.Foo // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
109+
let _: Base.Enum = Enum.Foo // expected-error {{'Enum' is inaccessible due to 'private' protection level}}
110+
111+
switch e {
112+
case .Bar: return
113+
case .Foo: return
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)