Skip to content
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
3 changes: 2 additions & 1 deletion .unacceptablelanguageignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ Sources/_Subprocess/Platforms/Subprocess+Linux.swift
Sources/_Subprocess/Platforms/Subprocess+Unix.swift
Sources/_Subprocess/Teardown.swift
Sources/_Subprocess/Subprocess.swift
NOTICE.txt
Sources/SwiftJavaToolLib/AndroidAPILevel.swift
NOTICE.txt
137 changes: 137 additions & 0 deletions Sources/SwiftJavaToolLib/AndroidAPILevel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Android SDK version codes, mirroring `android.os.Build.VERSION_CODES`.
///
/// Source: android/platform/frameworks/base, core/java/android/os/Build.java
/// E.g. https://raw.githubusercontent.com/aosp-mirror/platform_frameworks_base/master/core/java/android/os/Build.java (Apache 2.0)
public enum AndroidAPILevel: Int {
/// The original, first, version of Android. Released publicly as Android 1.0 in September 2008.
case BASE = 1
/// First Android update. Released publicly as Android 1.1 in February 2009.
case BASE_1_1 = 2
/// Cupcake. Released publicly as Android 1.5 in April 2009.
case CUPCAKE = 3
/// Donut. Released publicly as Android 1.6 in September 2009.
case DONUT = 4
/// Eclair. Released publicly as Android 2.0 in October 2009.
case ECLAIR = 5
/// Eclair 0.1. Released publicly as Android 2.0.1 in December 2009.
case ECLAIR_0_1 = 6
/// Eclair MR1. Released publicly as Android 2.1 in January 2010.
case ECLAIR_MR1 = 7
/// Froyo. Released publicly as Android 2.2 in May 2010.
case FROYO = 8
/// Gingerbread. Released publicly as Android 2.3 in December 2010.
case GINGERBREAD = 9
/// Gingerbread MR1. Released publicly as Android 2.3.3 in February 2011.
case GINGERBREAD_MR1 = 10
/// Honeycomb. Released publicly as Android 3.0 in February 2011.
case HONEYCOMB = 11
/// Honeycomb MR1. Released publicly as Android 3.1 in May 2011.
case HONEYCOMB_MR1 = 12
/// Honeycomb MR2. Released publicly as Android 3.2 in July 2011.
case HONEYCOMB_MR2 = 13
/// Ice Cream Sandwich. Released publicly as Android 4.0 in October 2011.
case ICE_CREAM_SANDWICH = 14
/// Ice Cream Sandwich MR1. Released publicly as Android 4.03 in December 2011.
case ICE_CREAM_SANDWICH_MR1 = 15
/// Jelly Bean. Released publicly as Android 4.1 in July 2012.
case JELLY_BEAN = 16
/// Jelly Bean MR1. Released publicly as Android 4.2 in November 2012.
case JELLY_BEAN_MR1 = 17
/// Jelly Bean MR2. Released publicly as Android 4.3 in July 2013.
case JELLY_BEAN_MR2 = 18
/// KitKat. Released publicly as Android 4.4 in October 2013.
case KITKAT = 19
/// KitKat for watches. Released publicly as Android 4.4W in June 2014.
case KITKAT_WATCH = 20
/// Lollipop. Released publicly as Android 5.0 in November 2014.
case LOLLIPOP = 21
/// Lollipop MR1. Released publicly as Android 5.1 in March 2015.
case LOLLIPOP_MR1 = 22
/// Marshmallow. Released publicly as Android 6.0 in October 2015.
case M = 23
/// Nougat. Released publicly as Android 7.0 in August 2016.
case N = 24
/// Nougat MR1. Released publicly as Android 7.1 in October 2016.
case N_MR1 = 25
/// Oreo. Released publicly as Android 8.0 in August 2017.
case O = 26
/// Oreo MR1. Released publicly as Android 8.1 in December 2017.
case O_MR1 = 27
/// Pie. Released publicly as Android 9 in August 2018.
case P = 28
/// Android 10. Released publicly in September 2019.
case Q = 29
/// Android 11. Released publicly in September 2020.
case R = 30
/// Android 12.
case S = 31
/// Android 12L.
case S_V2 = 32
/// Tiramisu. Android 13.
case TIRAMISU = 33
/// Upside Down Cake. Android 14.
case UPSIDE_DOWN_CAKE = 34
/// Vanilla Ice Cream. Android 15.
case VANILLA_ICE_CREAM = 35
/// Baklava. Android 16 (upcoming, not yet finalized).
case BAKLAVA = 36
/// Magic version number for a current development build, which has not yet turned into an official release.
case CUR_DEVELOPMENT = 10000

/// Human-readable release name for this API level.
public var name: String {
switch self {
case .BASE: "Base"
case .BASE_1_1: "Base 1.1"
case .CUPCAKE: "Cupcake"
case .DONUT: "Donut"
case .ECLAIR: "Eclair"
case .ECLAIR_0_1: "Eclair 0.1"
case .ECLAIR_MR1: "Eclair MR1"
case .FROYO: "Froyo"
case .GINGERBREAD: "Gingerbread"
case .GINGERBREAD_MR1: "Gingerbread MR1"
case .HONEYCOMB: "Honeycomb"
case .HONEYCOMB_MR1: "Honeycomb MR1"
case .HONEYCOMB_MR2: "Honeycomb MR2"
case .ICE_CREAM_SANDWICH: "Ice Cream Sandwich"
case .ICE_CREAM_SANDWICH_MR1: "Ice Cream Sandwich MR1"
case .JELLY_BEAN: "Jelly Bean"
case .JELLY_BEAN_MR1: "Jelly Bean MR1"
case .JELLY_BEAN_MR2: "Jelly Bean MR2"
case .KITKAT: "KitKat"
case .KITKAT_WATCH: "KitKat Watch"
case .LOLLIPOP: "Lollipop"
case .LOLLIPOP_MR1: "Lollipop MR1"
case .M: "Marshmallow"
case .N: "Nougat"
case .N_MR1: "Nougat MR1"
case .O: "Oreo"
case .O_MR1: "Oreo MR1"
case .P: "Pie"
case .Q: "Android 10"
case .R: "Android 11"
case .S: "Android 12"
case .S_V2: "Android 12L"
case .TIRAMISU: "Tiramisu"
case .UPSIDE_DOWN_CAKE: "Upside Down Cake"
case .VANILLA_ICE_CREAM: "Vanilla Ice Cream"
case .BAKLAVA: "Baklava"
case .CUR_DEVELOPMENT: "CUR_DEVELOPMENT"
}
}
}
107 changes: 98 additions & 9 deletions Sources/SwiftJavaToolLib/JavaClassTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -441,9 +441,10 @@ extension JavaClassTranslator {
// Emit the struct declaration describing the java class.
let classOrInterface: String = isInterface ? "JavaInterface" : "JavaClass"
let introducer = translateAsClass ? "open class" : "public struct"
let classAvailableAttributes = swiftAvailableAttributes(from: annotations)
var classDecl: DeclSyntax =
"""
@\(raw: classOrInterface)(\(literal: javaClass.getName())\(raw: extendsClause)\(raw: interfacesStr))
\(raw: classAvailableAttributes.render())@\(raw: classOrInterface)(\(literal: javaClass.getName())\(raw: extendsClause)\(raw: interfacesStr))
\(raw: introducer) \(raw: swiftInnermostTypeName)\(raw: genericParameterClause)\(raw: inheritanceClause) {
\(raw: members.map { $0.description }.joined(separator: "\n\n"))
}
Expand Down Expand Up @@ -590,18 +591,98 @@ extension JavaClassTranslator {
return protocolDecl.formatted(using: translator.format).cast(DeclSyntax.self)
}

/// A single Swift attribute derived from a Java annotation.
struct SwiftAttribute {
/// The attribute text, e.g. `@available(*, deprecated)`.
var value: String

/// The minimum Swift compiler version required to compile this attribute,
/// e.g. `(6, 3)` for `@available(Android ...)`. When non-nil the attribute
/// is wrapped in a `#if compiler(>=…)` block during rendering.
var minimumCompilerVersion: (Int, Int)? = nil
}

struct SwiftAvailableAttributes {
var attributes: [SwiftAttribute] = []

func render() -> String {
if attributes.isEmpty {
return ""
}
var lines: [String] = []
for attr in attributes {
if let (major, minor) = attr.minimumCompilerVersion {
lines.append("#if compiler(>=\(major).\(minor))")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's the right check we need

lines.append(attr.value)
lines.append("#endif")
} else {
lines.append(attr.value)
}
}
return lines.joined(separator: "\n") + "\n"
}
}

/// Build Swift `@available` attributes from Java annotations on a reflective element.
private func swiftAvailableAttributes(from annotations: [Annotation]) -> SwiftAvailableAttributes {
var result = SwiftAvailableAttributes()

for annotation in annotations {
guard let annotationClass = annotation.annotationType() else { continue }

if annotationClass.isKnown(.javaLangDeprecated) {
result.attributes.append(SwiftAttribute(value: "@available(*, deprecated)"))
continue
}

if annotationClass.isKnown(.androidxRequiresApi) || annotationClass.isKnown(.androidSupportRequiresApi) {
func apiLevelComment(_ level: Int32) -> String {
AndroidAPILevel(rawValue: Int(level)).map { " /* \($0.name) */" } ?? ""
}

// The annotation proxy exposes api() which returns the resolved integer value.
// Build.VERSION_CODES constants are resolved at compile time by javac.
let apiLevel = try? annotation.as(JavaObject.self)
.dynamicJavaMethodCall(methodName: "api", resultType: Int32.self)
if let apiLevel, apiLevel > 1 {
result.attributes.append(
SwiftAttribute(
value: "@available(Android \(apiLevel)\(apiLevelComment(apiLevel)), *)",
minimumCompilerVersion: (6, 3)
)
)
continue
}

let value = try? annotation.as(JavaObject.self)
.dynamicJavaMethodCall(methodName: "value", resultType: Int32.self)
if let value, value > 1 {
result.attributes.append(
SwiftAttribute(
value: "@available(Android \(value)\(apiLevelComment(value)), *)",
minimumCompilerVersion: (6, 3)
)
)
continue
}
}
}

return result
}

func renderAnnotationExtensions() -> [DeclSyntax] {
var extensions: [DeclSyntax] = []

for annotation in annotations {
let annotationName = annotation.annotationType().getName().splitSwiftTypeName().name
if annotationName == "ThreadSafe" || annotationName == "Immutable" { // If we are threadsafe, mark as unchecked Sendable
guard let annotationClass = annotation.annotationType() else { continue }
if annotationClass.isKnown(.threadSafe) || annotationClass.isKnown(.immutable) {
extensions.append(
"""
extension \(raw: swiftTypeName): @unchecked Swift.Sendable { }
"""
)
} else if annotationName == "NotThreadSafe" { // If we are _not_ threadsafe, mark sendable unavailable
} else if annotationClass.isKnown(.notThreadSafe) {
extensions.append(
"""
@available(unavailable, *)
Expand All @@ -625,10 +706,12 @@ extension JavaClassTranslator {
let accessModifier = javaConstructor.isPublic ? "public " : ""
let convenienceModifier = translateAsClass ? "convenience " : ""
let nonoverrideAttribute = translateAsClass ? "@_nonoverride " : ""
let constructorAnnotations = javaConstructor.getDeclaredAnnotations().compactMap(\.self)
let availableAttributes = swiftAvailableAttributes(from: constructorAnnotations)

// FIXME: handle generics in constructors
return """
@JavaMethod
\(raw: availableAttributes.render())@JavaMethod
\(raw: nonoverrideAttribute)\(raw: accessModifier)\(raw: convenienceModifier)init(\(raw: parametersStr))\(raw: throwsStr)
"""
}
Expand Down Expand Up @@ -747,6 +830,10 @@ extension JavaClassTranslator {
/// ```
"""

// --- Handle @available attributes from Java annotations (e.g. @Deprecated, @RequiresApi)
let methodAnnotations = javaMethod.getDeclaredAnnotations().compactMap(\.self)
let availableAttributes = swiftAvailableAttributes(from: methodAnnotations)

// Compute the parameters for '@...JavaMethod(...)'
let methodAttribute: AttributeSyntax
if implementedInSwift {
Expand Down Expand Up @@ -817,7 +904,7 @@ extension JavaClassTranslator {
return
"""
\(raw: docsString)
\(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClauseStr)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause)
\(raw: availableAttributes.render())\(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClauseStr)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause)

