Skip to content

Commit 36b9ddc

Browse files
authored
Merge pull request #1683 from Skoti/feature/access-level-on-imports
Support access level on import statements
2 parents 259bf67 + 3f34c41 commit 36b9ddc

File tree

15 files changed

+293
-122
lines changed

15 files changed

+293
-122
lines changed

Documentation/PLUGIN.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,26 @@ exposed via public API, so even if `ImplementationOnlyImports` is set to `true`,
182182
this will only work if the `Visibility` is set to `internal`.
183183

184184

185+
##### Generation Option: `UseAccessLevelOnImports` - imports preceded by a visibility modifier (`public`, `package`, `internal`)
186+
187+
The default behavior depends on the Swift version the plugin is compiled with.
188+
For Swift versions below 6.0 the default is `false` and the code generator does not precede any imports with a visibility modifier.
189+
You can change this by explicitly setting the `UseAccessLevelOnImports` option:
190+
191+
```
192+
$ protoc --swift_opt=UseAccessLevelOnImports=[value] --swift_out=. foo/bar/*.proto mumble/*.proto
193+
```
194+
195+
The possible values for `UseAccessLevelOnImports` are:
196+
197+
* `false`: Generates plain import directives without a visibility modifier.
198+
* `true`: Imports of internal dependencies and any modules defined in the module
199+
mappings will be preceded by a visibility modifier corresponding to the visibility of the generated types - see `Visibility` option.
200+
201+
**Important:** It is strongly encouraged to use `internal` imports instead of `@_implementationOnly` imports.
202+
Hence `UseAccessLevelOnImports` and `ImplementationOnlyImports` options exclude each other.
203+
204+
185205
### Building your project
186206

187207
After copying the `.pb.swift` files into your project, you will need

