From e6fc6a8b04625864e2fd1ca2ca57d6ceb843d13e Mon Sep 17 00:00:00 2001 From: Konrad 'ktoso' Malawski Date: Fri, 1 Aug 2025 11:39:55 +0900 Subject: [PATCH] Allow jextract to optionally import package/internal decls as well --- .../Convenience/SwiftSyntax+Extensions.swift | 69 ++++++++++++++++--- Sources/JExtractSwiftLib/Logger.swift | 17 +++++ .../Swift2JavaTranslator.swift | 4 +- .../JExtractSwiftLib/Swift2JavaVisitor.swift | 27 +++++--- .../Configuration.swift | 4 ++ .../GenerationMode.swift | 15 +++- .../Commands/JExtractCommand.swift | 5 ++ .../InternalExtractTests.swift | 49 +++++++++++++ 8 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 Tests/JExtractSwiftTests/InternalExtractTests.swift diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift index da836d45b..e71300af7 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift @@ -51,23 +51,76 @@ extension DeclModifierSyntax { extension DeclModifierSyntax { var isPublic: Bool { switch self.name.tokenKind { - case .keyword(.private): return false - case .keyword(.fileprivate): return false - case .keyword(.internal): return false - case .keyword(.package): return false - case .keyword(.public): return true - case .keyword(.open): return true - default: return false + case .keyword(.private): false + case .keyword(.fileprivate): false + case .keyword(.internal): false + case .keyword(.package): false + case .keyword(.public): true + case .keyword(.open): true + default: false } } + + var isPackage: Bool { + switch self.name.tokenKind { + case .keyword(.private): false + case .keyword(.fileprivate): false + case .keyword(.internal): false + case .keyword(.package): true + case .keyword(.public): false + case .keyword(.open): false + default: false + } + } + + var isAtLeastPackage: Bool { + isPackage || isPublic + } + + var isInternal: Bool { + return switch self.name.tokenKind { + case .keyword(.private): false + case .keyword(.fileprivate): false + case .keyword(.internal): true + case .keyword(.package): false + case .keyword(.public): false + case .keyword(.open): false + default: false + } + } + + var isAtLeastInternal: Bool { + isInternal || isPackage || isPublic + } } extension WithModifiersSyntax { var isPublic: Bool { - self.modifiers.contains { modifier in + return self.modifiers.contains { modifier in modifier.isPublic } } + + var isAtLeastPackage: Bool { + if self.modifiers.isEmpty { + return false + } + + return self.modifiers.contains { modifier in + modifier.isAtLeastInternal + } + } + + var isAtLeastInternal: Bool { + if self.modifiers.isEmpty { + // we assume that default access level is internal + return true + } + + return self.modifiers.contains { modifier in + modifier.isAtLeastInternal + } + } } extension AttributeListSyntax.Element { diff --git a/Sources/JExtractSwiftLib/Logger.swift b/Sources/JExtractSwiftLib/Logger.swift index 541dbae42..5bffdc8ca 100644 --- a/Sources/JExtractSwiftLib/Logger.swift +++ b/Sources/JExtractSwiftLib/Logger.swift @@ -27,6 +27,23 @@ public struct Logger { self.logLevel = logLevel } + public func error( + _ message: @autoclosure () -> String, + metadata: [String: Any] = [:], + file: String = #fileID, + line: UInt = #line, + function: String = #function + ) { + guard logLevel <= .error else { + return + } + + let metadataString: String = + if metadata.isEmpty { "" } else { "\(metadata)" } + + print("[error][\(file):\(line)](\(function)) \(message()) \(metadataString)") + } + public func warning( _ message: @autoclosure () -> String, metadata: [String: Any] = [:], diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift index 3e2359f27..300b979f7 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift @@ -200,7 +200,7 @@ extension Swift2JavaTranslator { _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, parent: ImportedNominalType? ) -> ImportedNominalType? { - if !nominalNode.shouldImport(log: log) { + if !nominalNode.shouldExtract(config: config, log: log) { return nil } @@ -225,7 +225,7 @@ extension Swift2JavaTranslator { guard swiftNominalDecl.moduleName == self.swiftModuleName else { return nil } - guard swiftNominalDecl.syntax!.shouldImport(log: log) else { + guard swiftNominalDecl.syntax!.shouldExtract(config: config, log: log) else { return nil } diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 90181757f..3efddbfc6 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -15,9 +15,13 @@ import Foundation import SwiftParser import SwiftSyntax +import JavaKitConfigurationShared final class Swift2JavaVisitor { let translator: Swift2JavaTranslator + var config: Configuration { + self.translator.config + } init(translator: Swift2JavaTranslator) { self.translator = translator @@ -48,7 +52,7 @@ final class Swift2JavaVisitor { case .extensionDecl(let node): self.visit(extensionDecl: node, in: parent) case .typeAliasDecl: - break // TODO: Implement + break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 case .associatedTypeDecl: break // TODO: Implement @@ -93,7 +97,7 @@ final class Swift2JavaVisitor { } func visit(functionDecl node: FunctionDeclSyntax, in typeContext: ImportedNominalType?) { - guard node.shouldImport(log: log) else { + guard node.shouldExtract(config: config, log: log) else { return } @@ -128,7 +132,7 @@ final class Swift2JavaVisitor { } func visit(variableDecl node: VariableDeclSyntax, in typeContext: ImportedNominalType?) { - guard node.shouldImport(log: log) else { + guard node.shouldExtract(config: config, log: log) else { return } @@ -182,7 +186,7 @@ final class Swift2JavaVisitor { self.log.info("Initializer must be within a current type; \(node)") return } - guard node.shouldImport(log: log) else { + guard node.shouldExtract(config: config, log: log) else { return } @@ -212,13 +216,20 @@ final class Swift2JavaVisitor { } extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { - func shouldImport(log: Logger) -> Bool { - guard accessControlModifiers.contains(where: { $0.isPublic }) else { - log.trace("Skip import '\(self.qualifiedNameForDebug)': not public") + func shouldExtract(config: Configuration, log: Logger) -> Bool { + let meetsRequiredAccessLevel: Bool = + switch config.effectiveMinimumInputAccessLevelMode { + case .public: self.isPublic + case .package: self.isAtLeastPackage + case .internal: self.isAtLeastInternal + } + + guard meetsRequiredAccessLevel else { + log.debug("Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)") return false } guard !attributes.contains(where: { $0.isJava }) else { - log.trace("Skip import '\(self.qualifiedNameForDebug)': is Java") + log.debug("Skip import '\(self.qualifiedNameForDebug)': is Java") return false } diff --git a/Sources/JavaKitConfigurationShared/Configuration.swift b/Sources/JavaKitConfigurationShared/Configuration.swift index c1ae7dd2b..0ff089da0 100644 --- a/Sources/JavaKitConfigurationShared/Configuration.swift +++ b/Sources/JavaKitConfigurationShared/Configuration.swift @@ -46,6 +46,10 @@ public struct Configuration: Codable { public var effectiveUnsignedNumbersMode: JExtractUnsignedIntegerMode { unsignedNumbersMode ?? .default } + public var minimumInputAccessLevelMode: JExtractMinimumAccessLevelMode? + public var effectiveMinimumInputAccessLevelMode: JExtractMinimumAccessLevelMode { + minimumInputAccessLevelMode ?? .default + } // ==== java 2 swift --------------------------------------------------------- diff --git a/Sources/JavaKitConfigurationShared/GenerationMode.swift b/Sources/JavaKitConfigurationShared/GenerationMode.swift index 190323a05..1feac411b 100644 --- a/Sources/JavaKitConfigurationShared/GenerationMode.swift +++ b/Sources/JavaKitConfigurationShared/GenerationMode.swift @@ -59,7 +59,20 @@ extension JExtractUnsignedIntegerMode { } } - public static var `default`: JExtractUnsignedIntegerMode { + public static var `default`: Self { .annotate } } + +/// The minimum access level which +public enum JExtractMinimumAccessLevelMode: String, Codable { + case `public` + case `package` + case `internal` +} + +extension JExtractMinimumAccessLevelMode { + public static var `default`: Self { + .public + } +} diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index 105ace456..54a997083 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -64,6 +64,9 @@ extension SwiftJava { @Option(help: "The mode of generation to use for the output files. Used with jextract mode. By default, unsigned Swift types are imported as their bit-width compatible signed Java counterparts, and annotated using the '@Unsigned' annotation. You may choose the 'wrap-guava' mode in order to import types as class wrapper types (`UnsignedInteger` et al) defined by the Google Guava library's `com.google.common.primitives' package. that ensure complete type-safety with regards to unsigned values, however they incur an allocation and performance overhead.") var unsignedNumbers: JExtractUnsignedIntegerMode = .default + @Option(help: "The lowest access level of Swift declarations that should be extracted, defaults to 'public'.") + var minimumInputAccessLevel: JExtractMinimumAccessLevelMode = .default + @Option( help: """ A swift-java configuration file for a given Swift module name on which this module depends, @@ -85,6 +88,7 @@ extension SwiftJava.JExtractCommand { config.outputSwiftDirectory = outputSwift config.writeEmptyFiles = writeEmptyFiles config.unsignedNumbersMode = unsignedNumbers + config.minimumInputAccessLevelMode = minimumInputAccessLevel try checkModeCompatibility() @@ -143,3 +147,4 @@ struct IllegalModeCombinationError: Error { extension JExtractGenerationMode: ExpressibleByArgument {} extension JExtractUnsignedIntegerMode: ExpressibleByArgument {} +extension JExtractMinimumAccessLevelMode: ExpressibleByArgument {} diff --git a/Tests/JExtractSwiftTests/InternalExtractTests.swift b/Tests/JExtractSwiftTests/InternalExtractTests.swift new file mode 100644 index 000000000..78e712363 --- /dev/null +++ b/Tests/JExtractSwiftTests/InternalExtractTests.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import JavaKitConfigurationShared +import Testing + +final class InternalExtractTests { + let text = + """ + internal func catchMeIfYouCan() + """ + + @Test("Import: internal decl if configured") + func data_swiftThunk() throws { + var config = Configuration() + config.minimumInputAccessLevelMode = .internal + + try assertOutput( + input: text, + config: config, + .ffm, .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * internal func catchMeIfYouCan() + * } + */ + public static void catchMeIfYouCan() { + swiftjava_SwiftModule_catchMeIfYouCan.call(); + } + """, + ] + ) + } +} \ No newline at end of file