CodeGenerator.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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. /// A list of extensions that define Custom Options
  49. /// (https://protobuf.dev/programming-guides/proto2/#customoptions) for this generator so
  50. /// they will be exposed on the `Descriptor` options.
  51. var customOptionExtensions: [any AnyMessageExtension] { get }
  52. /// If provided, the argument parsing will support `--version` and report
  53. /// this value.
  54. var version: String? { get }
  55. /// If provided and `printHelp` isn't provide, this value will be including in
  56. /// default output for the `--help` output.
  57. var projectURL: String? { get }
  58. /// If provided and `printHelp` isn't provide, this value will be including in
  59. /// default output for the `--help` output.
  60. var copyrightLine: String? { get }
  61. /// Will be called for `-h` or `--help`, should `print()` out whatever is
  62. /// desired; there is a default implementation that uses the above info
  63. /// when provided.
  64. func printHelp()
  65. }
  66. extension CommandLine {
  67. /// Get the command-line arguments passed to this process in a non mutable
  68. /// form. Idea from https://github.com/swiftlang/swift/issues/66213
  69. ///
  70. /// - Returns: An array of command-line arguments.
  71. fileprivate static let safeArguments: [String] =
  72. UnsafeBufferPointer(start: unsafeArgv, count: Int(argc)).compactMap {
  73. String(validatingUTF8: $0!)
  74. }
  75. }
  76. extension CodeGenerator {
  77. var programName: String {
  78. guard let name = CommandLine.safeArguments.first?.split(separator: "/").last else {
  79. return "<plugin>"
  80. }
  81. return String(name)
  82. }
  83. /// Runs as a protocol buffer compiler plugin based on the given arguments
  84. /// or falls back to `CommandLine.arguments`.
  85. public func main(_ args: [String]?) {
  86. let args = args ?? Array(CommandLine.safeArguments.dropFirst())
  87. for arg in args {
  88. if arg == "--version", let version = version {
  89. print("\(programName) \(version)")
  90. return
  91. }
  92. if arg == "-h" || arg == "--help" {
  93. printHelp()
  94. return
  95. }
  96. // Could look at bringing back the support for recorded requests, but
  97. // haven't needed it in a long time.
  98. var stderr = StandardErrorOutputStream()
  99. print("Unknown argument: \(arg)", to: &stderr)
  100. return
  101. }
  102. var extensionMap = SimpleExtensionMap()
  103. if !customOptionExtensions.isEmpty {
  104. for e in customOptionExtensions {
  105. // Don't include Google_Protobuf_FeatureSet, that will be handing via custom features.
  106. precondition(e.messageType == Google_Protobuf_EnumOptions.self ||
  107. e.messageType == Google_Protobuf_EnumValueOptions.self ||
  108. e.messageType == Google_Protobuf_ExtensionRangeOptions.self ||
  109. e.messageType == Google_Protobuf_FieldOptions.self ||
  110. e.messageType == Google_Protobuf_FileOptions.self ||
  111. e.messageType == Google_Protobuf_MessageOptions.self ||
  112. e.messageType == Google_Protobuf_MethodOptions.self ||
  113. e.messageType == Google_Protobuf_OneofOptions.self ||
  114. e.messageType == Google_Protobuf_ServiceOptions.self,
  115. "CodeGenerator `customOptionExtensions` must only extend the descriptor.proto 'Options' messages \(e.messageType).")
  116. }
  117. extensionMap.insert(contentsOf: customOptionExtensions)
  118. }
  119. let response: Google_Protobuf_Compiler_CodeGeneratorResponse
  120. do {
  121. let request = try Google_Protobuf_Compiler_CodeGeneratorRequest(
  122. serializedBytes: FileHandle.standardInput.readDataToEndOfFile(),
  123. extensions: extensionMap
  124. )
  125. response = generateCode(request: request, generator: self)
  126. } catch let e {
  127. response = Google_Protobuf_Compiler_CodeGeneratorResponse(
  128. error: "Received an unparsable request from the compiler: \(e)")
  129. }
  130. let serializedResponse: Data
  131. do {
  132. serializedResponse = try response.serializedBytes()
  133. } catch let e {
  134. var stderr = StandardErrorOutputStream()
  135. print("\(programName): Failure while serializing response: \(e)", to: &stderr)
  136. return
  137. }
  138. FileHandle.standardOutput.write(serializedResponse)
  139. }
  140. /// Runs as a protocol buffer compiler plugin; reading the generation request
  141. /// off stdin and sending the response on stdout.
  142. ///
  143. /// Instead of calling this, just add `@main` to your `CodeGenerator`.
  144. public static func main() {
  145. let generator = Self()
  146. generator.main(nil)
  147. }
  148. }
  149. // Provide default implementation for things so `CodeGenerator`s only have to
  150. // provide them if they wish too.
  151. extension CodeGenerator {
  152. public var supportedEditionRange: ClosedRange<Google_Protobuf_Edition> {
  153. // Default impl of unknown so generator don't have to provide this until
  154. // they support editions.
  155. return Google_Protobuf_Edition.unknown...Google_Protobuf_Edition.unknown
  156. }
  157. public var customOptionExtensions: [any AnyMessageExtension] { return [] }
  158. public var version: String? { return nil }
  159. public var projectURL: String? { return nil }
  160. public var copyrightLine: String? { return nil }
  161. public func printHelp() {
  162. print("\(programName): A plugin for protoc and should not normally be run directly.")
  163. if let copyright = copyrightLine {
  164. print("\(copyright)")
  165. }
  166. if let projectURL = projectURL {
  167. print(
  168. """
  169. For more information on the usage of this plugin, please see:
  170. \(projectURL)
  171. """)
  172. }
  173. }
  174. }
  175. /// Uses the given `Google_Protobuf_Compiler_CodeGeneratorRequest` and
  176. /// `CodeGenerator` to get code generated and create the
  177. /// `Google_Protobuf_Compiler_CodeGeneratorResponse`. If there is a failure,
  178. /// the failure will be used in the response to be returned to the protocol
  179. /// buffer compiler to then be reported.
  180. ///
  181. /// - Parameters:
  182. /// - request: The request proto as generated by the protocol buffer compiler.
  183. /// - geneator: The `CodeGenerator` to use for generation.
  184. ///
  185. /// - Returns a filled out response with the success or failure of the
  186. /// generation.
  187. public func generateCode(
  188. request: Google_Protobuf_Compiler_CodeGeneratorRequest,
  189. generator: any CodeGenerator
  190. ) -> Google_Protobuf_Compiler_CodeGeneratorResponse {
  191. // TODO: This will need update to language specific features.
  192. let descriptorSet = DescriptorSet(protos: request.protoFile)
  193. var files = [FileDescriptor]()
  194. for name in request.fileToGenerate {
  195. guard let fileDescriptor = descriptorSet.fileDescriptor(named: name) else {
  196. return Google_Protobuf_Compiler_CodeGeneratorResponse(
  197. error:
  198. "protoc asked plugin to generate a file but did not provide a descriptor for the file: \(name)"
  199. )
  200. }
  201. files.append(fileDescriptor)
  202. }
  203. let context = InternalProtoCompilerContext(request: request)
  204. let outputs = InternalGeneratorOutputs()
  205. let parameter = InternalCodeGeneratorParameter(request.parameter)
  206. do {
  207. try generator.generate(
  208. files: files, parameter: parameter, protoCompilerContext: context,
  209. generatorOutputs: outputs)
  210. } catch let e {
  211. return Google_Protobuf_Compiler_CodeGeneratorResponse(error: String(describing: e))
  212. }
  213. var response = Google_Protobuf_Compiler_CodeGeneratorResponse()
  214. response.file = outputs.files
  215. // TODO: Could supportedFeatures be completely handled within library?
  216. // - The only "hard" part around hiding the proto3 optional support is making
  217. // sure the oneof index related bits aren't leaked from FieldDescriptors.
  218. // Otherwise the oneof related apis could likely take over the "realOneof"
  219. // jobs and just never vend the synthetic information.
  220. // - The editions support bit likely could be computed based on the values
  221. // `supportedEditionRange` having been overridden.
  222. let supportedFeatures = generator.supportedFeatures
  223. response.supportedFeatures = supportedFeatures.reduce(0) { $0 | UInt64($1.rawValue) }
  224. if supportedFeatures.contains(.supportsEditions) {
  225. let supportedEditions = generator.supportedEditionRange
  226. precondition(supportedEditions.upperBound != .unknown,
  227. "For a CodeGenerator to support Editions, it must override `supportedEditionRange`")
  228. precondition(DescriptorSet.bundledEditionsSupport.contains(supportedEditions.lowerBound),
  229. "A CodeGenerator can't claim to support an Edition before what the library supports: \(supportedEditions.lowerBound) vs \(DescriptorSet.bundledEditionsSupport)")
  230. precondition(DescriptorSet.bundledEditionsSupport.contains(supportedEditions.upperBound),
  231. "A CodeGenerator can't claim to support an Edition after what the library supports: \(supportedEditions.upperBound) vs \(DescriptorSet.bundledEditionsSupport)")
  232. response.minimumEdition = Int32(supportedEditions.lowerBound.rawValue)
  233. response.maximumEdition = Int32(supportedEditions.upperBound.rawValue)
  234. }
  235. return response
  236. }
  237. // MARK: Internal supporting types
  238. /// Internal implementation of `CodeGeneratorParameter` for
  239. /// `generateCode(request:generator:)`
  240. struct InternalCodeGeneratorParameter: CodeGeneratorParameter {
  241. let parameter: String
  242. init(_ parameter: String) {
  243. self.parameter = parameter
  244. }
  245. var parsedPairs: [(key: String, value: String)] {
  246. guard !parameter.isEmpty else {
  247. return []
  248. }
  249. let parts = parameter.components(separatedBy: ",")
  250. return parts.map { s -> (key: String, value: String) in
  251. guard let index = s.range(of: "=")?.lowerBound else {
  252. // Key only, no value ("baz" in example).
  253. return (trimWhitespace(s), "")
  254. }
  255. return (
  256. key: trimWhitespace(s[..<index]),
  257. value: trimWhitespace(s[s.index(after: index)...])
  258. )
  259. }
  260. }
  261. }
  262. /// Internal implementation of `ProtoCompilerContext` for
  263. /// `generateCode(request:generator:)`
  264. private struct InternalProtoCompilerContext: ProtoCompilerContext {
  265. let version: Google_Protobuf_Compiler_Version?
  266. init(request: Google_Protobuf_Compiler_CodeGeneratorRequest) {
  267. self.version = request.hasCompilerVersion ? request.compilerVersion : nil
  268. }
  269. }
  270. /// Internal implementation of `GeneratorOutputs` for
  271. /// `generateCode(request:generator:)`
  272. private final class InternalGeneratorOutputs: GeneratorOutputs {
  273. enum OutputError: Error, CustomStringConvertible {
  274. /// Attempt to add two files with the same name.
  275. case duplicateName(String)
  276. var description: String {
  277. switch self {
  278. case .duplicateName(let name):
  279. return "Generator tried to generate two files named \(name)."
  280. }
  281. }
  282. }
  283. var files: [Google_Protobuf_Compiler_CodeGeneratorResponse.File] = []
  284. private var fileNames: Set<String> = []
  285. func add(fileName: String, contents: String) throws {
  286. guard !fileNames.contains(fileName) else {
  287. throw OutputError.duplicateName(fileName)
  288. }
  289. fileNames.insert(fileName)
  290. files.append(
  291. Google_Protobuf_Compiler_CodeGeneratorResponse.File(
  292. name: fileName,
  293. content: contents))
  294. }
  295. }