Browse Source

Update renderer-blank line after imports, delete blank line from docs (#1782)

Motivation:

We want to have a blank line between the imports and the generated code
and no blank line between the docs and the protocols/methods declarations.

Modifications:

Updated the TextBasedRenderer and the tests.

Result:

The generated code will have the right format.
Stefana-Ioana Dranca 2 years ago
parent
commit
8ac3470702

+ 21 - 10
Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift

@@ -136,8 +136,14 @@ struct TextBasedRenderer: RendererProtocol {
 
   /// Renders the specified Swift file.
   func renderFile(_ description: FileDescription) {
-    if let topComment = description.topComment { renderComment(topComment) }
-    if let imports = description.imports { renderImports(imports) }
+    if let topComment = description.topComment {
+      renderComment(topComment)
+      writer.writeLine("")
+    }
+    if let imports = description.imports {
+      renderImports(imports)
+      writer.writeLine("")
+    }
     for codeBlock in description.codeBlocks {
       renderCodeBlock(codeBlock)
       writer.writeLine("")
@@ -165,15 +171,18 @@ struct TextBasedRenderer: RendererProtocol {
       prefix = ""
       commentString = string
     }
-    if prefix.isEmpty {
-      writer.writeLine(commentString)
-    } else {
-      let lines = commentString.transformingLines { line in
-        if line.isEmpty { return prefix }
-        return "\(prefix) \(line)"
+
+    let lines = commentString.transformingLines { line, isLast in
+      // The last line of a comment that is blank should be dropped.
+      // Pre formatted documentation might contain such lines.
+      if line.isEmpty && prefix.isEmpty && isLast {
+        return nil
+      } else {
+        let formattedPrefix = !prefix.isEmpty && !line.isEmpty ? "\(prefix) " : prefix
+        return "\(formattedPrefix)\(line)"
       }
-      lines.forEach(writer.writeLine)
     }
+    lines.forEach(writer.writeLine)
   }
 
   /// Renders the specified import statements.
@@ -1095,7 +1104,9 @@ extension String {
   /// The closure takes a string representing one line as a parameter.
   /// - Parameter work: The closure that transforms each line.
   /// - Returns: A new string where each line has been transformed using the given closure.
-  fileprivate func transformingLines(_ work: (String) -> String) -> [String] { asLines().map(work) }
+  fileprivate func transformingLines(_ work: (String, Bool) -> String?) -> [String] {
+    asLines().enumeratedWithLastMarker().compactMap(work)
+  }
 }
 
 extension TextBasedRenderer {

+ 1 - 1
Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift

@@ -38,7 +38,7 @@ struct IDLToStructuredSwiftTranslator: Translator {
       try partialResult.append(translateImport(dependency: newDependency))
     }
 
-    var codeBlocks: [CodeBlock] = []
+    var codeBlocks = [CodeBlock]()
     codeBlocks.append(
       contentsOf: try typealiasTranslator.translate(from: codeGenerationRequest)
     )

+ 1 - 1
Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift

@@ -64,7 +64,7 @@ struct TypealiasTranslator: SpecializedTranslator {
   }
 
   func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] {
-    var codeBlocks: [CodeBlock] = []
+    var codeBlocks = [CodeBlock]()
     let services = codeGenerationRequest.services
     let servicesByNamespace = Dictionary(
       grouping: services,

+ 20 - 0
Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift

@@ -89,6 +89,22 @@ final class Test_TextBasedRenderer: XCTestCase {
         // Also, bar
         """#
     )
+    try _test(
+      .preFormatted("/// Lorem ipsum\n"),
+      renderedBy: TextBasedRenderer.renderComment,
+      rendersAs: """
+        /// Lorem ipsum
+        """
+    )
+    try _test(
+      .preFormatted("/// Lorem ipsum\n\n/// Lorem ipsum\n"),
+      renderedBy: TextBasedRenderer.renderComment,
+      rendersAs: """
+        /// Lorem ipsum
+
+        /// Lorem ipsum
+        """
+    )
   }
 
   func testImports() throws {
@@ -825,7 +841,9 @@ final class Test_TextBasedRenderer: XCTestCase {
       renderedBy: TextBasedRenderer.renderFile,
       rendersAs: #"""
         // hi
+
         import Foo
+
         struct Bar {}
 
         """#
@@ -847,7 +865,9 @@ final class Test_TextBasedRenderer: XCTestCase {
       renderedBy: TextBasedRenderer.renderFile,
       rendersAs: #"""
         // hi
+
         import Foo
+
         struct Bar {
           struct Baz {}
         }

+ 77 - 4
Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift

@@ -56,6 +56,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
     let expectedSwift =
       """
       /// Some really exciting license header 2023.
+
       import GRPCCore
       import Foo
       import typealias Foo.Bar
@@ -66,6 +67,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
       import let Foo.Baq
       import var Foo.Bag
       import func Foo.Bak
+
       """
     try self.assertIDLToStructuredSwiftTranslation(
       codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies),
@@ -93,6 +95,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
     let expectedSwift =
       """
       /// Some really exciting license header 2023.
+
       import GRPCCore
       @preconcurrency import Foo
       @preconcurrency import enum Foo.Bar
@@ -101,6 +104,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
       #else
       import Baz
       #endif
+
       """
     try self.assertIDLToStructuredSwiftTranslation(
       codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies),
@@ -123,9 +127,11 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
     let expectedSwift =
       """
       /// Some really exciting license header 2023.
+
       import GRPCCore
       @_spi(Secret) import Foo
       @_spi(Secret) import enum Foo.Bar
+
       """
     try self.assertIDLToStructuredSwiftTranslation(
       codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies),
@@ -134,17 +140,84 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
     )
   }
 
+  func testGeneration() throws {
+    var dependencies = [CodeGenerationRequest.Dependency]()
+    dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret"))
+    dependencies.append(
+      CodeGenerationRequest.Dependency(
+        item: .init(kind: .enum, name: "Bar"),
+        module: "Foo",
+        spi: "Secret"
+      )
+    )
+
+    let serviceA = ServiceDescriptor(
+      documentation: "/// Documentation for AService\n",
+      name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"),
+      namespace: Name(
+        base: "namespaceA",
+        generatedUpperCase: "NamespaceA",
+        generatedLowerCase: "namespaceA"
+      ),
+      methods: []
+    )
+
+    let expectedSwift =
+      """
+      /// Some really exciting license header 2023.
+
+      import GRPCCore
+      @_spi(Secret) import Foo
+      @_spi(Secret) import enum Foo.Bar
+
+      public enum NamespaceA {
+          public enum ServiceA {
+              public enum Methods {}
+              public static let methods: [MethodDescriptor] = []
+              public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol
+              public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol
+          }
+      }
+
+      /// Documentation for AService
+      public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {}
+
+      /// Conformance to `GRPCCore.RegistrableRPCService`.
+      extension NamespaceA.ServiceA.StreamingServiceProtocol {
+          public func registerMethods(with router: inout GRPCCore.RPCRouter) {}
+      }
+
+      /// Documentation for AService
+      public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol {}
+
+      /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`.
+      extension NamespaceA.ServiceA.ServiceProtocol {
+      }
+
+      """
+    try self.assertIDLToStructuredSwiftTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(
+        services: [serviceA],
+        dependencies: dependencies
+      ),
+      expectedSwift: expectedSwift,
+      accessLevel: .public,
+      server: true
+    )
+  }
+
   private func assertIDLToStructuredSwiftTranslation(
     codeGenerationRequest: CodeGenerationRequest,
     expectedSwift: String,
-    accessLevel: SourceGenerator.Configuration.AccessLevel
+    accessLevel: SourceGenerator.Configuration.AccessLevel,
+    server: Bool = false
   ) throws {
     let translator = IDLToStructuredSwiftTranslator()
     let structuredSwift = try translator.translate(
       codeGenerationRequest: codeGenerationRequest,
       accessLevel: accessLevel,
       client: false,
-      server: false
+      server: server
     )
     let renderer = TextBasedRenderer.default
     let sourceFile = try renderer.render(structured: structuredSwift)
@@ -262,7 +335,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
 
   func testSameGeneratedNameServicesSameNamespaceError() throws {
     let serviceA = ServiceDescriptor(
-      documentation: "Documentation for AService",
+      documentation: "/// Documentation for AService\n",
       name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"),
       namespace: Name(
         base: "namespacea",
@@ -272,7 +345,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
       methods: []
     )
     let serviceB = ServiceDescriptor(
-      documentation: "Documentation for BService",
+      documentation: "/// Documentation for BService\n",
       name: Name(base: "BService", generatedUpperCase: "AService", generatedLowerCase: "aService"),
       namespace: Name(
         base: "namespacea",

+ 4 - 20
Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift

@@ -72,30 +72,14 @@ internal func XCTAssertEqualWithDiff(
 }
 
 internal func makeCodeGenerationRequest(
-  services: [CodeGenerationRequest.ServiceDescriptor]
+  services: [CodeGenerationRequest.ServiceDescriptor] = [],
+  dependencies: [CodeGenerationRequest.Dependency] = []
 ) -> CodeGenerationRequest {
   return CodeGenerationRequest(
     fileName: "test.grpc",
-    leadingTrivia: "/// Some really exciting license header 2023.",
-    dependencies: [],
-    services: services,
-    lookupSerializer: {
-      "ProtobufSerializer<\($0)>()"
-    },
-    lookupDeserializer: {
-      "ProtobufDeserializer<\($0)>()"
-    }
-  )
-}
-
-internal func makeCodeGenerationRequest(
-  dependencies: [CodeGenerationRequest.Dependency]
-) -> CodeGenerationRequest {
-  return CodeGenerationRequest(
-    fileName: "test.grpc",
-    leadingTrivia: "/// Some really exciting license header 2023.",
+    leadingTrivia: "/// Some really exciting license header 2023.\n",
     dependencies: dependencies,
-    services: [],
+    services: services,
     lookupSerializer: {
       "ProtobufSerializer<\($0)>()"
     },