ProtoFileToModuleMappings.swift 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. // Sources/SwiftProtobufPluginLibrary/ProtoPathModuleMappings.swift - Helpers for module mappings option
  2. //
  3. // Copyright (c) 2014 - 2017 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. /// Helper handling proto file to module mappings.
  12. ///
  13. // -----------------------------------------------------------------------------
  14. import Foundation
  15. private let defaultSwiftProtobufModuleName = "SwiftProtobuf"
  16. /// Handles the mapping of proto files to the modules they will be compiled into.
  17. public struct ProtoFileToModuleMappings {
  18. /// Errors raised from parsing mappings
  19. public enum LoadError: Error, Equatable {
  20. /// Raised if the path wasn't found.
  21. case failToOpen(path: String)
  22. /// Raised if an mapping entry in the protobuf doesn't have a module name.
  23. /// mappingIndex is the index (0-N) of the mapping.
  24. case entryMissingModuleName(mappingIndex: Int)
  25. /// Raised if an mapping entry in the protobuf doesn't have any proto files listed.
  26. /// mappingIndex is the index (0-N) of the mapping.
  27. case entryHasNoProtoPaths(mappingIndex: Int)
  28. /// The given proto path was listed for both modules.
  29. case duplicateProtoPathMapping(path: String, firstModule: String, secondModule: String)
  30. }
  31. /// Proto file name to module name.
  32. /// This is really `private` to this type, it is just `internal` so the tests can
  33. /// access it to verify things.
  34. let mappings: [String:String]
  35. /// A Boolean value that indicates that there were developer provided
  36. /// mappings.
  37. ///
  38. /// Since `mappings` will have the bundled proto files also, this is used
  39. /// to track whether there are any provided mappings.
  40. public let hasMappings: Bool
  41. /// The name of the runtime module for SwiftProtobuf (usually "SwiftProtobuf").
  42. /// We expect to find the WKTs in the module named here.
  43. public let swiftProtobufModuleName: String
  44. /// Loads and parses the given module mapping from disk. Raises LoadError
  45. /// or TextFormatDecodingError.
  46. public init(path: String) throws {
  47. try self.init(path: path, swiftProtobufModuleName: nil)
  48. }
  49. /// Loads and parses the given module mapping from disk. Raises LoadError
  50. /// or TextFormatDecodingError.
  51. public init(path: String, swiftProtobufModuleName: String?) throws {
  52. let content: String
  53. do {
  54. content = try String(contentsOfFile: path, encoding: String.Encoding.utf8)
  55. } catch {
  56. throw LoadError.failToOpen(path: path)
  57. }
  58. let mappingsProto = try SwiftProtobuf_GenSwift_ModuleMappings(textFormatString: content)
  59. try self.init(moduleMappingsProto: mappingsProto, swiftProtobufModuleName: swiftProtobufModuleName)
  60. }
  61. /// Parses the given module mapping. Raises LoadError.
  62. public init(moduleMappingsProto mappings: SwiftProtobuf_GenSwift_ModuleMappings) throws {
  63. try self.init(moduleMappingsProto: mappings, swiftProtobufModuleName: nil)
  64. }
  65. /// Parses the given module mapping. Raises LoadError.
  66. public init(moduleMappingsProto mappings: SwiftProtobuf_GenSwift_ModuleMappings, swiftProtobufModuleName: String?) throws {
  67. self.swiftProtobufModuleName = swiftProtobufModuleName ?? defaultSwiftProtobufModuleName
  68. var builder = wktMappings(swiftProtobufModuleName: self.swiftProtobufModuleName)
  69. let initialCount = builder.count
  70. for (idx, mapping) in mappings.mapping.lazy.enumerated() {
  71. if mapping.moduleName.isEmpty {
  72. throw LoadError.entryMissingModuleName(mappingIndex: idx)
  73. }
  74. if mapping.protoFilePath.isEmpty {
  75. throw LoadError.entryHasNoProtoPaths(mappingIndex: idx)
  76. }
  77. for path in mapping.protoFilePath {
  78. if let existing = builder[path] {
  79. if existing != mapping.moduleName {
  80. throw LoadError.duplicateProtoPathMapping(path: path,
  81. firstModule: existing,
  82. secondModule: mapping.moduleName)
  83. }
  84. // Was a repeat, just allow it.
  85. } else {
  86. builder[path] = mapping.moduleName
  87. }
  88. }
  89. }
  90. self.mappings = builder
  91. self.hasMappings = initialCount != builder.count
  92. }
  93. public init() {
  94. try! self.init(moduleMappingsProto: SwiftProtobuf_GenSwift_ModuleMappings(), swiftProtobufModuleName: nil)
  95. }
  96. public init(swiftProtobufModuleName: String?) {
  97. try! self.init(moduleMappingsProto: SwiftProtobuf_GenSwift_ModuleMappings(), swiftProtobufModuleName: swiftProtobufModuleName)
  98. }
  99. /// Looks up the module a given file is in.
  100. public func moduleName(forFile file: FileDescriptor) -> String? {
  101. return mappings[file.name]
  102. }
  103. /// Returns the list of modules that need to be imported for a given file based on
  104. /// the dependencies it has.
  105. public func neededModules(forFile file: FileDescriptor) -> [String]? {
  106. guard hasMappings else { return nil }
  107. if file.dependencies.isEmpty {
  108. return nil
  109. }
  110. var collector = Set<String>()
  111. for dependency in file.dependencies {
  112. if let depModule = mappings[dependency.name] {
  113. collector.insert(depModule)
  114. }
  115. }
  116. // NOTE: This api is only used by gRPC (or things like it), with
  117. // `import public` now re-exporting things, this likely can go away or just
  118. // be reduced just the above loop, without the need for special casing the
  119. // `import public` cases. It will come down to what should expectations
  120. // be for protobuf messages, enums, and extensions with repsect to something
  121. // that generates on top if it. i.e. - should they re-export things or
  122. // should only the generated proto code do it?
  123. // Protocol Buffers has the concept of "public imports", these are imports
  124. // into a file that expose everything from within the file to the new
  125. // context. From the docs -
  126. // https://protobuf.dev/programming-guides/proto/#importing
  127. // `import public` dependencies can be transitively relied upon by anyone
  128. // importing the proto containing the import public statement.
  129. // To properly expose the types for use, it means in each file, the public imports
  130. // from the dependencies have to be hoisted and also imported.
  131. var visited = Set<String>()
  132. var toScan = file.dependencies
  133. while let dep = toScan.popLast() {
  134. for pubDep in dep.publicDependencies {
  135. let pubDepName = pubDep.name
  136. if visited.contains(pubDepName) { continue }
  137. visited.insert(pubDepName)
  138. toScan.append(pubDep)
  139. if let pubDepModule = mappings[pubDepName] {
  140. collector.insert(pubDepModule)
  141. }
  142. }
  143. }
  144. if let moduleForThisFile = mappings[file.name] {
  145. collector.remove(moduleForThisFile)
  146. }
  147. // The library itself (happens if the import one of the WKTs).
  148. collector.remove(self.swiftProtobufModuleName)
  149. if collector.isEmpty {
  150. return nil
  151. }
  152. return collector.sorted()
  153. }
  154. }
  155. // Used to seed the mappings, the wkt are all part of the main library.
  156. private func wktMappings(swiftProtobufModuleName: String) -> [String:String] {
  157. return SwiftProtobufInfo.bundledProtoFiles.reduce(into: [:]) { $0[$1] = swiftProtobufModuleName }
  158. }