CodeGenerator.swift 14 KB

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