Skip to content
Open
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
13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ let package = Package(
targets: ["SwiftJavaConfigurationShared"]
),

// Marker macros such as `@JavaExport` that steer jextract, without the JNI runtime
.library(
name: "SwiftJavaAnnotations",
targets: ["SwiftJavaAnnotations"]
),

.library(
name: "SwiftExtractConfigurationShared",
targets: ["SwiftExtractConfigurationShared"]
Expand Down Expand Up @@ -196,10 +202,17 @@ let package = Package(
.swiftLanguageMode(.v5)
]
),
.target(
name: "SwiftJavaAnnotations",
dependencies: [
"SwiftJavaMacros"
]
),
.target(
name: "SwiftJava",
dependencies: [
.product(name: "SwiftJavaJNICore", package: "swift-java-jni-core"),
"SwiftJavaAnnotations",
"SwiftJavaMacros",
],
exclude: ["swift-java.config"],
Expand Down
103 changes: 87 additions & 16 deletions Sources/JExtractSwiftLib/JavaExtractDecider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,21 @@ import SwiftExtract
import SwiftJavaConfigurationShared
import SwiftSyntax

public func makeSwiftJavaAnalyzer(config: Configuration) -> SwiftAnalyzer {
public func makeSwiftJavaAnalyzer(
config: Configuration,
dependencyConfigs: [DependencyConfig] = []
) -> SwiftAnalyzer {
SwiftAnalyzer(
config: config,
extractDecider: JavaExtractDecider(
accessLevel: config.effectiveMinimumInputAccessLevelMode,
exportMode: config.effectiveExportMode,
swiftModuleName: config.swiftModule,
dependencyExportModes: dependencyConfigs.reduce(into: [:]) { modes, dependency in
if let moduleName = dependency.swiftModuleName {
modes[moduleName] = dependency.configuration.effectiveExportMode
}
},
logLevel: config.logLevel ?? .info
),
)
Expand All @@ -33,17 +43,36 @@ public func makeSwiftJavaAnalyzer(config: Configuration) -> SwiftAnalyzer {
/// - Skips initializers of unspecialized generic types
/// - Force-extracts decls annotated `@JavaExport` even if they would
/// otherwise be filtered by access level
/// - Skips decls annotated `@JavaExport(false)`, or inside a type or extension
/// annotated `@JavaExport(false)`
/// - With the `annotated` export mode, extracts only decls annotated
/// `@JavaExport`, inside a type or extension annotated `@JavaExport`, or
/// types with such a member
/// - Skips Swift wrappers of Java types (`@JavaClass`, `@JavaInterface`,
/// `@JavaField`, `@JavaStaticField`, `@JavaMethod`, `@JavaStaticMethod`,
/// `@JavaImplementation`) since those are bridged the other way
/// - Skips Swift operators (`+`, `-`, prefix/postfix forms) — Java has no
/// operator-overload syntax, so the generator can't render them
public struct JavaExtractDecider: ExtractDecider {
public let accessLevel: AccessLevelMode
public let exportMode: JExtractExportMode
/// The module being extracted, whose types follow `exportMode`
public let swiftModuleName: String?
/// The export mode of each dependency module, from its `swift-java.config`
public let dependencyExportModes: [String: JExtractExportMode]
let log: Logger

public init(accessLevel: AccessLevelMode = .default, logLevel: LogLevel = .info) {
public init(
accessLevel: AccessLevelMode = .default,
exportMode: JExtractExportMode = .default,
swiftModuleName: String? = nil,
dependencyExportModes: [String: JExtractExportMode] = [:],
logLevel: LogLevel = .info
) {
self.accessLevel = accessLevel
self.exportMode = exportMode
self.swiftModuleName = swiftModuleName
self.dependencyExportModes = dependencyExportModes
self.log = Logger(label: "JavaExtractDecider", logLevel: logLevel)
}

Expand All @@ -62,33 +91,75 @@ public struct JavaExtractDecider: ExtractDecider {
return false
}

let attrs = decl.asProtocol((any WithAttributesSyntax).self)?.attributes
if attrs?.contains(where: { $0.isJavaExport }) == true {
switch decl.javaExportMarker {
case true?:
return true
case false?:
log.trace("Skip '\(decl.qualifiedNameForDebug)': annotated @JavaExport(false)")
return false
case nil:
break
}
let attrs = decl.asProtocol((any WithAttributesSyntax).self)?.attributes
if attrs?.contains(where: { $0.isJavaKitMacro }) == true {
log.trace("Skip '\(decl.qualifiedNameForDebug)': swift-java macro-wrapped Java type")
return false
}

// Swift operators have no Java mapping
if let fn = decl.as(FunctionDeclSyntax.self) {
switch fn.name.tokenKind {
case .prefixOperator, .postfixOperator:
log.trace("Skip '\(decl.qualifiedNameForDebug)': operators are not supported on Java")
return false
default:
break
}
// Swift prefix and postfix operators have no Java mapping. A declaration's operator name is
// lexed as a binary operator, so only the `prefix` or `postfix` modifier tells them apart.
if let fn = decl.as(FunctionDeclSyntax.self),
fn.modifiers.contains(where: { $0.name.tokenKind == .keyword(.prefix) || $0.name.tokenKind == .keyword(.postfix) })
{
log.trace("Skip '\(decl.qualifiedNameForDebug)': operators are not supported on Java")
return false
}

let inheritedMarker = decl.inheritedJavaExportMarker
if inheritedMarker == false {
log.trace("Skip '\(decl.qualifiedNameForDebug)': inside a declaration annotated @JavaExport(false)")
return false
}

guard let mod = decl.asProtocol((any WithModifiersSyntax).self) else {
return false
}
let ok = mod.isAtLeast(accessLevel, in: parent)
if !ok {
guard mod.isAtLeast(accessLevel, in: parent) else {
log.trace("Skip '\(decl.qualifiedNameForDebug)': not at least \(accessLevel)")
return false
}

if exportMode == .annotated, inheritedMarker != true, !decl.containsJavaExportedMember,
!decl.isFromKnownModuleSource
{
log.trace("Skip '\(decl.qualifiedNameForDebug)': not annotated @JavaExport (exportMode: annotated)")
return false
}
return true
}

public func isExported(_ type: SwiftNominalTypeDeclaration) -> Bool {
let mode: JExtractExportMode
if type.moduleName == swiftModuleName {
mode = exportMode
} else if let dependencyMode = dependencyExportModes[type.moduleName] {
mode = dependencyMode
} else {
// A known module such as Foundation, or a module that isn't extracted
return true
}
switch type.syntax.javaExportMarker ?? type.syntax.inheritedJavaExportMarker {
case true?: return true
case false?: return false
case nil: return mode == .all || type.syntax.containsJavaExportedMember
}
}

public func mayExtractMembers(of extensionDecl: ExtensionDeclSyntax) -> Bool {
switch extensionDecl.javaExportMarker ?? extensionDecl.inheritedJavaExportMarker {
case true?: true
case false?: false
case nil: exportMode == .all || extensionDecl.containsJavaExportedMember
}
return ok
}
}
2 changes: 1 addition & 1 deletion Sources/JExtractSwiftLib/Swift2Java.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public struct SwiftToJava {

let javaPackage = try validateJavaPackage(config.javaPackage)

let translator = makeSwiftJavaAnalyzer(config: config)
let translator = makeSwiftJavaAnalyzer(config: config, dependencyConfigs: dependencyConfigs)
let log = translator.log

guard let inputSwift = config.inputSwiftDirectory else {
Expand Down
44 changes: 41 additions & 3 deletions Sources/JExtractSwiftLib/SwiftSyntax+Java.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,47 @@ extension AttributeListSyntax.Element {
/// Whether this is a `@JavaExport` attribute (used on typealiases for specialization,
/// or on struct/class/enum to force-include them even when excluded by filters)
package var isJavaExport: Bool {
guard case let .attribute(attr) = self else { return false }
guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false }
return attrName == "JavaExport"
javaExportValue != nil
}

/// `true` for `@JavaExport`, `false` for `@JavaExport(false)`, `nil` for any other attribute
package var javaExportValue: Bool? {
guard case let .attribute(attr) = self else { return nil }
guard attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "JavaExport" else { return nil }
guard case .argumentList(let arguments) = attr.arguments,
let literal = arguments.first?.expression.as(BooleanLiteralExprSyntax.self)
else {
return true
}
return literal.literal.tokenKind == .keyword(.true)
}
}

extension SyntaxProtocol {
/// The value of this declaration's own `@JavaExport` attribute, if it has one
package var javaExportMarker: Bool? {
guard let attributes = self.asProtocol((any WithAttributesSyntax).self)?.attributes else { return nil }
return attributes.lazy.compactMap(\.javaExportValue).first
}

/// The `@JavaExport` value of the nearest enclosing type or extension that has one
package var inheritedJavaExportMarker: Bool? {
var node = self.parent
while let current = node {
if current.is(DeclSyntax.self), let marker = current.javaExportMarker {
return marker
}
node = current.parent
}
return nil
}

/// Whether a member of this type, at any depth, is marked `@JavaExport`
package var containsJavaExportedMember: Bool {
guard let group = self.asProtocol((any DeclGroupSyntax).self) else { return false }
return group.memberBlock.members.contains { item in
item.decl.javaExportMarker == true || item.decl.containsJavaExportedMember
}
}
}

Expand Down
18 changes: 18 additions & 0 deletions Sources/SwiftExtract/ExtractDecider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ public protocol ExtractDecider {
decl: DeclSyntax,
in parent: ExtractedNominalType?
) -> Bool

/// Whether any member of `extensionDecl` could be extracted. When this is `false`, an extension
/// whose extended type doesn't resolve is ignored instead of being reported as skipped.
func mayExtractMembers(of extensionDecl: ExtensionDeclSyntax) -> Bool

/// Whether `type`, from this module or a dependency, is exported. A declaration whose signature uses a
/// type that isn't exported is skipped, since the generated code would refer to a type that doesn't exist.
func isExported(_ type: SwiftNominalTypeDeclaration) -> Bool
}

extension ExtractDecider {
public func mayExtractMembers(of extensionDecl: ExtensionDeclSyntax) -> Bool {
true
}

public func isExported(_ type: SwiftNominalTypeDeclaration) -> Bool {
true
}
}

/// Minimal `ExtractDecider` that enforces only the configured access-level
Expand Down
53 changes: 53 additions & 0 deletions Sources/SwiftExtract/SwiftAnalysisVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ final class SwiftAnalysisVisitor {
return

case .unresolved:
guard analyzer.extractDecider.mayExtractMembers(of: node) else {
log.trace("Skip 'extension \(node.extendedType.trimmedDescription)': no member can be extracted")
return
}
self.reportSkipped(
node,
name: "extension \(node.extendedType.trimmedDescription)",
Expand Down Expand Up @@ -354,6 +358,7 @@ final class SwiftAnalysisVisitor {
enclosingType: typeContext?.swiftType,
lookupContext: analyzer.lookupContext,
)
try checkTypesAreExported(signature)
} catch {
self.reportSkipped(
node,
Expand Down Expand Up @@ -411,6 +416,7 @@ final class SwiftAnalysisVisitor {
enclosingType: typeContext.swiftType,
lookupContext: analyzer.lookupContext,
)
try checkTypesAreExported(signature)

let caseName = caseElement.name.text.unescapedSwiftName
let caseFunction = ExtractedFunc(
Expand Down Expand Up @@ -514,6 +520,7 @@ final class SwiftAnalysisVisitor {
enclosingType: typeContext.swiftType,
lookupContext: analyzer.lookupContext,
)
try checkTypesAreExported(signature)
} catch {
self.reportSkipped(
node,
Expand Down Expand Up @@ -632,6 +639,7 @@ final class SwiftAnalysisVisitor {
log.warning("Not supported declaration type \(node.kind) while calling importAccessor!")
return
}
try checkTypesAreExported(signature)

let extracted = ExtractedFunc(
module: analyzer.swiftModuleName,
Expand Down Expand Up @@ -893,6 +901,51 @@ final class SwiftAnalysisVisitor {
return "\(message). \(hint)"
}

/// A declaration uses a type that isn't exported, so the generated code would refer to a type that doesn't exist
struct UnexportedTypeError: Error, CustomStringConvertible {
let typeName: String

var description: String {
"uses '\(typeName)', which is not exported"
}
}

/// Throws `UnexportedTypeError` if a parameter or the result of `signature` uses a type that isn't exported
func checkTypesAreExported(_ signature: SwiftFunctionSignature) throws {
func check(_ type: SwiftType) throws {
switch type {
case .nominal(let nominal):
for argument in nominal.genericArguments {
try check(argument)
}
guard analyzer.extractDecider.isExported(nominal.nominalTypeDecl) else {
throw UnexportedTypeError(typeName: nominal.nominalTypeDecl.qualifiedName)
}
case .tuple(let elements):
for element in elements {
try check(element.type)
}
case .function(let function):
for parameter in function.parameters {
try check(parameter.type)
}
try check(function.resultType)
case .metatype(let type), .existential(let type), .opaque(let type), .inlineArray(_, let type):
try check(type)
case .composite(let types):
for type in types {
try check(type)
}
case .genericParameter:
break
}
}
for parameter in signature.parameters {
try check(parameter.type)
}
try check(signature.result.type)
}

/// Log a skipped declaration (as before) and report it to the configured
/// diagnostics sink, if any.
func reportSkipped(
Expand Down
9 changes: 9 additions & 0 deletions Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ public enum SwiftKnownModule: String {
}
}

extension SyntaxProtocol {
/// Whether this node is part of the source SwiftExtract uses to describe a known module, such as
/// the `Foundation.URL` it extracts when a module uses `URL`, rather than of the module's own source.
public var isFromKnownModuleSource: Bool {
let root = self.root.id
return root == swiftSourceFile.id || root == foundationEssentialsSourceFile.id
}
}

private var swiftSymbolTable: SwiftModuleSymbolTable {
var builder = SwiftParsedModuleSymbolTableBuilder(moduleName: "Swift", importedModules: [:])
builder.handle(sourceFile: swiftSourceFile, sourceFilePath: "SwiftStdlib.swift") // FIXME: missing path here
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftJava/CSwiftJavaJNI+Reexport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
//
//===----------------------------------------------------------------------===//

@_exported import SwiftJavaAnnotations
@_exported import SwiftJavaJNICore
12 changes: 0 additions & 12 deletions Sources/SwiftJava/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,3 @@ public macro JavaStaticMethod(_ javaMethodName: String? = nil) =
@attached(peer)
public macro JavaImplementation(_ fullClassName: String) =
#externalMacro(module: "SwiftJavaMacros", type: "JavaImplementationMacro")

/// Marker macro that forces a Swift declaration to be exported to Java via jextract.
///
/// When applied to a `typealias`, it registers a specialization entry for generic types:
/// ```swift
/// @JavaExport public typealias FishTank = Tank<Fish>
/// ```
/// This tells jextract to generate a concrete `FishTank` Java class for `Tank<Fish>`,
/// even if `Tank` was included in the `filterExclude` configuration.
@attached(peer)
public macro JavaExport() =
#externalMacro(module: "SwiftJavaMacros", type: "JavaExportMacro")
Loading