Skip to content

jextract: Allow importing internal decls #343

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 61 additions & 8 deletions Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions Sources/JExtractSwiftLib/Logger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [:],
Expand Down
4 changes: 2 additions & 2 deletions Sources/JExtractSwiftLib/Swift2JavaTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down
27 changes: 19 additions & 8 deletions Sources/JExtractSwiftLib/Swift2JavaVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions Sources/JavaKitConfigurationShared/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------------

Expand Down
15 changes: 14 additions & 1 deletion Sources/JavaKitConfigurationShared/GenerationMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
5 changes: 5 additions & 0 deletions Sources/SwiftJavaTool/Commands/JExtractCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -85,6 +88,7 @@ extension SwiftJava.JExtractCommand {
config.outputSwiftDirectory = outputSwift
config.writeEmptyFiles = writeEmptyFiles
config.unsignedNumbersMode = unsignedNumbers
config.minimumInputAccessLevelMode = minimumInputAccessLevel

try checkModeCompatibility()

Expand Down Expand Up @@ -143,3 +147,4 @@ struct IllegalModeCombinationError: Error {

extension JExtractGenerationMode: ExpressibleByArgument {}
extension JExtractUnsignedIntegerMode: ExpressibleByArgument {}
extension JExtractMinimumAccessLevelMode: ExpressibleByArgument {}
49 changes: 49 additions & 0 deletions Tests/JExtractSwiftTests/InternalExtractTests.swift
Original file line number Diff line number Diff line change
@@ -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();
}
""",
]
)
}
}
Loading