\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftOptionalMethodName)\(raw: genericParameterClauseStr)(\(raw: parameters.map(\.clause.description).joined(separator: ", ")))\(raw: throwsStr) -> \(raw: resultOptional)\(raw: whereClause) {
\(body)
Expand All @@ -827,7 +914,7 @@ extension JavaClassTranslator {
return
"""
\(raw: docsString)
\(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClauseStr)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause)
\(raw: availableAttributes.render())\(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClauseStr)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause)
"""
}
}
Expand All @@ -842,6 +929,8 @@ extension JavaClassTranslator {
)
let fieldAttribute: AttributeSyntax = javaField.isStatic ? "@JavaStaticField" : "@JavaField"
let swiftFieldName = javaField.getName().escapedSwiftName
let fieldAnnotations = javaField.getDeclaredAnnotations().compactMap(\.self)
let availableAttributes = swiftAvailableAttributes(from: fieldAnnotations)

if let optionalType = typeName.optionalWrappedType() {
let setter =
Expand All @@ -856,7 +945,7 @@ extension JavaClassTranslator {
""
}
return """
\(fieldAttribute)(isFinal: \(raw: javaField.isFinal))
\(raw: availableAttributes.render())\(fieldAttribute)(isFinal: \(raw: javaField.isFinal))
public var \(raw: swiftFieldName): \(raw: typeName)


Expand All @@ -868,7 +957,7 @@ extension JavaClassTranslator {
"""
} else {
return """
\(fieldAttribute)(isFinal: \(raw: javaField.isFinal))
\(raw: availableAttributes.render())\(fieldAttribute)(isFinal: \(raw: javaField.isFinal))
public var \(raw: swiftFieldName): \(raw: typeName)
"""
}
Expand Down
59 changes: 59 additions & 0 deletions Sources/SwiftJavaToolLib/KnownJavaAnnotation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 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 JavaLangReflect
import SwiftJava

