From 7b8907fad650d7aca321b3ed445bfaddce3e3ae2 Mon Sep 17 00:00:00 2001 From: Arpit Garg Date: Mon, 27 Apr 2026 20:22:09 +0530 Subject: [PATCH 1/2] Support access modifiers on generated imports Support access modifiers on generated imports ### Motivation With the introduction of Swift 6.0's `InternalImportsByDefault` flag, generated declarations need a way to re-export the symbols they depend on. Supporting the configured access modifier on imports ensures they remain visible to consumers of the generated code when necessary. ### Modifications - Add `accessModifier` property to `ImportDescription`. - Update `TextBasedRenderer` to conditionally render `public` and `package` access modifiers on imports, wrapped within an `#if compiler(>=6.0)` block to maintain compatibility with older compilers. - Introduce `importDescriptions(adding:)` in `FileTranslator` to automatically propagate the configured access modifier to both built-in and additional imports. - Adopt the new import generation logic in `ClientFileTranslator`, `ServerFileTranslator`, and `TypesFileTranslator`. - Add unit tests in `Test_TextBasedRenderer` to verify the correct rendering of imports with different access modifiers, attributes, and OS conditions. ### Result Generated Swift files can now properly emit `public import` or `package import` statements when compiled with Swift `6.0` and above, based on the provided configuration. ### Test Plan Unit tests added in `Test_TextBasedRenderer`. --- .../StructuredSwiftRepresentation.swift | 6 ++ .../Renderer/TextBasedRenderer.swift | 17 ++++- .../ClientTranslator/ClientTranslator.swift | 3 +- .../Translator/FileTranslator.swift | 27 ++++++++ .../ServerTranslator/ServerTranslator.swift | 5 +- .../TypesTranslator/TypesFileTranslator.swift | 2 +- .../Renderer/Test_TextBasedRenderer.swift | 64 +++++++++++++++++++ 7 files changed, 116 insertions(+), 8 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift index 4a7cacef5..4ac2a9e10 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift @@ -33,6 +33,12 @@ struct ImportDescription: Equatable, Codable { /// would be `@_spi(Secret) import Foo`. var spi: String? = nil + /// The access modifier to apply to the import statement. + /// + /// When set to `.public` or `.package`, the modifier is prepended to the + /// import statement (e.g. `public import Foo`). + var accessModifier: AccessModifier? = nil + /// Requirements for the `@preconcurrency` attribute. var preconcurrency: PreconcurrencyRequirement = .never diff --git a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift index b6d0a9c89..b848146a3 100644 --- a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift +++ b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift @@ -149,13 +149,26 @@ struct TextBasedRenderer: RendererProtocol { /// Renders a single import statement. func renderImport(_ description: ImportDescription) { + let accessModifierPrefix: String + switch description.accessModifier { + case .public: + accessModifierPrefix = renderedAccessModifier(.public) + " " + case .package: + accessModifierPrefix = renderedAccessModifier(.package) + " " + default: + accessModifierPrefix = "" + } + func render(preconcurrency: Bool) { let spiPrefix = description.spi.map { "@_spi(\($0)) " } ?? "" let preconcurrencyPrefix = preconcurrency ? "@preconcurrency " : "" + let attributePrefix = "\(preconcurrencyPrefix)\(spiPrefix)" if let moduleTypes = description.moduleTypes { - for type in moduleTypes { writer.writeLine("\(preconcurrencyPrefix)\(spiPrefix)import \(type)") } + for type in moduleTypes { + writer.writeLine("\(attributePrefix)\(accessModifierPrefix)import \(type)") + } } else { - writer.writeLine("\(preconcurrencyPrefix)\(spiPrefix)import \(description.moduleName)") + writer.writeLine("\(attributePrefix)\(accessModifierPrefix)import \(description.moduleName)") } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift index 7e976a61b..58798e2ee 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift @@ -34,8 +34,7 @@ struct ClientFileTranslator: FileTranslator { let topComment = self.topComment - let imports = - Constants.File.clientServerImports + config.additionalImports.map { ImportDescription(moduleName: $0) } + let imports = importDescriptions(adding: Constants.File.clientServerImports) let clientMethodDecls = try OperationDescription.all(from: doc.paths, in: components, context: context) .map(translateClientMethod(_:)) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift index 6d46f0264..d820fa5f8 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift @@ -89,6 +89,33 @@ extension FileTranslator { var topComment: Comment { .inline(([Constants.File.topComment] + config.additionalFileComments).joined(separator: "\n")) } + + /// Returns the imports for the generated file, with access modifiers applied. + /// + /// The configured access modifier is propagated to the built-in imports so + /// that, under Swift 6's `InternalImportsByDefault` flag, generated + /// declarations can re-export the symbols they depend on. + /// `additionalImports` from the configuration are also given the same + /// access modifier so they are visible to consumers of the generated code. + /// - Parameter baseImports: the base set of imports for the file (e.g. + /// ``Constants/File/imports`` or ``Constants/File/clientServerImports``). + /// - Returns: An array of ``ImportDescription`` values with appropriate + /// access modifier set. + func importDescriptions(adding baseImports: [ImportDescription]) -> [ImportDescription] { + let accessModifier: AccessModifier? + switch config.access { + case .public, .package: + accessModifier = config.access + default: + accessModifier = nil + } + let allImports: [ImportDescription] = baseImports + config.additionalImports.map { ImportDescription(moduleName: $0) } + return allImports.map { original in + var description = original + description.accessModifier = accessModifier + return description + } + } } /// A set of configuration values for concrete file translators. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift index 2091a0f8c..6ee1de1c0 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift @@ -32,9 +32,8 @@ struct ServerFileTranslator: FileTranslator { let topComment = self.topComment - let imports = - Constants.File.clientServerImports + config.additionalImports.map { ImportDescription(moduleName: $0) } - + let imports = importDescriptions(adding: Constants.File.clientServerImports) + let allOperations = try OperationDescription.all(from: doc.paths, in: components, context: context) let (registerHandlersDecl, serverMethodDecls) = try translateRegisterHandlers(allOperations) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift index ea8e6aa9a..cdf447f7b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift @@ -34,7 +34,7 @@ struct TypesFileTranslator: FileTranslator { let topComment = self.topComment - let imports = Constants.File.imports + config.additionalImports.map { ImportDescription(moduleName: $0) } + let imports = importDescriptions(adding: Constants.File.imports) let apiProtocol = try translateAPIProtocol(doc.paths) diff --git a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift index a8e16c7fa..17dd71964 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift @@ -116,6 +116,70 @@ final class Test_TextBasedRenderer: XCTestCase { ) } + func testImportsWithPublicAccessModifier() throws { + try _test( + [ImportDescription(moduleName: "Foo", accessModifier: .public)], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + public import Foo + """# + ) + } + + func testImportsWithPackageAccessModifier() throws { + try _test( + [ImportDescription(moduleName: "Foo", accessModifier: .package)], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + package import Foo + """# + ) + } + + func testImportsWithInternalAccessModifier() throws { + try _test( + [ImportDescription(moduleName: "Foo", accessModifier: .internal)], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + import Foo + """# + ) + } + + func testImportsWithAccessModifierAndAttributes() throws { + try _test( + [ImportDescription(moduleName: "Foo", spi: "Secret", accessModifier: .public, preconcurrency: .always)], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + @preconcurrency @_spi(Secret) public import Foo + """# + ) + } + + func testImportsWithAccessModifierAndModuleTypes() throws { + try _test( + [ImportDescription(moduleName: "Foundation", moduleTypes: ["struct Foundation.URL"], accessModifier: .public)], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + public import struct Foundation.URL + """# + ) + } + + func testImportsWithAccessModifierAndPreconcurrencyOnOS() throws { + try _test( + [ImportDescription(moduleName: "Foo", accessModifier: .public, preconcurrency: .onOS(["Linux"]))], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + #if os(Linux) + @preconcurrency public import Foo + #else + public import Foo + #endif + """# + ) + } + func testAccessModifiers() throws { try _test( .public, From 65a9d0ece19b0b6ea2d16160a7bb9f1af0fe6380 Mon Sep 17 00:00:00 2001 From: Arpit Garg Date: Sat, 16 May 2026 13:15:01 +0530 Subject: [PATCH 2/2] Fix CI failures introduced by access-modifier import feature. Add setsAccessModifier flag to ImportDescription; update Petstore snapshots and ChatGPT example. --- .../Sources/ChatGPT/Middleware.swift | 4 ++-- .../Layers/StructuredSwiftRepresentation.swift | 8 ++++++++ .../Renderer/TextBasedRenderer.swift | 13 ++++--------- .../Translator/CommonTypes/Constants.swift | 2 +- .../Translator/FileTranslator.swift | 14 ++++++-------- .../ServerTranslator/ServerTranslator.swift | 2 +- .../Renderer/Test_TextBasedRenderer.swift | 8 +++++++- .../ReferenceSources/Petstore/Client.swift | 14 +++++++------- .../ReferenceSources/Petstore/Server.swift | 14 +++++++------- .../ReferenceSources/Petstore/Types.swift | 14 +++++++------- 10 files changed, 50 insertions(+), 43 deletions(-) diff --git a/Examples/streaming-chatgpt-proxy/Sources/ChatGPT/Middleware.swift b/Examples/streaming-chatgpt-proxy/Sources/ChatGPT/Middleware.swift index e3e1bd9e1..e67d1d785 100644 --- a/Examples/streaming-chatgpt-proxy/Sources/ChatGPT/Middleware.swift +++ b/Examples/streaming-chatgpt-proxy/Sources/ChatGPT/Middleware.swift @@ -11,9 +11,9 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import Foundation +package import Foundation import HTTPTypes -import OpenAPIRuntime +package import OpenAPIRuntime package struct HeaderFieldMiddleware: ClientMiddleware { var name: HTTPField.Name diff --git a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift index 4ac2a9e10..b14212761 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift @@ -39,6 +39,14 @@ struct ImportDescription: Equatable, Codable { /// import statement (e.g. `public import Foo`). var accessModifier: AccessModifier? = nil + /// Whether the global access modifier from the generator config should be + /// applied to this import. + /// + /// Set to `false` for modules whose types don't appear in public + /// declarations (e.g. implementation-only imports), so that + /// `public import` or `package import` is not incorrectly emitted. + var setsAccessModifier: Bool = true + /// Requirements for the `@preconcurrency` attribute. var preconcurrency: PreconcurrencyRequirement = .never diff --git a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift index b848146a3..1fdb035fd 100644 --- a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift +++ b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift @@ -151,12 +151,9 @@ struct TextBasedRenderer: RendererProtocol { func renderImport(_ description: ImportDescription) { let accessModifierPrefix: String switch description.accessModifier { - case .public: - accessModifierPrefix = renderedAccessModifier(.public) + " " - case .package: - accessModifierPrefix = renderedAccessModifier(.package) + " " - default: - accessModifierPrefix = "" + case .public: accessModifierPrefix = renderedAccessModifier(.public) + " " + case .package: accessModifierPrefix = renderedAccessModifier(.package) + " " + default: accessModifierPrefix = "" } func render(preconcurrency: Bool) { @@ -164,9 +161,7 @@ struct TextBasedRenderer: RendererProtocol { let preconcurrencyPrefix = preconcurrency ? "@preconcurrency " : "" let attributePrefix = "\(preconcurrencyPrefix)\(spiPrefix)" if let moduleTypes = description.moduleTypes { - for type in moduleTypes { - writer.writeLine("\(attributePrefix)\(accessModifierPrefix)import \(type)") - } + for type in moduleTypes { writer.writeLine("\(attributePrefix)\(accessModifierPrefix)import \(type)") } } else { writer.writeLine("\(attributePrefix)\(accessModifierPrefix)import \(description.moduleName)") } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 6f3d1782e..e79ab7fe9 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -45,7 +45,7 @@ enum Constants { /// The descriptions of modules imported by client and server files. static let clientServerImports: [ImportDescription] = - imports + [ImportDescription(moduleName: Constants.Import.httpTypes)] + imports + [ImportDescription(moduleName: Constants.Import.httpTypes, setsAccessModifier: false)] } /// Constants related to the OpenAPI server object. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift index d820fa5f8..4b5046c60 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift @@ -102,17 +102,15 @@ extension FileTranslator { /// - Returns: An array of ``ImportDescription`` values with appropriate /// access modifier set. func importDescriptions(adding baseImports: [ImportDescription]) -> [ImportDescription] { - let accessModifier: AccessModifier? + let accessModifier: AccessModifier? switch config.access { - case .public, .package: - accessModifier = config.access - default: - accessModifier = nil + case .public, .package: accessModifier = config.access + default: accessModifier = nil } - let allImports: [ImportDescription] = baseImports + config.additionalImports.map { ImportDescription(moduleName: $0) } - return allImports.map { original in + let allImports = baseImports + config.additionalImports.map { ImportDescription(moduleName: $0) } + return allImports.map { original in var description = original - description.accessModifier = accessModifier + description.accessModifier = original.setsAccessModifier ? accessModifier : nil return description } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift index 6ee1de1c0..427b25bbe 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift @@ -33,7 +33,7 @@ struct ServerFileTranslator: FileTranslator { let topComment = self.topComment let imports = importDescriptions(adding: Constants.File.clientServerImports) - + let allOperations = try OperationDescription.all(from: doc.paths, in: components, context: context) let (registerHandlersDecl, serverMethodDecls) = try translateRegisterHandlers(allOperations) diff --git a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift index 17dd71964..b6c52da39 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift @@ -158,7 +158,13 @@ final class Test_TextBasedRenderer: XCTestCase { func testImportsWithAccessModifierAndModuleTypes() throws { try _test( - [ImportDescription(moduleName: "Foundation", moduleTypes: ["struct Foundation.URL"], accessModifier: .public)], + [ + ImportDescription( + moduleName: "Foundation", + moduleTypes: ["struct Foundation.URL"], + accessModifier: .public + ) + ], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" public import struct Foundation.URL diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index d9c80e62e..fbcde001b 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -1,13 +1,13 @@ // Generated by swift-openapi-generator, do not modify. -@_spi(Generated) import OpenAPIRuntime +@_spi(Generated) public import OpenAPIRuntime #if os(Linux) -@preconcurrency import struct Foundation.URL -@preconcurrency import struct Foundation.Data -@preconcurrency import struct Foundation.Date +@preconcurrency public import struct Foundation.URL +@preconcurrency public import struct Foundation.Data +@preconcurrency public import struct Foundation.Date #else -import struct Foundation.URL -import struct Foundation.Data -import struct Foundation.Date +public import struct Foundation.URL +public import struct Foundation.Data +public import struct Foundation.Date #endif import HTTPTypes /// Service for managing pet metadata. diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index dae16f3f2..31e0375ed 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -1,13 +1,13 @@ // Generated by swift-openapi-generator, do not modify. -@_spi(Generated) import OpenAPIRuntime +@_spi(Generated) public import OpenAPIRuntime #if os(Linux) -@preconcurrency import struct Foundation.URL -@preconcurrency import struct Foundation.Data -@preconcurrency import struct Foundation.Date +@preconcurrency public import struct Foundation.URL +@preconcurrency public import struct Foundation.Data +@preconcurrency public import struct Foundation.Date #else -import struct Foundation.URL -import struct Foundation.Data -import struct Foundation.Date +public import struct Foundation.URL +public import struct Foundation.Data +public import struct Foundation.Date #endif import HTTPTypes extension APIProtocol { diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index df2188cd7..be38b0f96 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -1,13 +1,13 @@ // Generated by swift-openapi-generator, do not modify. -@_spi(Generated) import OpenAPIRuntime +@_spi(Generated) public import OpenAPIRuntime #if os(Linux) -@preconcurrency import struct Foundation.URL -@preconcurrency import struct Foundation.Data -@preconcurrency import struct Foundation.Date +@preconcurrency public import struct Foundation.URL +@preconcurrency public import struct Foundation.Data +@preconcurrency public import struct Foundation.Date #else -import struct Foundation.URL -import struct Foundation.Data -import struct Foundation.Date +public import struct Foundation.URL +public import struct Foundation.Data +public import struct Foundation.Date #endif /// A type that performs HTTP operations defined by the OpenAPI document. public protocol APIProtocol: Sendable {