Skip to content

Commit 322f814

Browse files
committed
introduce "UseAccessLevelOnImports" generator option to support access level on import statements
1 parent 259bf67 commit 322f814

File tree

9 files changed

+209
-115
lines changed

9 files changed

+209
-115
lines changed

Documentation/PLUGIN.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,22 @@ 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+
By default, the code generator does not precede any imports with a visibility modifier.
188+
You can change this with the `UseAccessLevelOnImports` option:
189+
190+
```
191+
$ protoc --swift_opt=UseAccessLevelOnImports=[value] --swift_out=. foo/bar/*.proto mumble/*.proto
192+
```
193+
194+
The possible values for `UseAccessLevelOnImports` are:
195+
196+
* `false` (default): Generates plain import directives without a visibility modifier.
197+
* `true`: Imports of internal dependencies and any modules defined in the module
198+
mappings will be preceded by a visibility modifier corresponding to the visibility of the generated types - see `Visibility` option.
199+
200+
185201
### Building your project
186202

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

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

Sources/protoc-gen-swift/Docs.docc/index.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,22 @@ exposed via public API, so even if `ImplementationOnlyImports` is set to `true`,
188188
this will only work if the `Visibility` is set to `internal`.
189189

190190

191+
##### Generation Option: `UseAccessLevelOnImports` - imports preceded by a visibility modifier (`public`, `package`, `internal`)
192+
193+
By default, the code generator does not precede any imports with a visibility modifier.
194+
You can change this with the `UseAccessLevelOnImports` option:
195+
196+
```
197+
$ protoc --swift_opt=UseAccessLevelOnImports=[value] --swift_out=. foo/bar/*.proto mumble/*.proto
198+
```
199+
200+
The possible values for `UseAccessLevelOnImports` are:
201+
202+
* `false` (default): Generates plain import directives without a visibility modifier.
203+
* `true`: Imports of internal dependencies and any modules defined in the module
204+
mappings will be preceded by a visibility modifier corresponding to the visibility of the generated types - see `Visibility` option.
205+
206+
191207
### Building your project
192208

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

Sources/protoc-gen-swift/FileGenerator.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,13 @@ class FileGenerator {
100100
if fileDescriptor.isBundledProto {
101101
p.print("// 'import \(namer.swiftProtobufModuleName)' suppressed, this proto file is meant to be bundled in the runtime.")
102102
} else {
103-
let directive = self.generatorOptions.implementationOnlyImports ? "@_implementationOnly import" : "import"
104-
p.print("\(directive) \(namer.swiftProtobufModuleName)")
103+
p.print("\(generatorOptions.importDirective.snippet) \(namer.swiftProtobufModuleName)")
105104
}
106105

107106
let neededImports = fileDescriptor.computeImports(
108107
namer: namer,
109-
reexportPublicImports: self.generatorOptions.visibility != .internal,
110-
asImplementationOnly: self.generatorOptions.implementationOnlyImports)
108+
directive: generatorOptions.importDirective,
109+
reexportPublicImports: generatorOptions.visibility != .internal)
111110
if !neededImports.isEmpty {
112111
p.print()
113112
p.print(neededImports)

Sources/protoc-gen-swift/GeneratorOptions.swift

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,29 +30,44 @@ class GeneratorOptions {
3030
}
3131
}
3232

