From 7b8907fad650d7aca321b3ed445bfaddce3e3ae2 Mon Sep 17 00:00:00 2001 From: Arpit Garg Date: Mon, 27 Apr 2026 20:22:09 +0530 Subject: [PATCH] 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,