Browse Source

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.
Rick Newton-Rogers 1 year ago
parent
commit
5e92f645c5

+ 1 - 1
Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift

@@ -30,7 +30,7 @@
 /// A description of an import declaration.
 /// A description of an import declaration.
 ///
 ///
 /// For example: `import Foo`.
 /// For example: `import Foo`.
-struct ImportDescription: Equatable, Codable, Sendable {
+package struct ImportDescription: Equatable, Codable, Sendable {
   /// The access level of the imported module.
   /// The access level of the imported module.
   ///
   ///
   /// For example, the `public` in `public import Foo`.
   /// For example, the `public` in `public import Foo`.

+ 18 - 6
Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift

@@ -17,7 +17,9 @@
 /// Creates a representation for the server and client code, as well as for the enums containing useful type aliases and properties.
 /// Creates a representation for the server and client code, as well as for the enums containing useful type aliases and properties.
 /// The representation is generated based on the ``CodeGenerationRequest`` object and user specifications,
 /// The representation is generated based on the ``CodeGenerationRequest`` object and user specifications,
 /// using types from ``StructuredSwiftRepresentation``.
 /// using types from ``StructuredSwiftRepresentation``.
-struct IDLToStructuredSwiftTranslator: Translator {
+package struct IDLToStructuredSwiftTranslator: Translator {
+  package init() {}
+
   func translate(
   func translate(
     codeGenerationRequest: CodeGenerationRequest,
     codeGenerationRequest: CodeGenerationRequest,
     accessLevel: SourceGenerator.Config.AccessLevel,
     accessLevel: SourceGenerator.Config.AccessLevel,
@@ -72,13 +74,23 @@ struct IDLToStructuredSwiftTranslator: Translator {
       }
       }
     }
     }
 
 
-    let fileDescription = FileDescription(
-      topComment: .preFormatted(codeGenerationRequest.leadingTrivia),
-      imports: try self.makeImports(
+    let imports: [ImportDescription]
+    if codeGenerationRequest.services.isEmpty {
+      imports = []
+      codeBlocks.append(
+        CodeBlock(comment: .inline("This file contained no services."))
+      )
+    } else {
+      imports = try self.makeImports(
         dependencies: codeGenerationRequest.dependencies,
         dependencies: codeGenerationRequest.dependencies,
         accessLevel: accessLevel,
         accessLevel: accessLevel,
         accessLevelOnImports: accessLevelOnImports
         accessLevelOnImports: accessLevelOnImports
-      ),
+      )
+    }
+
+    let fileDescription = FileDescription(
+      topComment: .preFormatted(codeGenerationRequest.leadingTrivia),
+      imports: imports,
       codeBlocks: codeBlocks
       codeBlocks: codeBlocks
     )
     )
 
 
@@ -87,7 +99,7 @@ struct IDLToStructuredSwiftTranslator: Translator {
     return StructuredSwiftRepresentation(file: file)
     return StructuredSwiftRepresentation(file: file)
   }
   }
 
 
-  private func makeImports(
+  package func makeImports(
     dependencies: [Dependency],
     dependencies: [Dependency],
     accessLevel: SourceGenerator.Config.AccessLevel,
     accessLevel: SourceGenerator.Config.AccessLevel,
     accessLevelOnImports: Bool
     accessLevelOnImports: Bool

+ 3 - 2
Sources/GRPCCodeGen/SourceGenerator.swift

@@ -61,8 +61,8 @@ public struct SourceGenerator: Sendable {
 
 
     /// The possible access levels for the generated code.
     /// The possible access levels for the generated code.
     public struct AccessLevel: Sendable, Hashable {
     public struct AccessLevel: Sendable, Hashable {
-      internal var level: Level
-      internal enum Level {
+      package var level: Level
+      package enum Level {
         case `internal`
         case `internal`
         case `public`
         case `public`
         case `package`
         case `package`
@@ -94,6 +94,7 @@ public struct SourceGenerator: Sendable {
       client: self.config.client,
       client: self.config.client,
       server: self.config.server
       server: self.config.server
     )
     )
+
     let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation)
     let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation)
 
 
     return sourceFile
     return sourceFile

+ 1 - 1
Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift

@@ -18,7 +18,7 @@ import Testing
 
 
 @testable import GRPCCodeGen
 @testable import GRPCCodeGen
 
 
-extension StructuedSwiftTests {
+extension StructuredSwiftTests {
   @Suite("Client")
   @Suite("Client")
   struct Client {
   struct Client {
     @Test(
     @Test(

+ 201 - 0
Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift

@@ -0,0 +1,201 @@
+/*
+ * Copyright 2025, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import GRPCCodeGen
+import Testing
+
+extension StructuredSwiftTests {
+  @Suite("Import")
+  struct Import {
+    static let translator = IDLToStructuredSwiftTranslator()
+
+    static let allAccessLevels: [SourceGenerator.Config.AccessLevel] = [
+      .internal, .public, .package,
+    ]
+
+    @Test(
+      "import rendering",
+      arguments: allAccessLevels
+    )
+    func imports(accessLevel: SourceGenerator.Config.AccessLevel) throws {
+      var dependencies = [Dependency]()
+      dependencies.append(Dependency(module: "Foo", accessLevel: .public))
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .typealias, name: "Bar"),
+          module: "Foo",
+          accessLevel: .internal
+        )
+      )
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .struct, name: "Baz"),
+          module: "Foo",
+          accessLevel: .package
+        )
+      )
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .class, name: "Bac"),
+          module: "Foo",
+          accessLevel: .package
+        )
+      )
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .enum, name: "Bap"),
+          module: "Foo",
+          accessLevel: .package
+        )
+      )
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .protocol, name: "Bat"),
+          module: "Foo",
+          accessLevel: .package
+        )
+      )
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .let, name: "Baq"),
+          module: "Foo",
+          accessLevel: .package
+        )
+      )
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .var, name: "Bag"),
+          module: "Foo",
+          accessLevel: .package
+        )
+      )
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .func, name: "Bak"),
+          module: "Foo",
+          accessLevel: .package
+        )
+      )
+
+      let expected =
+        """
+        \(accessLevel.level) import GRPCCore
+        public import Foo
+        internal import typealias Foo.Bar
+        package import struct Foo.Baz
+        package import class Foo.Bac
+        package import enum Foo.Bap
+        package import protocol Foo.Bat
+        package import let Foo.Baq
+        package import var Foo.Bag
+        package import func Foo.Bak
+        """
+
+      let imports = try StructuredSwiftTests.Import.translator.makeImports(
+        dependencies: dependencies,
+        accessLevel: accessLevel,
+        accessLevelOnImports: true
+      )
+
+      #expect(render(imports) == expected)
+    }
+
+    @Test(
+      "preconcurrency import rendering",
+      arguments: allAccessLevels
+    )
+    func preconcurrencyImports(accessLevel: SourceGenerator.Config.AccessLevel) throws {
+      var dependencies = [Dependency]()
+      dependencies.append(
+        Dependency(
+          module: "Foo",
+          preconcurrency: .required,
+          accessLevel: .internal
+        )
+      )
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .enum, name: "Bar"),
+          module: "Foo",
+          preconcurrency: .required,
+          accessLevel: .internal
+        )
+      )
+      dependencies.append(
+        Dependency(
+          module: "Baz",
+          preconcurrency: .requiredOnOS(["Deq", "Der"]),
+          accessLevel: .internal
+        )
+      )
+
+      let expected =
+        """
+        \(accessLevel.level) import GRPCCore
+        @preconcurrency internal import Foo
+        @preconcurrency internal import enum Foo.Bar
+        #if os(Deq) || os(Der)
+        @preconcurrency internal import Baz
+        #else
+        internal import Baz
+        #endif
+        """
+
+      let imports = try StructuredSwiftTests.Import.translator.makeImports(
+        dependencies: dependencies,
+        accessLevel: accessLevel,
+        accessLevelOnImports: true
+      )
+
+      #expect(render(imports) == expected)
+    }
+
+    @Test(
+      "SPI import rendering",
+      arguments: allAccessLevels
+    )
+    func spiImports(accessLevel: SourceGenerator.Config.AccessLevel) throws {
+      var dependencies = [Dependency]()
+      dependencies.append(
+        Dependency(module: "Foo", spi: "Secret", accessLevel: .internal)
+      )
+      dependencies.append(
+        Dependency(
+          item: .init(kind: .enum, name: "Bar"),
+          module: "Foo",
+          spi: "Secret",
+          accessLevel: .internal
+        )
+      )
+
+      let expected =
+        """
+        \(accessLevel.level) import GRPCCore
+        @_spi(Secret) internal import Foo
+        @_spi(Secret) internal import enum Foo.Bar
+        """
+
+      let imports = try StructuredSwiftTests.Import.translator.makeImports(
+        dependencies: dependencies,
+        accessLevel: accessLevel,
+        accessLevelOnImports: true
+      )
+
+      #expect(render(imports) == expected)
+    }
+
+  }
+}

+ 1 - 1
Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift

@@ -18,7 +18,7 @@ import Testing
 
 
 @testable import GRPCCodeGen
 @testable import GRPCCodeGen
 
 
-extension StructuedSwiftTests {
+extension StructuredSwiftTests {
   @Suite("Metadata")
   @Suite("Metadata")
   struct Metadata {
   struct Metadata {
     @Test("typealias Input = <Name>", arguments: AccessModifier.allCases)
     @Test("typealias Input = <Name>", arguments: AccessModifier.allCases)

+ 1 - 1
Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift

@@ -18,7 +18,7 @@ import Testing
 
 
 @testable import GRPCCodeGen
 @testable import GRPCCodeGen
 
 
-extension StructuedSwiftTests {
+extension StructuredSwiftTests {
   @Suite("Server")
   @Suite("Server")
   struct Server {
   struct Server {
     @Test(
     @Test(

+ 8 - 2
Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift

@@ -19,8 +19,8 @@ import Testing
 @testable import GRPCCodeGen
 @testable import GRPCCodeGen
 
 
 // Used as a namespace for organising other structured swift tests.
 // Used as a namespace for organising other structured swift tests.
-@Suite("Structued Swift")
-struct StructuedSwiftTests {}
+@Suite("Structured Swift")
+struct StructuredSwiftTests {}
 
 
 func render(_ declaration: Declaration) -> String {
 func render(_ declaration: Declaration) -> String {
   let renderer = TextBasedRenderer(indentation: 2)
   let renderer = TextBasedRenderer(indentation: 2)
@@ -40,6 +40,12 @@ func render(_ blocks: [CodeBlock]) -> String {
   return renderer.renderedContents()
   return renderer.renderedContents()
 }
 }
 
 
+func render(_ imports: [ImportDescription]) -> String {
+  let renderer = TextBasedRenderer(indentation: 2)
+  renderer.renderImports(imports)
+  return renderer.renderedContents()
+}
+
 enum RPCKind: Hashable, Sendable, CaseIterable {
 enum RPCKind: Hashable, Sendable, CaseIterable {
   case unary
   case unary
   case clientStreaming
   case clientStreaming

+ 19 - 158
Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift

@@ -21,164 +21,6 @@ import XCTest
 @testable import GRPCCodeGen
 @testable import GRPCCodeGen
 
 
 final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
 final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
-  func testImports() throws {
-    var dependencies = [Dependency]()
-    dependencies.append(Dependency(module: "Foo", accessLevel: .public))
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .typealias, name: "Bar"),
-        module: "Foo",
-        accessLevel: .internal
-      )
-    )
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .struct, name: "Baz"),
-        module: "Foo",
-        accessLevel: .package
-      )
-    )
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .class, name: "Bac"),
-        module: "Foo",
-        accessLevel: .package
-      )
-    )
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .enum, name: "Bap"),
-        module: "Foo",
-        accessLevel: .package
-      )
-    )
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .protocol, name: "Bat"),
-        module: "Foo",
-        accessLevel: .package
-      )
-    )
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .let, name: "Baq"),
-        module: "Foo",
-        accessLevel: .package
-      )
-    )
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .var, name: "Bag"),
-        module: "Foo",
-        accessLevel: .package
-      )
-    )
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .func, name: "Bak"),
-        module: "Foo",
-        accessLevel: .package
-      )
-    )
-
-    let expectedSwift =
-      """
-      /// Some really exciting license header 2023.
-
-      public import GRPCCore
-      public import Foo
-      internal import typealias Foo.Bar
-      package import struct Foo.Baz
-      package import class Foo.Bac
-      package import enum Foo.Bap
-      package import protocol Foo.Bat
-      package import let Foo.Baq
-      package import var Foo.Bag
-      package import func Foo.Bak
-
-      """
-    try self.assertIDLToStructuredSwiftTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies),
-      expectedSwift: expectedSwift,
-      accessLevel: .public
-    )
-  }
-
-  func testPreconcurrencyImports() throws {
-    var dependencies = [Dependency]()
-    dependencies.append(
-      Dependency(
-        module: "Foo",
-        preconcurrency: .required,
-        accessLevel: .internal
-      )
-    )
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .enum, name: "Bar"),
-        module: "Foo",
-        preconcurrency: .required,
-        accessLevel: .internal
-      )
-    )
-    dependencies.append(
-      Dependency(
-        module: "Baz",
-        preconcurrency: .requiredOnOS(["Deq", "Der"]),
-        accessLevel: .internal
-      )
-    )
-    let expectedSwift =
-      """
-      /// Some really exciting license header 2023.
-
-      public import GRPCCore
-      @preconcurrency internal import Foo
-      @preconcurrency internal import enum Foo.Bar
-      #if os(Deq) || os(Der)
-      @preconcurrency internal import Baz
-      #else
-      internal import Baz
-      #endif
-
-      """
-    try self.assertIDLToStructuredSwiftTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies),
-      expectedSwift: expectedSwift,
-      accessLevel: .public
-    )
-  }
-
-  func testSPIImports() throws {
-    var dependencies = [Dependency]()
-    dependencies.append(
-      Dependency(module: "Foo", spi: "Secret", accessLevel: .internal)
-    )
-    dependencies.append(
-      Dependency(
-        item: .init(kind: .enum, name: "Bar"),
-        module: "Foo",
-        spi: "Secret",
-        accessLevel: .internal
-      )
-    )
-
-    let expectedSwift =
-      """
-      /// Some really exciting license header 2023.
-
-      public import GRPCCore
-      @_spi(Secret) internal import Foo
-      @_spi(Secret) internal import enum Foo.Bar
-
-      """
-    try self.assertIDLToStructuredSwiftTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies),
-      expectedSwift: expectedSwift,
-      accessLevel: .public
-    )
-  }
-
   func testGeneration() throws {
   func testGeneration() throws {
     var dependencies = [Dependency]()
     var dependencies = [Dependency]()
     dependencies.append(
     dependencies.append(
@@ -298,6 +140,25 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
     )
     )
   }
   }
 
 
+  func testEmptyFileGeneration() throws {
+    let expectedSwift =
+      """
+      /// Some really exciting license header 2023.
+
+
+      // This file contained no services.
+      """
+    try self.assertIDLToStructuredSwiftTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(
+        services: [],
+        dependencies: []
+      ),
+      expectedSwift: expectedSwift,
+      accessLevel: .public,
+      server: true
+    )
+  }
+
   private func assertIDLToStructuredSwiftTranslation(
   private func assertIDLToStructuredSwiftTranslation(
     codeGenerationRequest: CodeGenerationRequest,
     codeGenerationRequest: CodeGenerationRequest,
     expectedSwift: String,
     expectedSwift: String,