CodeGenerator.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. // Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift
  2. //
  3. // Copyright (c) 2014 - 2023 Apple Inc. and the project authors
  4. // Licensed under Apache License v2.0 with Runtime Library Exception
  5. //
  6. // See LICENSE.txt for license information:
  7. // https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
  8. //
  9. // -----------------------------------------------------------------------------
  10. ///
  11. /// This provides the basic interface for writing a CodeGenerator.
  12. ///
  13. // -----------------------------------------------------------------------------
  14. import Foundation
  15. import SwiftProtobuf
  16. /// A protocol that generator should conform to then get easy support for
  17. /// being a protocol buffer compiler pluign.
  18. public protocol CodeGenerator {
  19. init()
  20. /// Generates code for the given proto files.
  21. ///
  22. /// - Parameters:
  23. /// - parameter: The parameter (or paramenters) passed for the generator.
  24. /// This is for parameters specific to this generator,
  25. /// `parse(parameter:)` (below) can be used to split back out
  26. /// multiple parameters into the combined for the protocol buffer
  27. /// compiler uses.
  28. /// - protoCompilerContext: Context information about the protocol buffer
  29. /// compiler being used.
  30. /// - generatorOutputs: A object that can be used to send back the
  31. /// generated outputs.
  32. ///
  33. /// - Throws: Can throw any `Error` to fail generate. `String(describing:)`
  34. /// will be called on the error to provide the error string reported
  35. /// to the user attempting to generate sources.
  36. func generate(
  37. files: [FileDescriptor],
  38. parameter: any CodeGeneratorParameter,
  39. protoCompilerContext: any ProtoCompilerContext,
  40. generatorOutputs: any GeneratorOutputs) throws
  41. /// The list of features this CodeGenerator support to be reported back to
  42. /// the protocol buffer compiler.
  43. var supportedFeatures: [Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] { get }
  44. /// The Protobuf Edition range that this generator can handle. Attempting
  45. /// to generate for an Edition outside this range will cause protoc to
  46. /// error.
  47. var supportedEditionRange: ClosedRange<Google_Protobuf_Edition> { get }
  48. /// If provided, the argument parsing will support `--version` and report
  49. /// this value.
  50. var version: String? { get }
  51. /// If provided and `printHelp` isn't provide, this value will be including in
  52. /// default output for the `--help` output.
  53. var projectURL: String? { get }
  54. /// If provided and `printHelp` isn't provide, this value will be including in
  55. /// default output for the `--help` output.
  56. var copyrightLine: String? { get }
  57. /// Will be called for `-h` or `--help`, should `print()` out whatever is
  58. /// desired; there is a default implementation that uses the above info
  59. /// when provided.
  60. func printHelp()
  61. }
  62. extension CommandLine {
  63. /// Get the command-line arguments passed to this process in a non mutable
  64. /// form. Idea from https://github.com/apple/swift/issues/66213
  65. ///
  66. /// - Returns: An array of command-line arguments.
  67. fileprivate static let safeArguments: [String] =
  68. UnsafeBufferPointer(start: unsafeArgv, count: Int(argc)).compactMap {
  69. String(validatingUTF8: $0!)
  70. }
  71. }
  72. extension CodeGenerator {
  73. var programName: String {
  74. guard let name = CommandLine.safeArguments.first?.split(separator: "/").last else {
  75. return "<plugin>"
  76. }
  77. return String(name)
  78. }
  79. /// Runs as a protocol buffer compiler plugin based on the given arguments
  80. /// or falls back to `CommandLine.arguments`.
  81. public func main(_ args: [String]?) {
  82. let args = args ?? Array(CommandLine.safeArguments.dropFirst())
  83. for arg in args {
  84. if arg == "--version", let version = version {
  85. print("\(programName) \(version)")
  86. return
  87. }
  88. if arg == "-h" || arg == "--help" {
  89. printHelp()
  90. return
  91. }
  92. // Could look at bringing back the support for recorded requests, but
  93. // haven't needed it in a long time.
  94. var stderr = StandardErrorOutputStream()
  95. print("Unknown argument: \(arg)", to: &stderr)
  96. return
  97. }
  98. let response: Google_Protobuf_Compiler_CodeGeneratorResponse
  99. do {
  100. let request = try Google_Protobuf_Compiler_CodeGeneratorRequest(
  101. serializedBytes: FileHandle.standardInput.readDataToEndOfFile()
  102. )
  103. response = generateCode(request: request, generator: self)
  104. } catch let e {
  105. response = Google_Protobuf_Compiler_CodeGeneratorResponse(
  106. error: "Received an unparsable request from the compiler: \(e)")
  107. }
  108. let serializedResponse: Data
  109. do {
  110. serializedResponse = try response.serializedBytes()
  111. } catch let e {
  112. var stderr = StandardErrorOutputStream()
  113. print("\(programName): Failure while serializing response: \(e)", to: &stderr)
  114. return
  115. }
  116. FileHandle.standardOutput.write(serializedResponse)
  117. }
  118. /// Runs as a protocol buffer compiler plugin; reading the generation request
  119. /// off stdin and sending the response on stdout.
  120. ///
  121. /// Instead of calling this, just add `@main` to your `CodeGenerator`.
  122. public static func main() {
  123. let generator = Self()
  124. generator.main(nil)
  125. }
  126. }
  127. // Provide default implementation for things so `CodeGenerator`s only have to
  128. // provide them if they wish too.
  129. extension CodeGenerator {
  130. public var supportedEditionRange: ClosedRange<Google_Protobuf_Edition> {
  131. // Default impl of unknown so generator don't have to provide this until
  132. // they support editions.
  133. return Google_Protobuf_Edition.unknown...Google_Protobuf_Edition.unknown
  134. }
  135. public var version: String? { return nil }
  136. public var projectURL: String? { return nil }
  137. public var copyrightLine: String? { return nil }
  138. public func printHelp() {
  139. print("\(programName): A plugin for protoc and should not normally be run directly.")
  140. if let copyright = copyrightLine {
  141. print("\(copyright)")
  142. }
  143. if let projectURL = projectURL {
  144. print(
  145. """
  146. For more information on the usage of this plugin, please see:
  147. \(projectURL)
  148. """)
  149. }
  150. }
  151. }
  152. /// Uses the given `Google_Protobuf_Compiler_CodeGeneratorRequest` and
  153. /// `CodeGenerator` to get code generated and create the
  154. /// `Google_Protobuf_Compiler_CodeGeneratorResponse`. If there is a failure,
  155. /// the failure will be used in the response to be returned to the protocol
  156. /// buffer compiler to then be reported.
  157. ///
  158. /// - Parameters:
  159. /// - request: The request proto as generated by the protocol buffer compiler.
  160. /// - geneator: The `CodeGenerator` to use for generation.
  161. ///
  162. /// - Returns a filled out response with the success or failure of the
  163. /// generation.
  164. public func generateCode(
  165. request: Google_Protobuf_Compiler_CodeGeneratorRequest,
  166. generator: any CodeGenerator
  167. ) -> Google_Protobuf_Compiler_CodeGeneratorResponse {
  168. // TODO: This will need update to language specific features.
  169. let descriptorSet = DescriptorSet(protos: request.protoFile)
  170. var files = [FileDescriptor]()
  171. for name in request.fileToGenerate {
  172. guard let fileDescriptor = descriptorSet.fileDescriptor(named: name) else {
  173. return Google_Protobuf_Compiler_CodeGeneratorResponse(
  174. error:
  175. "protoc asked plugin to generate a file but did not provide a descriptor for the file: \(name)"
  176. )
  177. }
  178. files.append(fileDescriptor)
  179. }
  180. let context = InternalProtoCompilerContext(request: request)
  181. let outputs = InternalGeneratorOutputs()
  182. let parameter = InternalCodeGeneratorParameter(request.parameter)
  183. do {
  184. try generator.generate(
  185. files: files, parameter: parameter, protoCompilerContext: context,
  186. generatorOutputs: outputs)
  187. } catch let e {
  188. return Google_Protobuf_Compiler_CodeGeneratorResponse(error: String(describing: e))
  189. }
  190. var response = Google_Protobuf_Compiler_CodeGeneratorResponse()
  191. response.file = outputs.files
  192. // TODO: Could supportedFeatures be completely handled within library?
  193. // - The only "hard" part around hiding the proto3 optional support is making
  194. // sure the oneof index related bits aren't leaked from FieldDescriptors.
  195. // Otherwise the oneof related apis could likely take over the "realOneof"
  196. // jobs and just never vend the synthetic information.
  197. // - The editions support bit likely could be computed based on the values
  198. // `supportedEditionRange` having been overridden.
  199. let supportedFeatures = generator.supportedFeatures
  200. response.supportedFeatures = supportedFeatures.reduce(0) { $0 | UInt64($1.rawValue) }
  201. if supportedFeatures.contains(.supportsEditions) {
  202. let supportedEditions = generator.supportedEditionRange
  203. precondition(supportedEditions.upperBound != .unknown,
  204. "For a CodeGenerator to support Editions, it must override `supportedEditionRange`")
  205. precondition(DescriptorSet.bundledEditionsSupport.contains(supportedEditions.lowerBound),
  206. "A CodeGenerator can't claim to support an Edition before what the library supports: \(supportedEditions.lowerBound) vs \(DescriptorSet.bundledEditionsSupport)")
  207. precondition(DescriptorSet.bundledEditionsSupport.contains(supportedEditions.upperBound),
  208. "A CodeGenerator can't claim to support an Edition after what the library supports: \(supportedEditions.upperBound) vs \(DescriptorSet.bundledEditionsSupport)")
  209. response.minimumEdition = Int32(supportedEditions.lowerBound.rawValue)
  210. response.maximumEdition = Int32(supportedEditions.upperBound.rawValue)
  211. }
  212. return response
  213. }
  214. // MARK: Internal supporting types
  215. /// Internal implementation of `CodeGeneratorParameter` for
  216. /// `generateCode(request:generator:)`
  217. struct InternalCodeGeneratorParameter: CodeGeneratorParameter {
  218. let parameter: String
  219. init(_ parameter: String) {
  220. self.parameter = parameter
  221. }
  222. var parsedPairs: [(key: String, value: String)] {
  223. guard !parameter.isEmpty else {
  224. return []
  225. }
  226. let parts = parameter.components(separatedBy: ",")
  227. return parts.map { s -> (key: String, value: String) in
  228. guard let index = s.range(of: "=")?.lowerBound else {
  229. // Key only, no value ("baz" in example).
  230. return (trimWhitespace(s), "")
  231. }
  232. return (
  233. key: trimWhitespace(s[..<index]),
  234. value: trimWhitespace(s[s.index(after: index)...])
  235. )
  236. }
  237. }
  238. }
  239. /// Internal implementation of `ProtoCompilerContext` for
  240. /// `generateCode(request:generator:)`
  241. private struct InternalProtoCompilerContext: ProtoCompilerContext {
  242. let version: Google_Protobuf_Compiler_Version?
  243. init(request: Google_Protobuf_Compiler_CodeGeneratorRequest) {
  244. self.version = request.hasCompilerVersion ? request.compilerVersion : nil
  245. }
  246. }
  247. /// Internal implementation of `GeneratorOutputs` for
  248. /// `generateCode(request:generator:)`
  249. private final class InternalGeneratorOutputs: GeneratorOutputs {
  250. enum OutputError: Error, CustomStringConvertible {
  251. /// Attempt to add two files with the same name.
  252. case duplicateName(String)
  253. var description: String {
  254. switch self {
  255. case .duplicateName(let name):
  256. return "Generator tried to generate two files named \(name)."
  257. }
  258. }
  259. }
  260. var files: [Google_Protobuf_Compiler_CodeGeneratorResponse.File] = []
  261. private var fileNames: Set<String> = []
  262. func add(fileName: String, contents: String) throws {
  263. guard !fileNames.contains(fileName) else {
  264. throw OutputError.duplicateName(fileName)
  265. }
  266. fileNames.insert(fileName)
  267. files.append(
  268. Google_Protobuf_Compiler_CodeGeneratorResponse.File(
  269. name: fileName,
  270. content: contents))
  271. }
  272. }