33-
enum Visibility {
33+
enum Visibility: String {
3434
case `internal`
3535
case `public`
3636
case `package`
3737

3838
init?(flag: String) {
39-
switch flag.lowercased() {
40-
case "internal":
41-
self = .internal
42-
case "public":
43-
self = .public
44-
case "package":
45-
self = .package
46-
default:
47-
return nil
39+
self.init(rawValue: flag.lowercased())
40+
}
41+
}
42+
43+
enum ImportDirective: Equatable {
44+
case accessLevel(Visibility)
45+
case plain
46+
case implementationOnly
47+
48+
var isAccessLevel: Bool {
49+
switch self {
50+
case .accessLevel: true
51+
default: false
52+
}
53+
}
54+
55+
var snippet: String {
56+
switch self {
57+
case let .accessLevel(visibility):
58+
"\(visibility) import"
59+
case .plain:
60+
"import"
61+
case .implementationOnly:
62+
"@_implementationOnly import"
4863
}
4964
}
5065
}
5166

5267
let outputNaming: OutputNaming
5368
let protoToModuleMappings: ProtoFileToModuleMappings
5469
let visibility: Visibility
55-
let implementationOnlyImports: Bool
70+
let importDirective: ImportDirective
5671
let experimentalStripNonfunctionalCodegen: Bool
5772

5873
/// A string snippet to insert for the visibility
@@ -64,6 +79,7 @@ class GeneratorOptions {
6479
var visibility: Visibility = .internal
6580
var swiftProtobufModuleName: String? = nil
6681
var implementationOnlyImports: Bool = false
82+
var useAccessLevelOnImports = false
6783
var experimentalStripNonfunctionalCodegen: Bool = false
6884

6985
for pair in parameter.parsedPairs {
@@ -102,6 +118,13 @@ class GeneratorOptions {
102118
throw GenerationError.invalidParameterValue(name: pair.key,
103119
value: pair.value)
104120
}
121+
case "UseAccessLevelOnImports":
122+
if let value = Bool(pair.value) {
123+
useAccessLevelOnImports = value
124+
} else {
125+
throw GenerationError.invalidParameterValue(name: pair.key,
126+
value: pair.value)
127+
}
105128
case "experimental_strip_nonfunctional_codegen":
106129
if pair.value.isEmpty { // Also support option without any value.
107130
experimentalStripNonfunctionalCodegen = true
@@ -140,13 +163,22 @@ class GeneratorOptions {
140163
visibilitySourceSnippet = "package "
141164
}
142165

143-
self.implementationOnlyImports = implementationOnlyImports
144166
self.experimentalStripNonfunctionalCodegen = experimentalStripNonfunctionalCodegen
145167

168+
self.importDirective = switch (implementationOnlyImports, useAccessLevelOnImports) {
169+
case (false, false): .plain
170+
case (false, true): .accessLevel(visibility)
171+
case (true, false): .implementationOnly
172+
case (true, true): throw GenerationError.message(message: """
173+
When using access levels on imports the @_implementationOnly option is unnecessary.
174+
Disable @_implementationOnly imports.
175+
""")
176+
}
177+
146178
// ------------------------------------------------------------------------
147179
// Now do "cross option" validations.
148180

149-
if self.implementationOnlyImports && self.visibility != .internal {
181+
if implementationOnlyImports && self.visibility != .internal {
150182
throw GenerationError.message(message: """
151183
Cannot use @_implementationOnly imports when the proto visibility is public or package.
152184
Either change the visibility to internal, or disable @_implementationOnly imports.

Tests/SwiftProtobufPluginLibraryTests/Test_ProtoFileToModuleMappings.swift

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,6 @@ import SwiftProtobuf
1313
import SwiftProtobufTestHelpers
1414
@testable import SwiftProtobufPluginLibrary
1515

16-
// Support equality to simplify testing of getting the correct errors.
17-
extension ProtoFileToModuleMappings.LoadError: Equatable {
18-
public static func ==(lhs: ProtoFileToModuleMappings.LoadError, rhs: ProtoFileToModuleMappings.LoadError) -> Bool {
19-
switch (lhs, rhs) {
20-
case (.entryMissingModuleName(let l), .entryMissingModuleName(let r)): return l == r
21-
case (.entryHasNoProtoPaths(let l), .entryHasNoProtoPaths(let r)): return l == r
22-
case (.duplicateProtoPathMapping(let l1, let l2, let l3),
23-
.duplicateProtoPathMapping(let r1, let r2, let r3)): return l1 == r1 && l2 == r2 && l3 == r3
24-
default: return false
25-
}
26-
}
27-
}
28-
2916
// Helpers to make test cases.
3017

3118
fileprivate typealias FileDescriptorProto = Google_Protobuf_FileDescriptorProto

0 commit comments

Comments
 (0)