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= :` (https://openjdk.org/jeps/413) + /// - JDK 17 and below: `

{@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 "
{@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 "}
" + } + } + 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} instead of {@snippet}",
+    arguments: [
+      (
+        JExtractGenerationMode.jni,
+        [
+          """
+          /**
+           * Simple summary
+           *
+           * 

Downcall to Swift: + *

{@code
+           * public func f()
+           * }
+ */ + public static void f() { + """ + ] + ), + ( + JExtractGenerationMode.ffm, + [ + """ + /** + * Simple summary + * + *

Downcall to Swift: + *

{@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 + * + *

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 + ) + } }