diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5eb529d8..fb3c62424 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,6 +102,9 @@ jobs: - name: Test SPM plugin working-directory: main run: make test-spm-plugin PROTOC=../protobuf/cmake_build/protoc + - name: Compilation Tests + working-directory: main + run: make compile-tests PROTOC=../protobuf/cmake_build/protoc sanitizer_testing: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index ff388b930..941c0c823 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ xcbaselines /docs /build mined_words.txt +/CompileTests/MultiModule/.build +/CompileTests/MultiModule/.swiftpm /*DescriptorTestData.bin /Package.resolved diff --git a/CompileTests/MultiModule/Package.swift b/CompileTests/MultiModule/Package.swift new file mode 100644 index 000000000..4be7b44d2 --- /dev/null +++ b/CompileTests/MultiModule/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version: 5.6 + +import PackageDescription + +let package = Package( + name: "CompileTests", + dependencies: [ + .package(name: "swift-protobuf", path: "../..") + ], + targets: [ + .testTarget( + name: "Test1", + dependencies: ["ImportsAPublicly"] + ), + .testTarget( + name: "Test2", + dependencies: ["ImportsImportsAPublicly"] + ), + .target( + name: "ModuleA", + dependencies: [ + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + ] + ), + .target( + name: "ImportsAPublicly", + dependencies: [ + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .target(name: "ModuleA"), + ] + ), + .target( + name: "ImportsImportsAPublicly", + dependencies: [ + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .target(name: "ImportsAPublicly"), + ] + ), + ] +) diff --git a/CompileTests/MultiModule/README.md b/CompileTests/MultiModule/README.md new file mode 100644 index 000000000..245e25c16 --- /dev/null +++ b/CompileTests/MultiModule/README.md @@ -0,0 +1,7 @@ +# CompileTests/MultiModule + +This is a test case that uses sources generated into multiple modules and +ensures the generated code compiles with the cross modules references. + +This can't use the SwiftPM Plugin as that currently doesn't have support for the +module mappings. diff --git a/CompileTests/MultiModule/Sources/ImportsAPublicly/imports_a_publicly.pb.swift b/CompileTests/MultiModule/Sources/ImportsAPublicly/imports_a_publicly.pb.swift new file mode 100644 index 000000000..99f4f3e88 --- /dev/null +++ b/CompileTests/MultiModule/Sources/ImportsAPublicly/imports_a_publicly.pb.swift @@ -0,0 +1,106 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Sources/ImportsAPublicly/imports_a_publicly.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// Use of 'import public' causes re-exports: +@_exported import enum ModuleA.E +@_exported import let ModuleA.Extensions_ext_str +@_exported import struct ModuleA.A + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public struct ImportsAPublicly: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var a: ModuleA.A { + get {return _a ?? ModuleA.A()} + set {_a = newValue} + } + /// Returns true if `a` has been explicitly set. + public var hasA: Bool {return self._a != nil} + /// Clears the value of `a`. Subsequent reads from it will return its default value. + public mutating func clearA() {self._a = nil} + + public var e: ModuleA.E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _a: ModuleA.A? = nil + fileprivate var _e: ModuleA.E? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension ImportsAPublicly: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "ImportsAPublicly" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 11: .same(proto: "a"), + 12: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if let v = self._a, !v.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 11: try { try decoder.decodeSingularMessageField(value: &self._a) }() + case 12: try { try decoder.decodeSingularEnumField(value: &self._e) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._a { + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + } }() + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 12) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: ImportsAPublicly, rhs: ImportsAPublicly) -> Bool { + if lhs._a != rhs._a {return false} + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/CompileTests/MultiModule/Sources/ImportsImportsAPublicly/imports_imports_a_publicly.pb.swift b/CompileTests/MultiModule/Sources/ImportsImportsAPublicly/imports_imports_a_publicly.pb.swift new file mode 100644 index 000000000..7e1ec8739 --- /dev/null +++ b/CompileTests/MultiModule/Sources/ImportsImportsAPublicly/imports_imports_a_publicly.pb.swift @@ -0,0 +1,107 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Sources/ImportsImportsAPublicly/imports_imports_a_publicly.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// Use of 'import public' causes re-exports: +@_exported import enum ModuleA.E +@_exported import let ModuleA.Extensions_ext_str +@_exported import struct ImportsAPublicly.ImportsAPublicly +@_exported import struct ModuleA.A + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public struct ImportsImportsAPublicly: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var a: ModuleA.A { + get {return _a ?? ModuleA.A()} + set {_a = newValue} + } + /// Returns true if `a` has been explicitly set. + public var hasA: Bool {return self._a != nil} + /// Clears the value of `a`. Subsequent reads from it will return its default value. + public mutating func clearA() {self._a = nil} + + public var e: ModuleA.E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _a: ModuleA.A? = nil + fileprivate var _e: ModuleA.E? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension ImportsImportsAPublicly: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "ImportsImportsAPublicly" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 21: .same(proto: "a"), + 22: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if let v = self._a, !v.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 21: try { try decoder.decodeSingularMessageField(value: &self._a) }() + case 22: try { try decoder.decodeSingularEnumField(value: &self._e) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._a { + try visitor.visitSingularMessageField(value: v, fieldNumber: 21) + } }() + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 22) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: ImportsImportsAPublicly, rhs: ImportsImportsAPublicly) -> Bool { + if lhs._a != rhs._a {return false} + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/CompileTests/MultiModule/Sources/ModuleA/a.pb.swift b/CompileTests/MultiModule/Sources/ModuleA/a.pb.swift new file mode 100644 index 000000000..165a10475 --- /dev/null +++ b/CompileTests/MultiModule/Sources/ModuleA/a.pb.swift @@ -0,0 +1,174 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Sources/ModuleA/a.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public enum E: SwiftProtobuf.Enum { + public typealias RawValue = Int + case unset // = 0 + case a // = 1 + case b // = 2 + + public init() { + self = .unset + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .unset + case 1: self = .a + case 2: self = .b + default: return nil + } + } + + public var rawValue: Int { + switch self { + case .unset: return 0 + case .a: return 1 + case .b: return 2 + } + } + +} + +public struct A: SwiftProtobuf.ExtensibleMessage, Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var e: E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + public var _protobuf_extensionFieldValues = SwiftProtobuf.ExtensionFieldValueSet() + fileprivate var _e: E? = nil +} + +// MARK: - Extension support defined in a.proto. + +// MARK: - Extension Properties + +// Swift Extensions on the exteneded Messages to add easy access to the declared +// extension fields. The names are based on the extension field name from the proto +// declaration. To avoid naming collisions, the names are prefixed with the name of +// the scope where the extend directive occurs. + +extension A { + + public var extStr: String { + get {return getExtensionValue(ext: Extensions_ext_str) ?? String()} + set {setExtensionValue(ext: Extensions_ext_str, value: newValue)} + } + /// Returns true if extension `Extensions_ext_str` + /// has been explicitly set. + public var hasExtStr: Bool { + return hasExtensionValue(ext: Extensions_ext_str) + } + /// Clears the value of extension `Extensions_ext_str`. + /// Subsequent reads from it will return its default value. + public mutating func clearExtStr() { + clearExtensionValue(ext: Extensions_ext_str) + } + +} + +// MARK: - File's ExtensionMap: A_Extensions + +/// A `SwiftProtobuf.SimpleExtensionMap` that includes all of the extensions defined by +/// this .proto file. It can be used any place an `SwiftProtobuf.ExtensionMap` is needed +/// in parsing, or it can be combined with other `SwiftProtobuf.SimpleExtensionMap`s to create +/// a larger `SwiftProtobuf.SimpleExtensionMap`. +public let A_Extensions: SwiftProtobuf.SimpleExtensionMap = [ + Extensions_ext_str +] + +// Extension Objects - The only reason these might be needed is when manually +// constructing a `SimpleExtensionMap`, otherwise, use the above _Extension Properties_ +// accessors for the extension fields on the messages directly. + +public let Extensions_ext_str = SwiftProtobuf.MessageExtension, A>( + _protobuf_fieldNumber: 100, + fieldName: "ext_str" +) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension E: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "E_UNSET"), + 1: .same(proto: "E_A"), + 2: .same(proto: "E_B"), + ] +} + +extension A: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "A" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if !_protobuf_extensionFieldValues.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self._e) }() + case 100..<1001: + try { try decoder.decodeExtensionField(values: &_protobuf_extensionFieldValues, messageType: A.self, fieldNumber: fieldNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 1) + } }() + try visitor.visitExtensionFields(fields: _protobuf_extensionFieldValues, start: 100, end: 1001) + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: A, rhs: A) -> Bool { + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + if lhs._protobuf_extensionFieldValues != rhs._protobuf_extensionFieldValues {return false} + return true + } +} diff --git a/CompileTests/MultiModule/Tests/Test1/test1.swift b/CompileTests/MultiModule/Tests/Test1/test1.swift new file mode 100644 index 000000000..474a7c09c --- /dev/null +++ b/CompileTests/MultiModule/Tests/Test1/test1.swift @@ -0,0 +1,31 @@ +import ImportsAPublicly +// Don't need to import ModuleA because of the file being a `import public` + +import XCTest + +final class ExampleTests: XCTestCase { + func testA() { + let anA = A.with { $0.e = .a } + XCTAssertEqual(anA.e, .a) + } + + func testImportsAPublicly() { + let imports = ImportsAPublicly.with { $0.a.e = .a } + XCTAssertEqual(imports.a.e, .a) + } + + func testInterop() { + let anA = A.with { $0.e = .b } + let imports = ImportsAPublicly.with { + $0.a = anA + $0.e = .b + } + XCTAssertEqual(imports.a.e, imports.e) + let transtively = UsesATransitively.with { + $0.a = anA + $0.e = imports.e + } + XCTAssertEqual(transtively.a, anA) + XCTAssertEqual(transtively.e, imports.e) + } +} diff --git a/CompileTests/MultiModule/Tests/Test1/uses_a_transitively.pb.swift b/CompileTests/MultiModule/Tests/Test1/uses_a_transitively.pb.swift new file mode 100644 index 000000000..e88624091 --- /dev/null +++ b/CompileTests/MultiModule/Tests/Test1/uses_a_transitively.pb.swift @@ -0,0 +1,103 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Tests/Test1/uses_a_transitively.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +import ImportsAPublicly + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public struct UsesATransitively: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var a: ModuleA.A { + get {return _a ?? ModuleA.A()} + set {_a = newValue} + } + /// Returns true if `a` has been explicitly set. + public var hasA: Bool {return self._a != nil} + /// Clears the value of `a`. Subsequent reads from it will return its default value. + public mutating func clearA() {self._a = nil} + + public var e: ModuleA.E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _a: ModuleA.A? = nil + fileprivate var _e: ModuleA.E? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension UsesATransitively: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "UsesATransitively" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 101: .same(proto: "a"), + 102: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if let v = self._a, !v.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 101: try { try decoder.decodeSingularMessageField(value: &self._a) }() + case 102: try { try decoder.decodeSingularEnumField(value: &self._e) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._a { + try visitor.visitSingularMessageField(value: v, fieldNumber: 101) + } }() + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 102) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: UsesATransitively, rhs: UsesATransitively) -> Bool { + if lhs._a != rhs._a {return false} + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/CompileTests/MultiModule/Tests/Test2/test2.swift b/CompileTests/MultiModule/Tests/Test2/test2.swift new file mode 100644 index 000000000..45d62fbb5 --- /dev/null +++ b/CompileTests/MultiModule/Tests/Test2/test2.swift @@ -0,0 +1,31 @@ +import ImportsImportsAPublicly +// Don't need to import ModuleA because of the file being a `import public` + +import XCTest + +final class ExampleTests: XCTestCase { + func testA() { + let anA = A.with { $0.e = .a } + XCTAssertEqual(anA.e, .a) + } + + func testImportsImportsAPublicly() { + let imports = ImportsImportsAPublicly.with { $0.a.e = .a } + XCTAssertEqual(imports.a.e, .a) + } + + func testInterop() { + let anA = A.with { $0.e = .b } + let imports = ImportsImportsAPublicly.with { + $0.a = anA + $0.e = .b + } + XCTAssertEqual(imports.a.e, imports.e) + let transtively = UsesATransitively2.with { + $0.a = anA + $0.e = imports.e + } + XCTAssertEqual(transtively.a, anA) + XCTAssertEqual(transtively.e, imports.e) + } +} diff --git a/CompileTests/MultiModule/Tests/Test2/uses_a_transitively2.pb.swift b/CompileTests/MultiModule/Tests/Test2/uses_a_transitively2.pb.swift new file mode 100644 index 000000000..6c056cb8a --- /dev/null +++ b/CompileTests/MultiModule/Tests/Test2/uses_a_transitively2.pb.swift @@ -0,0 +1,103 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Tests/Test2/uses_a_transitively2.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +import ImportsImportsAPublicly + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public struct UsesATransitively2: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var a: ModuleA.A { + get {return _a ?? ModuleA.A()} + set {_a = newValue} + } + /// Returns true if `a` has been explicitly set. + public var hasA: Bool {return self._a != nil} + /// Clears the value of `a`. Subsequent reads from it will return its default value. + public mutating func clearA() {self._a = nil} + + public var e: ModuleA.E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _a: ModuleA.A? = nil + fileprivate var _e: ModuleA.E? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension UsesATransitively2: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "UsesATransitively2" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 111: .same(proto: "a"), + 122: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if let v = self._a, !v.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 111: try { try decoder.decodeSingularMessageField(value: &self._a) }() + case 122: try { try decoder.decodeSingularEnumField(value: &self._e) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._a { + try visitor.visitSingularMessageField(value: v, fieldNumber: 111) + } }() + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 122) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: UsesATransitively2, rhs: UsesATransitively2) -> Bool { + if lhs._a != rhs._a {return false} + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Makefile b/Makefile index 8c7eee166..51c14fa7e 100644 --- a/Makefile +++ b/Makefile @@ -77,12 +77,16 @@ PROTOS_DIRS=Conformance protoc-gen-swiftTests SwiftProtobuf SwiftProtobufPluginL check-proto-files \ check-version-numbers \ clean \ + compile-tests \ + compile-tests-multimodule \ default \ docs \ install \ pod-lib-lint \ reference \ regenerate \ + regenerate-compiletests-multimodule-protos \ + regenerate-compiletests-protos \ regenerate-conformance-protos \ regenerate-fuzz-protos \ regenerate-library-protos \ @@ -93,6 +97,7 @@ PROTOS_DIRS=Conformance protoc-gen-swiftTests SwiftProtobuf SwiftProtobufPluginL test-everything \ test-plugin \ test-runtime \ + test-spm-plugin \ update-proto-files default: build @@ -182,11 +187,26 @@ test-plugin: build ${PROTOC_GEN_SWIFT} --tfiws_out=_test/$$d \ `find Protos/$$d -type f -name "*.proto"` || exit 1; \ done + @mkdir -p _test/CompileTests/MultiModule + ${GENERATE_SRCS} \ + -I Protos/CompileTests/MultiModule \ + --tfiws_opt=Visibility=Public \ + --tfiws_opt=ProtoPathModuleMappings=Protos/CompileTests/MultiModule/module_mappings.pbascii \ + --tfiws_out=_test/CompileTests/MultiModule \ + `(find Protos/CompileTests/MultiModule -type f -name "*.proto")` diff -ru _test Reference # Test the SPM plugin. test-spm-plugin: - env PROTOC_PATH=$(shell realpath ${PROTOC}) swift test --package-path PluginExamples + env PROTOC_PATH=$(shell realpath ${PROTOC}) ${SWIFT} test --package-path PluginExamples + +compile-tests: compile-tests-multimodule + +# Test that ensure generating public into multiple modules with `import public` +# yields buildable code. +compile-tests-multimodule: + ${SWIFT} test --package-path CompileTests/MultiModule + # Rebuild the reference files by running the local version of protoc-gen-swift # against our menagerie of sample protos. @@ -213,6 +233,13 @@ reference: build ${PROTOC_GEN_SWIFT} --tfiws_out=Reference/$$d \ `find Protos/$$d -type f -name "*.proto"` || exit 1; \ done + @mkdir -p Reference/CompileTests/MultiModule + ${GENERATE_SRCS} \ + -I Protos/CompileTests/MultiModule \ + --tfiws_opt=Visibility=Public \ + --tfiws_opt=ProtoPathModuleMappings=Protos/CompileTests/MultiModule/module_mappings.pbascii \ + --tfiws_out=Reference/CompileTests/MultiModule \ + `(find Protos/CompileTests/MultiModule -type f -name "*.proto")` # # Rebuild the generated .pb.swift test files by running @@ -228,6 +255,7 @@ regenerate: \ regenerate-fuzz-protos \ regenerate-plugin-protos \ regenerate-test-protos \ + regenerate-compiletests-protos \ regenerate-conformance-protos \ Tests/protoc-gen-swiftTests/DescriptorTestData.swift \ Tests/SwiftProtobufPluginLibraryTests/DescriptorTestData.swift @@ -419,6 +447,21 @@ regenerate-conformance-protos: build ${PROTOC_GEN_SWIFT} --tfiws_out=Sources/Conformance \ `find Protos/Conformance -type f -name "*.proto"` +# Rebuild just the protos used by the CompileTests. +regenerate-compiletests-protos: regenerate-compiletests-multimodule-protos + +# Update the CompileTests/MultiModule files. +# NOTE: Any changes here must be done of the "test-plugin" target so it +# generates in the same way. +regenerate-compiletests-multimodule-protos: build ${PROTOC_GEN_SWIFT} + find CompileTests/MultiModule -name "*.pb.swift" -exec rm -f {} \; + ${GENERATE_SRCS} \ + -I Protos/CompileTests/MultiModule \ + --tfiws_opt=Visibility=Public \ + --tfiws_opt=ProtoPathModuleMappings=Protos/CompileTests/MultiModule/module_mappings.pbascii \ + --tfiws_out=CompileTests/MultiModule \ + `(find Protos/CompileTests/MultiModule -type f -name "*.proto")` + # Helper to check if there is a protobuf checkout as expected. check-for-protobuf-checkout: @if [ ! -d "${GOOGLE_PROTOBUF_CHECKOUT}/src/google/protobuf" ]; then \ diff --git a/Protos/CompileTests/MultiModule/Sources/ImportsAPublicly/imports_a_publicly.proto b/Protos/CompileTests/MultiModule/Sources/ImportsAPublicly/imports_a_publicly.proto new file mode 100644 index 000000000..f8bdb3722 --- /dev/null +++ b/Protos/CompileTests/MultiModule/Sources/ImportsAPublicly/imports_a_publicly.proto @@ -0,0 +1,6 @@ +import public "Sources/ModuleA/a.proto"; + +message ImportsAPublicly { + optional A a = 11; + optional E e = 12; +} diff --git a/Protos/CompileTests/MultiModule/Sources/ImportsImportsAPublicly/imports_imports_a_publicly.proto b/Protos/CompileTests/MultiModule/Sources/ImportsImportsAPublicly/imports_imports_a_publicly.proto new file mode 100644 index 000000000..0b34fc68d --- /dev/null +++ b/Protos/CompileTests/MultiModule/Sources/ImportsImportsAPublicly/imports_imports_a_publicly.proto @@ -0,0 +1,6 @@ +import public "Sources/ImportsAPublicly/imports_a_publicly.proto"; + +message ImportsImportsAPublicly { + optional A a = 21; + optional E e = 22; +} diff --git a/Protos/CompileTests/MultiModule/Sources/ModuleA/a.proto b/Protos/CompileTests/MultiModule/Sources/ModuleA/a.proto new file mode 100644 index 000000000..75c1506a7 --- /dev/null +++ b/Protos/CompileTests/MultiModule/Sources/ModuleA/a.proto @@ -0,0 +1,14 @@ +enum E { + E_UNSET = 0; + E_A = 1; + E_B = 2; +} + +message A { + optional E e = 1; + extensions 100 to 1000; +} + +extend A { + optional string ext_str = 100; +} diff --git a/Protos/CompileTests/MultiModule/Tests/Test1/uses_a_transitively.proto b/Protos/CompileTests/MultiModule/Tests/Test1/uses_a_transitively.proto new file mode 100644 index 000000000..f8c2c3a80 --- /dev/null +++ b/Protos/CompileTests/MultiModule/Tests/Test1/uses_a_transitively.proto @@ -0,0 +1,6 @@ +import "Sources/ImportsAPublicly/imports_a_publicly.proto"; + +message UsesATransitively { + optional A a = 101; + optional E e = 102; +} diff --git a/Protos/CompileTests/MultiModule/Tests/Test2/uses_a_transitively2.proto b/Protos/CompileTests/MultiModule/Tests/Test2/uses_a_transitively2.proto new file mode 100644 index 000000000..e05621dfa --- /dev/null +++ b/Protos/CompileTests/MultiModule/Tests/Test2/uses_a_transitively2.proto @@ -0,0 +1,6 @@ +import "Sources/ImportsImportsAPublicly/imports_imports_a_publicly.proto"; + +message UsesATransitively2 { + optional A a = 111; + optional E e = 122; +} diff --git a/Protos/CompileTests/MultiModule/module_mappings.pbascii b/Protos/CompileTests/MultiModule/module_mappings.pbascii new file mode 100644 index 000000000..656198148 --- /dev/null +++ b/Protos/CompileTests/MultiModule/module_mappings.pbascii @@ -0,0 +1,12 @@ +mapping { + module_name: "ModuleA" + proto_file_path: "Sources/ModuleA/a.proto" +} +mapping { + module_name: "ImportsAPublicly" + proto_file_path: "Sources/ImportsAPublicly/imports_a_publicly.proto" +} +mapping { + module_name: "ImportsImportsAPublicly" + proto_file_path: "Sources/ImportsImportsAPublicly/imports_imports_a_publicly.proto" +} diff --git a/Reference/CompileTests/MultiModule/Sources/ImportsAPublicly/imports_a_publicly.pb.swift b/Reference/CompileTests/MultiModule/Sources/ImportsAPublicly/imports_a_publicly.pb.swift new file mode 100644 index 000000000..99f4f3e88 --- /dev/null +++ b/Reference/CompileTests/MultiModule/Sources/ImportsAPublicly/imports_a_publicly.pb.swift @@ -0,0 +1,106 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Sources/ImportsAPublicly/imports_a_publicly.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// Use of 'import public' causes re-exports: +@_exported import enum ModuleA.E +@_exported import let ModuleA.Extensions_ext_str +@_exported import struct ModuleA.A + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public struct ImportsAPublicly: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var a: ModuleA.A { + get {return _a ?? ModuleA.A()} + set {_a = newValue} + } + /// Returns true if `a` has been explicitly set. + public var hasA: Bool {return self._a != nil} + /// Clears the value of `a`. Subsequent reads from it will return its default value. + public mutating func clearA() {self._a = nil} + + public var e: ModuleA.E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _a: ModuleA.A? = nil + fileprivate var _e: ModuleA.E? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension ImportsAPublicly: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "ImportsAPublicly" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 11: .same(proto: "a"), + 12: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if let v = self._a, !v.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 11: try { try decoder.decodeSingularMessageField(value: &self._a) }() + case 12: try { try decoder.decodeSingularEnumField(value: &self._e) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._a { + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + } }() + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 12) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: ImportsAPublicly, rhs: ImportsAPublicly) -> Bool { + if lhs._a != rhs._a {return false} + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Reference/CompileTests/MultiModule/Sources/ImportsImportsAPublicly/imports_imports_a_publicly.pb.swift b/Reference/CompileTests/MultiModule/Sources/ImportsImportsAPublicly/imports_imports_a_publicly.pb.swift new file mode 100644 index 000000000..7e1ec8739 --- /dev/null +++ b/Reference/CompileTests/MultiModule/Sources/ImportsImportsAPublicly/imports_imports_a_publicly.pb.swift @@ -0,0 +1,107 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Sources/ImportsImportsAPublicly/imports_imports_a_publicly.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// Use of 'import public' causes re-exports: +@_exported import enum ModuleA.E +@_exported import let ModuleA.Extensions_ext_str +@_exported import struct ImportsAPublicly.ImportsAPublicly +@_exported import struct ModuleA.A + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public struct ImportsImportsAPublicly: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var a: ModuleA.A { + get {return _a ?? ModuleA.A()} + set {_a = newValue} + } + /// Returns true if `a` has been explicitly set. + public var hasA: Bool {return self._a != nil} + /// Clears the value of `a`. Subsequent reads from it will return its default value. + public mutating func clearA() {self._a = nil} + + public var e: ModuleA.E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _a: ModuleA.A? = nil + fileprivate var _e: ModuleA.E? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension ImportsImportsAPublicly: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "ImportsImportsAPublicly" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 21: .same(proto: "a"), + 22: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if let v = self._a, !v.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 21: try { try decoder.decodeSingularMessageField(value: &self._a) }() + case 22: try { try decoder.decodeSingularEnumField(value: &self._e) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._a { + try visitor.visitSingularMessageField(value: v, fieldNumber: 21) + } }() + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 22) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: ImportsImportsAPublicly, rhs: ImportsImportsAPublicly) -> Bool { + if lhs._a != rhs._a {return false} + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Reference/CompileTests/MultiModule/Sources/ModuleA/a.pb.swift b/Reference/CompileTests/MultiModule/Sources/ModuleA/a.pb.swift new file mode 100644 index 000000000..165a10475 --- /dev/null +++ b/Reference/CompileTests/MultiModule/Sources/ModuleA/a.pb.swift @@ -0,0 +1,174 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Sources/ModuleA/a.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public enum E: SwiftProtobuf.Enum { + public typealias RawValue = Int + case unset // = 0 + case a // = 1 + case b // = 2 + + public init() { + self = .unset + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .unset + case 1: self = .a + case 2: self = .b + default: return nil + } + } + + public var rawValue: Int { + switch self { + case .unset: return 0 + case .a: return 1 + case .b: return 2 + } + } + +} + +public struct A: SwiftProtobuf.ExtensibleMessage, Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var e: E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + public var _protobuf_extensionFieldValues = SwiftProtobuf.ExtensionFieldValueSet() + fileprivate var _e: E? = nil +} + +// MARK: - Extension support defined in a.proto. + +// MARK: - Extension Properties + +// Swift Extensions on the exteneded Messages to add easy access to the declared +// extension fields. The names are based on the extension field name from the proto +// declaration. To avoid naming collisions, the names are prefixed with the name of +// the scope where the extend directive occurs. + +extension A { + + public var extStr: String { + get {return getExtensionValue(ext: Extensions_ext_str) ?? String()} + set {setExtensionValue(ext: Extensions_ext_str, value: newValue)} + } + /// Returns true if extension `Extensions_ext_str` + /// has been explicitly set. + public var hasExtStr: Bool { + return hasExtensionValue(ext: Extensions_ext_str) + } + /// Clears the value of extension `Extensions_ext_str`. + /// Subsequent reads from it will return its default value. + public mutating func clearExtStr() { + clearExtensionValue(ext: Extensions_ext_str) + } + +} + +// MARK: - File's ExtensionMap: A_Extensions + +/// A `SwiftProtobuf.SimpleExtensionMap` that includes all of the extensions defined by +/// this .proto file. It can be used any place an `SwiftProtobuf.ExtensionMap` is needed +/// in parsing, or it can be combined with other `SwiftProtobuf.SimpleExtensionMap`s to create +/// a larger `SwiftProtobuf.SimpleExtensionMap`. +public let A_Extensions: SwiftProtobuf.SimpleExtensionMap = [ + Extensions_ext_str +] + +// Extension Objects - The only reason these might be needed is when manually +// constructing a `SimpleExtensionMap`, otherwise, use the above _Extension Properties_ +// accessors for the extension fields on the messages directly. + +public let Extensions_ext_str = SwiftProtobuf.MessageExtension, A>( + _protobuf_fieldNumber: 100, + fieldName: "ext_str" +) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension E: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "E_UNSET"), + 1: .same(proto: "E_A"), + 2: .same(proto: "E_B"), + ] +} + +extension A: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "A" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if !_protobuf_extensionFieldValues.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self._e) }() + case 100..<1001: + try { try decoder.decodeExtensionField(values: &_protobuf_extensionFieldValues, messageType: A.self, fieldNumber: fieldNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 1) + } }() + try visitor.visitExtensionFields(fields: _protobuf_extensionFieldValues, start: 100, end: 1001) + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: A, rhs: A) -> Bool { + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + if lhs._protobuf_extensionFieldValues != rhs._protobuf_extensionFieldValues {return false} + return true + } +} diff --git a/Reference/CompileTests/MultiModule/Tests/Test1/uses_a_transitively.pb.swift b/Reference/CompileTests/MultiModule/Tests/Test1/uses_a_transitively.pb.swift new file mode 100644 index 000000000..e88624091 --- /dev/null +++ b/Reference/CompileTests/MultiModule/Tests/Test1/uses_a_transitively.pb.swift @@ -0,0 +1,103 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Tests/Test1/uses_a_transitively.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +import ImportsAPublicly + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public struct UsesATransitively: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var a: ModuleA.A { + get {return _a ?? ModuleA.A()} + set {_a = newValue} + } + /// Returns true if `a` has been explicitly set. + public var hasA: Bool {return self._a != nil} + /// Clears the value of `a`. Subsequent reads from it will return its default value. + public mutating func clearA() {self._a = nil} + + public var e: ModuleA.E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _a: ModuleA.A? = nil + fileprivate var _e: ModuleA.E? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension UsesATransitively: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "UsesATransitively" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 101: .same(proto: "a"), + 102: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if let v = self._a, !v.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 101: try { try decoder.decodeSingularMessageField(value: &self._a) }() + case 102: try { try decoder.decodeSingularEnumField(value: &self._e) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._a { + try visitor.visitSingularMessageField(value: v, fieldNumber: 101) + } }() + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 102) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: UsesATransitively, rhs: UsesATransitively) -> Bool { + if lhs._a != rhs._a {return false} + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Reference/CompileTests/MultiModule/Tests/Test2/uses_a_transitively2.pb.swift b/Reference/CompileTests/MultiModule/Tests/Test2/uses_a_transitively2.pb.swift new file mode 100644 index 000000000..6c056cb8a --- /dev/null +++ b/Reference/CompileTests/MultiModule/Tests/Test2/uses_a_transitively2.pb.swift @@ -0,0 +1,103 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Tests/Test2/uses_a_transitively2.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +import ImportsImportsAPublicly + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _3: SwiftProtobuf.ProtobufAPIVersion_3 {} + typealias Version = _3 +} + +public struct UsesATransitively2: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var a: ModuleA.A { + get {return _a ?? ModuleA.A()} + set {_a = newValue} + } + /// Returns true if `a` has been explicitly set. + public var hasA: Bool {return self._a != nil} + /// Clears the value of `a`. Subsequent reads from it will return its default value. + public mutating func clearA() {self._a = nil} + + public var e: ModuleA.E { + get {return _e ?? .unset} + set {_e = newValue} + } + /// Returns true if `e` has been explicitly set. + public var hasE: Bool {return self._e != nil} + /// Clears the value of `e`. Subsequent reads from it will return its default value. + public mutating func clearE() {self._e = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _a: ModuleA.A? = nil + fileprivate var _e: ModuleA.E? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension UsesATransitively2: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = "UsesATransitively2" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 111: .same(proto: "a"), + 122: .same(proto: "e"), + ] + + public var isInitialized: Bool { + if let v = self._a, !v.isInitialized {return false} + return true + } + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 111: try { try decoder.decodeSingularMessageField(value: &self._a) }() + case 122: try { try decoder.decodeSingularEnumField(value: &self._e) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._a { + try visitor.visitSingularMessageField(value: v, fieldNumber: 111) + } }() + try { if let v = self._e { + try visitor.visitSingularEnumField(value: v, fieldNumber: 122) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: UsesATransitively2, rhs: UsesATransitively2) -> Bool { + if lhs._a != rhs._a {return false} + if lhs._e != rhs._e {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/SwiftProtobufPluginLibrary/ProtoFileToModuleMappings.swift b/Sources/SwiftProtobufPluginLibrary/ProtoFileToModuleMappings.swift index df21faf33..5b8549521 100644 --- a/Sources/SwiftProtobufPluginLibrary/ProtoFileToModuleMappings.swift +++ b/Sources/SwiftProtobufPluginLibrary/ProtoFileToModuleMappings.swift @@ -132,6 +132,14 @@ public struct ProtoFileToModuleMappings { } } + // NOTE: This api is only used by gRPC (or things like it), with + // `import public` now re-exporting things, this likely can go away or just + // be reduced just the above loop, without the need for special casing the + // `import public` cases. It will come down to what should expectations + // be for protobuf messages, enums, and extensions with repsect to something + // that generates on top if it. i.e. - should they re-export things or + // should only the generated proto code do it? + // Protocol Buffers has the concept of "public imports", these are imports // into a file that expose everything from within the file to the new // context. From the docs - diff --git a/Sources/SwiftProtobufPluginLibrary/SwiftProtobufNamer.swift b/Sources/SwiftProtobufPluginLibrary/SwiftProtobufNamer.swift index 83942d7b2..0dab8324c 100644 --- a/Sources/SwiftProtobufPluginLibrary/SwiftProtobufNamer.swift +++ b/Sources/SwiftProtobufPluginLibrary/SwiftProtobufNamer.swift @@ -17,8 +17,8 @@ import Foundation public final class SwiftProtobufNamer { var filePrefixCache = [String:String]() var enumValueRelativeNameCache = [String:String]() - var mappings: ProtoFileToModuleMappings - var targetModule: String + public let mappings: ProtoFileToModuleMappings + public let targetModule: String public var swiftProtobufModuleName: String { return mappings.swiftProtobufModuleName } diff --git a/Sources/SwiftProtobufTestHelpers/Descriptor+TestHelpers.swift b/Sources/SwiftProtobufTestHelpers/Descriptor+TestHelpers.swift index 263e988db..817b853d4 100644 --- a/Sources/SwiftProtobufTestHelpers/Descriptor+TestHelpers.swift +++ b/Sources/SwiftProtobufTestHelpers/Descriptor+TestHelpers.swift @@ -11,12 +11,13 @@ import SwiftProtobuf public extension Google_Protobuf_FileDescriptorProto { - init(name: String, dependencies: [String] = [], publicDependencies: [Int32] = []) { + init(name: String, dependencies: [String] = [], publicDependencies: [Int32] = [], package: String = "") { for idx in publicDependencies { precondition(Int(idx) <= dependencies.count) } self.init() self.name = name - dependency = dependencies - publicDependency = publicDependencies + self.dependency = dependencies + self.publicDependency = publicDependencies + self.package = package } init(textFormatStrings: [String]) throws { let s = textFormatStrings.joined(separator: "\n") + "\n" @@ -27,7 +28,7 @@ public extension Google_Protobuf_FileDescriptorProto { public extension Google_Protobuf_FileDescriptorSet { init(files: [Google_Protobuf_FileDescriptorProto]) { self.init() - file = files + self.file = files } init(file: Google_Protobuf_FileDescriptorProto) { self.init() diff --git a/Sources/protoc-gen-swift/Descriptor+Extensions.swift b/Sources/protoc-gen-swift/Descriptor+Extensions.swift index 0cb60effe..0aaa00e67 100644 --- a/Sources/protoc-gen-swift/Descriptor+Extensions.swift +++ b/Sources/protoc-gen-swift/Descriptor+Extensions.swift @@ -14,6 +14,150 @@ extension FileDescriptor { var isBundledProto: Bool { return SwiftProtobufInfo.isBundledProto(file: self) } + + // Returns a string of any import lines for the give file based on the file's + // imports. The string may include multiple lines. + // + // Protocol Buffers has the concept of "public imports", these are imports + // into a file that expose everything from within the file to the new + // context. From the docs - + // https://protobuf.dev/programming-guides/proto/#importing + // `import public` dependencies can be transitively relied upon by anyone + // importing the proto containing the import public statement. + // To properly expose the types for use, it means in each file, the public + // imports from the dependencies (recursively) have to be hoisted and + // reexported. This way someone importing a given module still sees the type + // even when moved. + // + // NOTE: There is a weakness for Swift with protobuf extensions. To make + // the protobuf extensions easier to use, a Swift extension is declared with + // field exposed as a property on the exteneded message. There is no way + // to reexport the Swift `extension` and/or added properties. But the raw + // types are re-exported to minimize the breaking of code if a type is moved + // between files/modules. + // + // `reexportPublicImports` will cause the `import public` types to be + // reexported to avoid breaking downstream code using a type that might have + // moved between .proto files. + // + // `asImplementationOnly` will cause all of the import directives to be + // marked as `@_implementationOnly`. It will also cause all of the `file`'s + // `publicDependencies` to instead be recursively pulled up as direct imports + // to ensure the generate file compiles, and no `import public` files are + // re-exported. + // + // Aside: This could be moved into the plugin library, but it doesn't seem + // like anyone else would need the logic. Swift GRPC support probably stick + // with the support for the module mappings. + public func computeImports( + namer: SwiftProtobufNamer, + reexportPublicImports: Bool, + asImplementationOnly: Bool + ) -> String { + // The namer should be configured with the module this file generated for. + assert(namer.targetModule == (namer.mappings.moduleName(forFile: self) ?? "")) + // Both options can't be enabled. + assert(!reexportPublicImports || + !asImplementationOnly || + reexportPublicImports != asImplementationOnly) + + guard namer.mappings.hasMappings else { + // No module mappings? Everything must be the same module, so no Swift + // imports will be needed. + return "" + } + + if dependencies.isEmpty { + // No proto dependencies (imports), then no Swift imports will be needed. + return "" + } + + let directive = asImplementationOnly ? "@_implementationOnly import" : "import" + var imports = Set() + for dependency in dependencies { + if SwiftProtobufInfo.isBundledProto(file: dependency) { + continue // No import needed for the runtime, that's always added. + } + if reexportPublicImports && publicDependencies.contains(where: { $0 === dependency }) { + // When re-exporting, the `import public` types will be imported + // instead of importing the module. + continue + } + if let depModule = namer.mappings.moduleName(forFile: dependency), + depModule != namer.targetModule { + // Different module, import it. + imports.insert("\(directive) \(depModule)") + } + } + + // If not re-exporting imports, then there is nothing special needed for + // `import public` files, as any transitive `import public` directives + // would have already re-exported the types, so everything this file needs + // will be covered by the above imports. + let exportingImports: [String] = + reexportPublicImports ? computeSymbolReExports(namer: namer) : [String]() + + var result = imports.sorted().joined(separator: "\n") + if !exportingImports.isEmpty { + if !result.isEmpty { + result.append("\n") + } + result.append("// Use of 'import public' causes re-exports:\n") + result.append(exportingImports.sorted().joined(separator: "\n")) + } + return result + } + + // Internal helper to `computeImports(...)`. + private func computeSymbolReExports(namer: SwiftProtobufNamer) -> [String] { + var result = [String]() + + // To handle re-exporting, recuively walk all the `import public` files + // and make this module do a Swift exporting import of the specific + // symbols. That will keep any type that gets moved between .proto files + // still exposed from the same modules so as not to break developer + // authored code. + var toScan = publicDependencies + var visited = Set() + while let dependency = toScan.popLast() { + let dependencyName = dependency.name + if visited.contains(dependencyName) { continue } + visited.insert(dependencyName) + + if SwiftProtobufInfo.isBundledProto(file: dependency) { + continue // Bundlined file, nothing to do. + } + guard let depModule = namer.mappings.moduleName(forFile: dependency) else { + continue // No mapping, assume same module, nothing to do. + } + if depModule == namer.targetModule { + // Same module, nothing to do (that generated file will do any re-exports). + continue + } + + toScan.append(contentsOf: dependency.publicDependencies) + + // NOTE: This re-exports/imports from the module that defines the type. + // If Xcode/SwiftPM ever were to do some sort of "layering checks" to + // ensure there is a direct dependency on the thing being imported, this + // could be updated do the re-export/import from the middle step in + // chained imports. + + for m in dependency.messages { + result.append("@_exported import struct \(namer.fullName(message: m))") + } + for e in dependency.enums { + result.append("@_exported import enum \(namer.fullName(enum: e))") + } + // There is nothing we can do for the Swift extensions declared on the + // extended Messages, best we can do is expose the raw extenions + // themselves. + for e in dependency.extensions { + result.append("@_exported import let \(namer.fullName(extensionField: e))") + } + } + return result + } } extension Descriptor { diff --git a/Sources/protoc-gen-swift/FileGenerator.swift b/Sources/protoc-gen-swift/FileGenerator.swift index e4055805a..18e46bbe3 100644 --- a/Sources/protoc-gen-swift/FileGenerator.swift +++ b/Sources/protoc-gen-swift/FileGenerator.swift @@ -90,25 +90,20 @@ class FileGenerator { p.print("\(comments)import Foundation") - // Import all other imports as @_implementationOnly if the option is - // set, to avoid exposing internal types to users. - let visibilityAnnotation: String - if self.generatorOptions.implementationOnlyImports { - precondition(self.generatorOptions.visibility == .internal) - visibilityAnnotation = "@_implementationOnly " - } else { - visibilityAnnotation = "" - } if fileDescriptor.isBundledProto { p.print("// 'import \(namer.swiftProtobufModuleName)' suppressed, this proto file is meant to be bundled in the runtime.") } else { - p.print("\(visibilityAnnotation)import \(namer.swiftProtobufModuleName)") + let directive = self.generatorOptions.implementationOnlyImports ? "@_implementationOnly import" : "import" + p.print("\(directive) \(namer.swiftProtobufModuleName)") } - if let neededImports = generatorOptions.protoToModuleMappings.neededModules(forFile: fileDescriptor) { + + let neededImports = fileDescriptor.computeImports( + namer: namer, + reexportPublicImports: self.generatorOptions.visibility != .internal, + asImplementationOnly: self.generatorOptions.implementationOnlyImports) + if !neededImports.isEmpty { p.print() - for i in neededImports { - p.print("\(visibilityAnnotation)import \(i)") - } + p.print(neededImports) } p.print() diff --git a/Tests/protoc-gen-swiftTests/Test_DescriptorExtensions.swift b/Tests/protoc-gen-swiftTests/Test_DescriptorExtensions.swift index 712070cb1..1c2a342eb 100644 --- a/Tests/protoc-gen-swiftTests/Test_DescriptorExtensions.swift +++ b/Tests/protoc-gen-swiftTests/Test_DescriptorExtensions.swift @@ -13,6 +13,8 @@ import SwiftProtobuf import SwiftProtobufPluginLibrary @testable import protoc_gen_swift +fileprivate typealias FileDescriptorProto = Google_Protobuf_FileDescriptorProto + final class Test_DescriptorExtensions: XCTestCase { func testExtensionRanges() throws { @@ -194,4 +196,271 @@ final class Test_DescriptorExtensions: XCTestCase { XCTAssertIdentical(aliasInfo.original(of: e.values[5]), e.values[0]) } + func test_File_computeImports_noImportPublic() { + let configText = """ + mapping { module_name: "foo", proto_file_path: "file" } + mapping { module_name: "bar", proto_file_path: "dir1/file" } + mapping { module_name: "baz", proto_file_path: ["dir2/file","file4"] } + mapping { module_name: "foo", proto_file_path: "file5" } + """ + + let config = try! SwiftProtobuf_GenSwift_ModuleMappings(textFormatString: configText) + let mapper = try! ProtoFileToModuleMappings(moduleMappingsProto: config) + + let fileProtos = [ + FileDescriptorProto(name: "file"), + FileDescriptorProto(name: "google/protobuf/any.proto", package: "google.protobuf"), + FileDescriptorProto(name: "dir1/file", dependencies: ["file"]), + FileDescriptorProto(name: "dir2/file", dependencies: ["google/protobuf/any.proto"]), + FileDescriptorProto(name: "file4", dependencies: ["dir2/file", "dir1/file", "file"]), + FileDescriptorProto(name: "file5", dependencies: ["file"]), + ] + let descSet = DescriptorSet(protos: fileProtos) + + // ( filename, imports, implOnly imports ) + let tests: [(String, String, String)] = [ + ( "file", "", "" ), + ( "dir1/file", "import foo", "@_implementationOnly import foo" ), + ( "dir2/file", "", "" ), + ( "file4", "import bar\nimport foo", "@_implementationOnly import bar\n@_implementationOnly import foo" ), + ( "file5", "", "" ), + ] + + for (name, expected, expectedImplOnly) in tests { + let fileDesc = descSet.files.filter{ $0.name == name }.first! + do { // reexportPublicImports: false, asImplementationOnly: false + let namer = + SwiftProtobufNamer(currentFile: fileDesc, + protoFileToModuleMappings: mapper) + let result = fileDesc.computeImports(namer: namer, reexportPublicImports: false, asImplementationOnly: false) + XCTAssertEqual(result, expected, "Looking for \(name)") + } + do { // reexportPublicImports: true, asImplementationOnly: false - No `import publc`, same as previous + let namer = + SwiftProtobufNamer(currentFile: fileDesc, + protoFileToModuleMappings: mapper) + let result = fileDesc.computeImports(namer: namer, reexportPublicImports: true, asImplementationOnly: false) + XCTAssertEqual(result, expected, "Looking for \(name)") + } + do { // reexportPublicImports: false, asImplementationOnly: true + let namer = + SwiftProtobufNamer(currentFile: fileDesc, + protoFileToModuleMappings: mapper) + let result = fileDesc.computeImports(namer: namer, reexportPublicImports: false, asImplementationOnly: true) + XCTAssertEqual(result, expectedImplOnly, "Looking for \(name)") + } + } + } + + func test_File_computeImports_PublicImports() { + // See the notes on computeImports(namer:reexportPublicImports:asImplementationOnly:) + // about how public import complicate things. + + // Given: + // + // + File: a.proto + // message A {} + // + // enum E { + // E_UNSET = 0; + // E_A = 1; + // E_B = 2; + // } + // + // + File: imports_a_publicly.proto + // import public "a.proto"; + // + // message ImportsAPublicly { + // optional A a = 1; + // optional E e = 2; + // } + // + // + File: imports_imports_a_publicly.proto + // import public "imports_a_publicly.proto"; + // + // message ImportsImportsAPublicly { + // optional A a = 1; + // } + // + // + File: uses_a_transitively.proto + // import "imports_a_publicly.proto"; + // + // message UsesATransitively { + // optional A a = 1; + // } + // + // + File: uses_a_transitively2.proto + // import "imports_imports_a_publicly.proto"; + // + // message UsesATransitively2 { + // optional A a = 1; + // } + // + // With a mapping file of: + // + // mapping { + // module_name: "A" + // proto_file_path: "a.proto" + // } + // mapping { + // module_name: "ImportsAPublicly" + // proto_file_path: "imports_a_publicly.proto" + // } + // mapping { + // module_name: "ImportsImportsAPublicly" + // proto_file_path: "imports_imports_a_publicly.proto" + // } + + let configText = """ + mapping { module_name: "A", proto_file_path: "a.proto" } + mapping { module_name: "ImportsAPublicly", proto_file_path: "imports_a_publicly.proto" } + mapping { module_name: "ImportsImportsAPublicly", proto_file_path: "imports_imports_a_publicly.proto" } + """ + + let config = try! SwiftProtobuf_GenSwift_ModuleMappings(textFormatString: configText) + let mapper = try! ProtoFileToModuleMappings(moduleMappingsProto: config) + + let fileProtos = [ + try! FileDescriptorProto(textFormatString: """ + name: "a.proto" + message_type { + name: "A" + } + enum_type { + name: "E" + value { + name: "E_UNSET" + number: 0 + } + value { + name: "E_A" + number: 1 + } + value { + name: "E_B" + number: 2 + } + } + """), + try! FileDescriptorProto(textFormatString: """ + name: "imports_a_publicly.proto" + dependency: "a.proto" + message_type { + name: "ImportsAPublicly" + field { + name: "a" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".A" + json_name: "a" + } + field { + name: "e" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_ENUM + type_name: ".E" + json_name: "e" + } + } + public_dependency: 0 + """), + try! FileDescriptorProto(textFormatString: """ + name: "imports_imports_a_publicly.proto" + dependency: "imports_a_publicly.proto" + message_type { + name: "ImportsImportsAPublicly" + field { + name: "a" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".A" + json_name: "a" + } + } + public_dependency: 0 + """), + try! FileDescriptorProto(textFormatString: """ + name: "uses_a_transitively.proto" + dependency: "imports_a_publicly.proto" + message_type { + name: "UsesATransitively" + field { + name: "a" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".A" + json_name: "a" + } + } + """), + try! FileDescriptorProto(textFormatString: """ + name: "uses_a_transitively2.proto" + dependency: "imports_imports_a_publicly.proto" + message_type { + name: "UsesATransitively2" + field { + name: "a" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".A" + json_name: "a" + } + } + """), + ] + let descSet = DescriptorSet(protos: fileProtos) + + // ( filename, imports, reExportPublicImports imports, implOnly imports ) + let tests: [(String, String, String, String)] = [ + ( "a.proto", + "", "", "" ), + ( "imports_a_publicly.proto", + "import A", + "// Use of 'import public' causes re-exports:\n@_exported import enum A.E\n@_exported import struct A.A", + "@_implementationOnly import A" ), + ( "imports_imports_a_publicly.proto", + "import ImportsAPublicly", + "// Use of 'import public' causes re-exports:\n@_exported import enum A.E\n@_exported import struct A.A\n@_exported import struct ImportsAPublicly.ImportsAPublicly", + "@_implementationOnly import ImportsAPublicly" ), + ( "uses_a_transitively.proto", + "import ImportsAPublicly", // this reexports A, so we don't directly pull in A. + "import ImportsAPublicly", // just a plain `import`, nothing to re-export. + "@_implementationOnly import ImportsAPublicly" ), + ( "uses_a_transitively2.proto", + "import ImportsImportsAPublicly", // this chain reexports A, so we don't directly pull in A. + "import ImportsImportsAPublicly", // just a plain `import`, nothing to re-export. + "@_implementationOnly import ImportsImportsAPublicly" ), + ] + + for (name, expected, expectedReExport, expectedImplOnly) in tests { + let fileDesc = descSet.files.filter{ $0.name == name }.first! + do { // reexportPublicImports: false, asImplementationOnly: false + let namer = + SwiftProtobufNamer(currentFile: fileDesc, + protoFileToModuleMappings: mapper) + let result = fileDesc.computeImports(namer: namer, reexportPublicImports: false, asImplementationOnly: false) + XCTAssertEqual(result, expected, "Looking for \(name)") + } + do { // reexportPublicImports: true, asImplementationOnly: false + let namer = + SwiftProtobufNamer(currentFile: fileDesc, + protoFileToModuleMappings: mapper) + let result = fileDesc.computeImports(namer: namer, reexportPublicImports: true, asImplementationOnly: false) + XCTAssertEqual(result, expectedReExport, "Looking for \(name)") + } + do { // reexportPublicImports: false, asImplementationOnly: true + let namer = + SwiftProtobufNamer(currentFile: fileDesc, + protoFileToModuleMappings: mapper) + let result = fileDesc.computeImports(namer: namer, reexportPublicImports: false, asImplementationOnly: true) + XCTAssertEqual(result, expectedImplOnly, "Looking for \(name)") + } + } + } + }