Skip to content

Commit 5e92f64

Browse files
authored
Make empty generated source files descriptive (#2151)
### Motivation: Sometimes protobuf definition files contain no gRPC services and result in an empty file. This is intentional but could be confusing. ### Modifications: Empty files now contain a comment indicating they are intentional. ``` // This file contained no services. ``` This is analogous to the behavior of swift-protobuf which adds: ``` // This file contained no messages, enums, or extensions. ``` ### Result: More descriptive empty source files.
1 parent 0696e0a commit 5e92f64

9 files changed

+253
-172
lines changed

Diff for: Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
/// A description of an import declaration.
3131
///
3232
/// For example: `import Foo`.
33-
struct ImportDescription: Equatable, Codable, Sendable {
33+
package struct ImportDescription: Equatable, Codable, Sendable {
3434
/// The access level of the imported module.
3535
///
3636
/// For example, the `public` in `public import Foo`.

Diff for: Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift

+18-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
/// Creates a representation for the server and client code, as well as for the enums containing useful type aliases and properties.
1818
/// The representation is generated based on the ``CodeGenerationRequest`` object and user specifications,
1919
/// using types from ``StructuredSwiftRepresentation``.
20-
struct IDLToStructuredSwiftTranslator: Translator {
20+
package struct IDLToStructuredSwiftTranslator: Translator {
21+
package init() {}
22+
2123
func translate(
2224
codeGenerationRequest: CodeGenerationRequest,
2325
accessLevel: SourceGenerator.Config.AccessLevel,
@@ -72,13 +74,23 @@ struct IDLToStructuredSwiftTranslator: Translator {
7274
}
7375
}
7476

75-
let fileDescription = FileDescription(
76-
topComment: .preFormatted(codeGenerationRequest.leadingTrivia),
77-
imports: try self.makeImports(
77+
let imports: [ImportDescription]
78+
if codeGenerationRequest.services.isEmpty {
79+
imports = []
80+
codeBlocks.append(
81+
CodeBlock(comment: .inline("This file contained no services."))
82+
)
83+
} else {
84+
imports = try self.makeImports(
7885
dependencies: codeGenerationRequest.dependencies,
7986
accessLevel: accessLevel,
8087
accessLevelOnImports: accessLevelOnImports
81-
),
88+
)
89+
}
90+
91+
let fileDescription = FileDescription(
92+
topComment: .preFormatted(codeGenerationRequest.leadingTrivia),
93+
imports: imports,
8294
codeBlocks: codeBlocks
8395
)
8496

@@ -87,7 +99,7 @@ struct IDLToStructuredSwiftTranslator: Translator {
8799
return StructuredSwiftRepresentation(file: file)
88100
}
89101

90-
private func makeImports(
102+
package func makeImports(
91103
dependencies: [Dependency],
92104
accessLevel: SourceGenerator.Config.AccessLevel,
93105
accessLevelOnImports: Bool

Diff for: Sources/GRPCCodeGen/SourceGenerator.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ public struct SourceGenerator: Sendable {
6161

6262
/// The possible access levels for the generated code.
6363
public struct AccessLevel: Sendable, Hashable {
64-
internal var level: Level
65-
internal enum Level {
64+
package var level: Level
65+
package enum Level {
6666
case `internal`
6767
case `public`
6868
case `package`
@@ -94,6 +94,7 @@ public struct SourceGenerator: Sendable {
9494
client: self.config.client,
9595
server: self.config.server
9696
)
97+
9798
let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation)
9899

99100
return sourceFile

Diff for: Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Testing
1818

1919
@testable import GRPCCodeGen
2020

21-
extension StructuedSwiftTests {
21+
extension StructuredSwiftTests {
2222
@Suite("Client")
2323
struct Client {
2424
@Test(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright 2025, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import GRPCCodeGen
18+
import Testing
19+
20+
extension StructuredSwiftTests {
21+
@Suite("Import")
22+
struct Import {
23+
static let translator = IDLToStructuredSwiftTranslator()
24+
25+
static let allAccessLevels: [SourceGenerator.Config.AccessLevel] = [
26+
.internal, .public, .package,
27+
]
28+
29+
@Test(
30+
"import rendering",
31+
arguments: allAccessLevels
32+
)
33+
func imports(accessLevel: SourceGenerator.Config.AccessLevel) throws {
34+
var dependencies = [Dependency]()
35+
dependencies.append(Dependency(module: "Foo", accessLevel: .public))
36+
dependencies.append(
37+
Dependency(
38+
item: .init(kind: .typealias, name: "Bar"),
39+
module: "Foo",
40+
accessLevel: .internal
41+
)
42+
)
43+
dependencies.append(
44+
Dependency(
45+
item: .init(kind: .struct, name: "Baz"),
46+
module: "Foo",
47+
accessLevel: .package
48+
)
49+
)
50+
dependencies.append(
51+
Dependency(
52+
item: .init(kind: .class, name: "Bac"),
53+
module: "Foo",
54+
accessLevel: .package
55+
)
56+
)
57+
dependencies.append(
58+
Dependency(
59+
item: .init(kind: .enum, name: "Bap"),
60+
module: "Foo",
61+
accessLevel: .package
62+
)
63+
)
64+
dependencies.append(
65+
Dependency(
66+
item: .init(kind: .protocol, name: "Bat"),
67+
module: "Foo",
68+
accessLevel: .package
69+
)
70+
)
71+
dependencies.append(
72+
Dependency(
73+
item: .init(kind: .let, name: "Baq"),
74+
module: "Foo",
75+
accessLevel: .package
76+
)
77+
)
78+
dependencies.append(
79+
Dependency(
80+
item: .init(kind: .var, name: "Bag"),
81+
module: "Foo",
82+
accessLevel: .package
83+
)
84+
)
85+
dependencies.append(
86+
Dependency(
87+
item: .init(kind: .func, name: "Bak"),
88+
module: "Foo",
89+
accessLevel: .package
90+
)
91+
)
92+
93+
let expected =
94+
"""
95+
\(accessLevel.level) import GRPCCore
96+
public import Foo
97+
internal import typealias Foo.Bar
98+
package import struct Foo.Baz
99+
package import class Foo.Bac
100+
package import enum Foo.Bap
101+
package import protocol Foo.Bat
102+
package import let Foo.Baq
103+
package import var Foo.Bag
104+
package import func Foo.Bak
105+
"""
106+
107+
let imports = try StructuredSwiftTests.Import.translator.makeImports(
108+
dependencies: dependencies,
109+
accessLevel: accessLevel,
110+
accessLevelOnImports: true
111+
)
112+
113+
#expect(render(imports) == expected)
114+
}
115+
116+
@Test(
117+
"preconcurrency import rendering",
118+
arguments: allAccessLevels
119+
)
120+
func preconcurrencyImports(accessLevel: SourceGenerator.Config.AccessLevel) throws {
121+
var dependencies = [Dependency]()
122+
dependencies.append(
123+
Dependency(
124+
module: "Foo",
125+
preconcurrency: .required,
126+
accessLevel: .internal
127+
)
128+
)
129+
dependencies.append(
130+
Dependency(
131+
item: .init(kind: .enum, name: "Bar"),
132+
module: "Foo",
133+
preconcurrency: .required,
134+
accessLevel: .internal
135+
)
136+
)
137+
dependencies.append(
138+
Dependency(
139+
module: "Baz",
140+
preconcurrency: .requiredOnOS(["Deq", "Der"]),
141+
accessLevel: .internal
142+
)
143+
)
144+
145+
let expected =
146+
"""
147+
\(accessLevel.level) import GRPCCore
148+
@preconcurrency internal import Foo
149+
@preconcurrency internal import enum Foo.Bar
150+
#if os(Deq) || os(Der)
151+
@preconcurrency internal import Baz
152+
#else
153+
internal import Baz
154+
#endif
155+
"""
156+
157+
let imports = try StructuredSwiftTests.Import.translator.makeImports(
158+
dependencies: dependencies,
159+
accessLevel: accessLevel,
160+
accessLevelOnImports: true
161+
)
162+
163+
#expect(render(imports) == expected)
164+
}
165+
166+
@Test(
167+
"SPI import rendering",
168+
arguments: allAccessLevels
169+
)
170+
func spiImports(accessLevel: SourceGenerator.Config.AccessLevel) throws {
171+
var dependencies = [Dependency]()
172+
dependencies.append(
173+
Dependency(module: "Foo", spi: "Secret", accessLevel: .internal)
174+
)
175+
dependencies.append(
176+
Dependency(
177+
item: .init(kind: .enum, name: "Bar"),
178+
module: "Foo",
179+
spi: "Secret",
180+
accessLevel: .internal
181+
)
182+
)
183+
184+
let expected =
185+
"""
186+
\(accessLevel.level) import GRPCCore
187+
@_spi(Secret) internal import Foo
188+
@_spi(Secret) internal import enum Foo.Bar
189+
"""
190+
191+
let imports = try StructuredSwiftTests.Import.translator.makeImports(
192+
dependencies: dependencies,
193+
accessLevel: accessLevel,
194+
accessLevelOnImports: true
195+
)
196+
197+
#expect(render(imports) == expected)
198+
}
199+
200+
}
201+
}

Diff for: Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Testing
1818

1919
@testable import GRPCCodeGen
2020

21-
extension StructuedSwiftTests {
21+
extension StructuredSwiftTests {
2222
@Suite("Metadata")
2323
struct Metadata {
2424
@Test("typealias Input = <Name>", arguments: AccessModifier.allCases)

Diff for: Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Testing
1818

1919
@testable import GRPCCodeGen
2020

21-
extension StructuedSwiftTests {
21+
extension StructuredSwiftTests {
2222
@Suite("Server")
2323
struct Server {
2424
@Test(

Diff for: Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import Testing
1919
@testable import GRPCCodeGen
2020

2121
// Used as a namespace for organising other structured swift tests.
22-
@Suite("Structued Swift")
23-
struct StructuedSwiftTests {}
22+
@Suite("Structured Swift")
23+
struct StructuredSwiftTests {}
2424

2525
func render(_ declaration: Declaration) -> String {
2626
let renderer = TextBasedRenderer(indentation: 2)
@@ -40,6 +40,12 @@ func render(_ blocks: [CodeBlock]) -> String {
4040
return renderer.renderedContents()
4141
}
4242

43+
func render(_ imports: [ImportDescription]) -> String {
44+
let renderer = TextBasedRenderer(indentation: 2)
45+
renderer.renderImports(imports)
46+
return renderer.renderedContents()
47+
}
48+
4349
enum RPCKind: Hashable, Sendable, CaseIterable {
4450
case unary
4551
case clientStreaming

0 commit comments

Comments
 (0)