/// Well-known Java annotation types that the Swift wrapper generator handles
/// during code generation.
public enum KnownJavaAnnotation: String {
case javaLangDeprecated = "java.lang.Deprecated"
case androidxRequiresApi = "androidx.annotation.RequiresApi"
case androidSupportRequiresApi = "android.support.annotation.RequiresApi"

// Thread-safety annotations (may originate from javax.annotation.concurrent
// or net.jcip.annotations; matched by simple name). If someone made their own
// but used the same names, we just assume they meant the same meaning -- it'd
// be wild to call an annotation ThreadSafe and not have it mean that :-)
case threadSafe = "ThreadSafe"
case immutable = "Immutable"
case notThreadSafe = "NotThreadSafe"

/// Whether this case should be matched by simple (unqualified) class name
/// rather than the fully-qualified name.
private var matchesBySimpleName: Bool {
switch self {
case .threadSafe, .immutable, .notThreadSafe:
return true
default:
return false
}
}

/// Check whether the given fully-qualified annotation class name matches
/// this known annotation.
func matches(fullyQualifiedName fqn: String) -> Bool {
if matchesBySimpleName {
return fqn.splitSwiftTypeName().name == rawValue
}
return fqn == rawValue
}
}

extension JavaClass<Annotation> {
/// Check whether this annotation class matches a known annotation type.
func isKnown(_ known: KnownJavaAnnotation) -> Bool {
known.matches(fullyQualifiedName: self.getName())
}
}
Loading
Loading