diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index cb7eac727..afd32d7fe 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftJavaConfigurationShared import SwiftJavaJNICore extension FFMSwift2JavaGenerator { @@ -65,9 +66,9 @@ extension FFMSwift2JavaGenerator { printer.printBraceBlock( """ /** - * {@snippet lang=c : + * \(config.javadocCodeSnippetStart(lang: "c")) * \(cFunc.description) - * } + * \(config.javadocCodeSnippetEnd) */ private static class \(cFunc.name) """ @@ -398,6 +399,7 @@ extension FFMSwift2JavaGenerator { TranslatedDocumentation.printDocumentation( importedFunc: decl, translatedDecl: translated, + config: config, in: &printer, ) printer.printBraceBlock( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 90e8ae7ce..3377af730 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -15,6 +15,7 @@ import CodePrinting import Foundation import OrderedCollections +import SwiftJavaConfigurationShared import SwiftJavaJNICore // MARK: Defaults @@ -693,6 +694,7 @@ extension JNISwift2JavaGenerator { TranslatedDocumentation.printDocumentation( importedFunc: importedFunc, translatedDecl: translatedDecl, + config: config, in: &printer, ) } @@ -725,6 +727,7 @@ extension JNISwift2JavaGenerator { TranslatedDocumentation.printDocumentation( importedFunc: importedFunc, translatedDecl: translatedDecl, + config: config, in: &printer, ) } diff --git a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift index 54c00cdec..2db9d3270 100644 --- a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift +++ b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift @@ -13,12 +13,14 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftJavaConfigurationShared import SwiftSyntax enum TranslatedDocumentation { static func printDocumentation( importedFunc: ImportedFunc, translatedDecl: FFMSwift2JavaGenerator.TranslatedFunctionDecl, + config: Configuration, in printer: inout CodePrinter ) { var documentation = SwiftDocumentationParser.parse(importedFunc.swiftDecl) @@ -32,12 +34,13 @@ enum TranslatedDocumentation { ) } - printDocumentation(documentation, syntax: importedFunc.swiftDecl, in: &printer) + printDocumentation(documentation, syntax: importedFunc.swiftDecl, config: config, in: &printer) } static func printDocumentation( importedFunc: ImportedFunc, translatedDecl: JNISwift2JavaGenerator.TranslatedFunctionDecl, + config: Configuration, in printer: inout CodePrinter ) { var documentation = SwiftDocumentationParser.parse(importedFunc.swiftDecl) @@ -51,12 +54,13 @@ enum TranslatedDocumentation { ) } - printDocumentation(documentation, syntax: importedFunc.swiftDecl, in: &printer) + printDocumentation(documentation, syntax: importedFunc.swiftDecl, config: config, in: &printer) } private static func printDocumentation( _ parsedDocumentation: SwiftDocumentation?, syntax: some DeclSyntaxProtocol, + config: Configuration, in printer: inout CodePrinter ) { var groups = [String]() @@ -71,12 +75,13 @@ enum TranslatedDocumentation { } } + let signatureString = syntax.signatureString groups.append( """ \(parsedDocumentation != nil ? "
" : "")Downcall to Swift:
- {@snippet lang=swift :
- \(syntax.signatureString)
- }
+ \(config.javadocCodeSnippetStart(lang: "swift"))
+ \(signatureString)
+ \(config.javadocCodeSnippetEnd)
"""
)
diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift
index 43ab445ef..e4d774ae5 100644
--- a/Sources/SwiftJavaConfigurationShared/Configuration.swift
+++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift
@@ -77,6 +77,42 @@ public struct Configuration: Codable {
asyncFuncMode ?? .default
}
+ public var javaSourceLevel: JavaSourceLevel?
+ public var effectiveJavaSourceLevel: JavaSourceLevel {
+ javaSourceLevel ?? .default
+ }
+
+ /// Check whether the effective Java source level supports the given feature
+ public func supports(_ feature: JavaSourceFeature) -> Bool {
+ effectiveJavaSourceLevel >= feature.minimumJavaSourceLevel
+ }
+
+ /// Opening tag for a JavaDoc code snippet block.
+ ///
+ /// - JDK 18+: `{@snippet lang= Downcall to Swift:
+ * Downcall to Swift:
+ * Downcall to Swift:
+ * {@snippet lang=swift :
+ * public func f()
+ * }
+ */
+ public static void f() {
+ """
+ ]
+ ),
+ (
+ JExtractGenerationMode.ffm,
+ [
+ """
+ /**
+ * Simple summary
+ *
+ * Downcall to Swift:
+ * {@snippet lang=swift :
+ * public func f()
+ * }
+ */
+ public static void f() {
+ """
+ ]
+ ),
+ ]
+ )
+ func jdk22Snippets(mode: JExtractGenerationMode, expectedJavaChunks: [String]) throws {
+ let text =
+ """
+ /// Simple summary
+ public func f() {}
+ """
+
+ var config = Configuration()
+ config.javaSourceLevel = .jdk22
+
+ try assertOutput(
+ input: text,
+ config: config,
+ mode,
+ .java,
+ expectedChunks: expectedJavaChunks
+ )
+ }
}
{@code`
+ public func javadocCodeSnippetStart(lang: String) -> String {
+ // TODO: also handle ``` once we support /// style comments in JDK22+
+ if supports(.javadocSnippets) {
+ return "{@snippet lang=\(lang) :"
+ } else {
+ return ""
+ }
+ }
+
public var enableJavaCallbacks: Bool?
public var effectiveEnableJavaCallbacks: Bool {
enableJavaCallbacks ?? false
diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceFeature.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceFeature.swift
new file mode 100644
index 000000000..88cb0f176
--- /dev/null
+++ b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceFeature.swift
@@ -0,0 +1,33 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 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
+//
+//===----------------------------------------------------------------------===//
+
+/// A feature that requires a minimum Java source level.
+///
+/// Use with ``Configuration/supports(_:)`` to conditionally emit
+/// source-level-dependent constructs.
+public struct JavaSourceFeature: Sendable {
+ /// The minimum Java source level required for this feature
+ public let minimumJavaSourceLevel: JavaSourceLevel
+
+ /// Human-readable description of the feature
+ public let description: String
+}
+
+extension JavaSourceFeature {
+ /// JavaDoc `{@snippet}` tag support (JEP 413, JDK 18+)
+ public static let javadocSnippets = JavaSourceFeature(
+ minimumJavaSourceLevel: .jdk18,
+ description: "JavaDoc {@snippet} tag (JEP 413)"
+ )
+}
diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift
new file mode 100644
index 000000000..f7c37e669
--- /dev/null
+++ b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift
@@ -0,0 +1,55 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 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
+//
+//===----------------------------------------------------------------------===//
+
+/// The Java source level to target when generating Java code.
+///
+/// Controls which Java language features may appear in generated output.
+/// Encoded as a plain integer in JSON (e.g. `"javaSourceLevel": 17`).
+public enum JavaSourceLevel: Int, Comparable, Sendable {
+ case jdk17 = 17
+ case jdk18 = 18
+ case jdk21 = 21
+ case jdk22 = 22
+ case jdk24 = 24
+
+ public static var `default`: Self { .jdk22 }
+
+ public static func < (lhs: Self, rhs: Self) -> Bool {
+ lhs.rawValue < rhs.rawValue
+ }
+}
+
+// ==== -----------------------------------------------------------------------
+// MARK: Codable
+
+extension JavaSourceLevel: Codable {
+ public init(from decoder: any Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let rawValue = try container.decode(Int.self)
+ guard let level = JavaSourceLevel(rawValue: rawValue) else {
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "Unknown JavaSourceLevel: \(rawValue). Supported values: \(JavaSourceLevel.allCases.map(\.rawValue))"
+ )
+ }
+ self = level
+ }
+
+ public func encode(to encoder: any Encoder) throws {
+ var container = encoder.singleValueContainer()
+ try container.encode(rawValue)
+ }
+}
+
+extension JavaSourceLevel: CaseIterable {}
diff --git a/Tests/JExtractSwiftTests/SwiftDocumentationParsingTests.swift b/Tests/JExtractSwiftTests/SwiftDocumentationParsingTests.swift
index d49010c89..3bfb998ec 100644
--- a/Tests/JExtractSwiftTests/SwiftDocumentationParsingTests.swift
+++ b/Tests/JExtractSwiftTests/SwiftDocumentationParsingTests.swift
@@ -520,4 +520,116 @@ struct SwiftDocumentationParsingTests {
expectedChunks: expectedJavaChunks
)
}
+
+ @Test(
+ "JDK 17 fallback: {@code"
+ }
+ }
+
+ /// Closing tag for a JavaDoc code snippet block.
+ ///
+ /// - JDK 18+: `}` (https://openjdk.org/jeps/413)
+ /// - JDK 17 and below: `}`
+ public var javadocCodeSnippetEnd: String {
+ // TODO: also handle ``` once we support /// style comments in JDK22+
+ if supports(.javadocSnippets) {
+ return "}"
+ } else {
+ return "}{@code} instead of {@snippet}",
+ arguments: [
+ (
+ JExtractGenerationMode.jni,
+ [
+ """
+ /**
+ * Simple summary
+ *
+ * {@code
+ * public func f()
+ * }
+ */
+ public static void f() {
+ """
+ ]
+ ),
+ (
+ JExtractGenerationMode.ffm,
+ [
+ """
+ /**
+ * Simple summary
+ *
+ * {@code
+ * public func f()
+ * }
+ */
+ public static void f() {
+ """
+ ]
+ ),
+ ]
+ )
+ func jdk17Fallback(mode: JExtractGenerationMode, expectedJavaChunks: [String]) throws {
+ let text =
+ """
+ /// Simple summary
+ public func f() {}
+ """
+
+ var config = Configuration()
+ config.javaSourceLevel = .jdk17
+
+ try assertOutput(
+ input: text,
+ config: config,
+ mode,
+ .java,
+ expectedChunks: expectedJavaChunks
+ )
+ }
+
+ @Test(
+ "JDK 22 uses {@snippet} tags",
+ arguments: [
+ (
+ JExtractGenerationMode.jni,
+ [
+ """
+ /**
+ * Simple summary
+ *
+ *