PluginExamples/Package.swift

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,22 @@ let package = Package(
77
dependencies: [
88
.package(path: "../")
99
],
10-
targets: [
10+
targets: targets()
11+
)
12+
13+
private func targets() -> [Target] {
14+
var testDependencies: [Target.Dependency] = [
15+
.target(name: "Simple"),
16+
.target(name: "Nested"),
17+
.target(name: "Import"),
18+
]
19+
#if compiler(>=5.9)
20+
testDependencies.append(.target(name: "AccessLevelOnImport"))
21+
#endif
22+
var targets: [Target] = [
1123
.testTarget(
1224
name: "ExampleTests",
13-
dependencies: [
14-
.target(name: "Simple"),
15-
.target(name: "Nested"),
16-
.target(name: "Import"),
17-
]
25+
dependencies: testDependencies
1826
),
1927
.target(
2028
name: "Simple",
@@ -44,4 +52,21 @@ let package = Package(
4452
]
4553
),
4654
]
47-
)
55+
#if compiler(>=5.9)
56+
targets.append(
57+
.target(
58+
name: "AccessLevelOnImport",
59+
dependencies: [
60+
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
61+
],
62+
swiftSettings: [
63+
.enableExperimentalFeature("AccessLevelOnImport"),
64+
],
65+
plugins: [
66+
.plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf")
67+
]
68+
)
69+
)
70+
#endif
71+
return targets
72+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
syntax = "proto3";
2+
3+
import "Dependency/Dependency.proto";
4+
5+
message AccessLevelOnImport {
6+
Dependency dependency = 1;
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
syntax = "proto3";
2+
3+
message Dependency {
4+
string name = 1;
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/// DO NOT DELETE.
2+
///
3+
/// We need to keep this file otherwise the plugin is not running.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"invocations": [
3+
{
4+
"protoFiles": [
5+
"AccessLevelOnImport/AccessLevelOnImport.proto",
6+
"Dependency/Dependency.proto",
7+
],
8+
"visibility": "public",
9+
"useAccessLevelOnImports": true
10+
}
11+
]
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#if compiler(>=5.9)
2+
3+
import AccessLevelOnImport
4+
import XCTest
5+
6+
final class AccessLevelOnImportTests: XCTestCase {
7+
func testAccessLevelOnImport() {
8+
let access = AccessLevelOnImport.with { $0.dependency = .with { $0.name = "Dependency" } }
9+
XCTAssertEqual(access.dependency.name, "Dependency")
10+
}
11+
}
12+
13+
#endif

Plugins/SwiftProtobufPlugin/plugin.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ struct SwiftProtobufPlugin {
8585
var fileNaming: FileNaming?
8686
/// Whether internal imports should be annotated as `@_implementationOnly`.
8787
var implementationOnlyImports: Bool?
88+
/// Whether import statements should be preceded with visibility.
89+
var useAccessLevelOnImports: Bool?
8890
}
8991

9092
/// The path to the `protoc` binary.
@@ -188,6 +190,11 @@ struct SwiftProtobufPlugin {
188190
protocArgs.append("--swift_opt=ImplementationOnlyImports=\(implementationOnlyImports)")
189191
}
190192

193+
// Add the useAccessLevelOnImports only imports flag if it was set
194+
if let useAccessLevelOnImports = invocation.useAccessLevelOnImports {
195+
protocArgs.append("--swift_opt=UseAccessLevelOnImports=\(useAccessLevelOnImports)")
196+
}
197+
191198
var inputFiles = [Path]()
192199
var outputFiles = [Path]()
193200

Sources/SwiftProtobufPluginLibrary/ProtoFileToModuleMappings.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ private let defaultSwiftProtobufModuleName = "SwiftProtobuf"
2020
public struct ProtoFileToModuleMappings {
2121

2222
/// Errors raised from parsing mappings
23-
public enum LoadError: Error {
23+
public enum LoadError: Error, Equatable {
2424
/// Raised if the path wasn't found.
2525
case failToOpen(path: String)
2626
/// Raised if an mapping entry in the protobuf doesn't have a module name.

Sources/protoc-gen-swift/Descriptor+Extensions.swift

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,15 @@ extension FileDescriptor {
4949
// Aside: This could be moved into the plugin library, but it doesn't seem
5050
// like anyone else would need the logic. Swift GRPC support probably stick
5151
// with the support for the module mappings.
52-
public func computeImports(
52+
func computeImports(
5353
namer: SwiftProtobufNamer,
54-
reexportPublicImports: Bool,
55-
asImplementationOnly: Bool
54+
directive: GeneratorOptions.ImportDirective,
55+
reexportPublicImports: Bool
5656
) -> String {
5757
// The namer should be configured with the module this file generated for.
5858
assert(namer.targetModule == (namer.mappings.moduleName(forFile: self) ?? ""))
5959
// Both options can't be enabled.
60-
assert(!reexportPublicImports ||
61-
!asImplementationOnly ||
62-
reexportPublicImports != asImplementationOnly)
60+
assert(!reexportPublicImports || directive != .implementationOnly)
6361

6462
guard namer.mappings.hasMappings else {
6563
// No module mappings? Everything must be the same module, so no Swift
@@ -72,7 +70,7 @@ extension FileDescriptor {
7270
return ""
7371
}
7472

75-
let directive = asImplementationOnly ? "@_implementationOnly import" : "import"
73+
let importSnippet = directive.snippet
7674
var imports = Set<String>()
7775
for dependency in dependencies {
7876
if SwiftProtobufInfo.isBundledProto(file: dependency) {
@@ -86,16 +84,19 @@ extension FileDescriptor {
8684
if let depModule = namer.mappings.moduleName(forFile: dependency),
8785
depModule != namer.targetModule {
8886
// Different module, import it.
89-
imports.insert("\(directive) \(depModule)")
87+
imports.insert("\(importSnippet) \(depModule)")
9088
}
9189
}
9290

9391
// If not re-exporting imports, then there is nothing special needed for
9492
// `import public` files, as any transitive `import public` directives
9593
// would have already re-exported the types, so everything this file needs
9694
// will be covered by the above imports.
97-
let exportingImports: [String] =
98-
reexportPublicImports ? computeSymbolReExports(namer: namer) : [String]()
95+
let exportingImports: [String] = reexportPublicImports
96+
? computeSymbolReExports(
97+
namer: namer,
98+
useAccessLevelOnImports: directive.isAccessLevel)
99+
: [String]()
99100

100101
var result = imports.sorted().joined(separator: "\n")
101102
if !exportingImports.isEmpty {
@@ -109,7 +110,7 @@ extension FileDescriptor {
109110
}
110111

111112
// Internal helper to `computeImports(...)`.
112-
private func computeSymbolReExports(namer: SwiftProtobufNamer) -> [String] {
113+
private func computeSymbolReExports(namer: SwiftProtobufNamer, useAccessLevelOnImports: Bool) -> [String] {
113114
var result = [String]()
114115

115116
// To handle re-exporting, recursively walk all the `import public` files
@@ -119,6 +120,7 @@ extension FileDescriptor {
119120
// authored code.
120121
var toScan = publicDependencies
121122
var visited = Set<String>()
123+
let exportedImportDirective = "@_exported\(useAccessLevelOnImports ? " public" : "") import"
122124
while let dependency = toScan.popLast() {
123125
let dependencyName = dependency.name
124126
if visited.contains(dependencyName) { continue }
@@ -144,16 +146,16 @@ extension FileDescriptor {
144146
// chained imports.
145147

146148
for m in dependency.messages {
147-
result.append("@_exported import struct \(namer.fullName(message: m))")
149+
result.append("\(exportedImportDirective) struct \(namer.fullName(message: m))")
148150
}
149151
for e in dependency.enums {
150-
result.append("@_exported import enum \(namer.fullName(enum: e))")
152+
result.append("\(exportedImportDirective) enum \(namer.fullName(enum: e))")
151153
}
152154
// There is nothing we can do for the Swift extensions declared on the
153155
// extended Messages, best we can do is expose the raw extensions
154156
// themselves.
155157
for e in dependency.extensions {
156-
result.append("@_exported import let \(namer.fullName(extensionField: e))")
158+
result.append("\(exportedImportDirective) let \(namer.fullName(extensionField: e))")
157159
}
158160
}
159161
return result

0 commit comments

Comments
 (0)