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
51 changes: 51 additions & 0 deletions Sources/JExtractSwiftLib/JavaPackageValidation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Errors raised when `jextract` is configured without a usable Java package.
package enum JavaPackageError: Error, Equatable, CustomStringConvertible {
/// `javaPackage` was `nil`, empty, whitespace-only, or had surrounding whitespace.
case missing
/// `javaPackage` contained an empty segment, e.g. `com..foo`, `.foo`, or `foo.`.
case emptySegment(String)

package var description: String {
switch self {
case .missing:
return
"Missing or empty Java package. jextract requires a non-empty '--java-package' (or javaPackage in swift-java.config). The unnamed/default package is not supported."
case .emptySegment(let javaPackage):
return "Invalid Java package '\(javaPackage)': must not contain empty segments."
}
}
}

/// Returns `javaPackage` unchanged when it is a non-empty, non-blank package name with no empty segments.
///
/// Surrounding whitespace is rejected rather than trimmed, so malformed configuration is not silently rewritten.
package func validateJavaPackage(_ javaPackage: String?) throws -> String {
guard let javaPackage,
javaPackage == javaPackage.trimmingCharacters(in: .whitespacesAndNewlines),
!javaPackage.isEmpty
else {
throw JavaPackageError.missing
}

for component in javaPackage.split(separator: ".", omittingEmptySubsequences: false) {
if component.isEmpty {
throw JavaPackageError.emptySegment(javaPackage)
}
}

return javaPackage
}
12 changes: 4 additions & 8 deletions Sources/JExtractSwiftLib/Swift2Java.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,11 @@ public struct SwiftToJava {
fatalError("Missing '--swift-module' name.")
}

let javaPackage = try validateJavaPackage(config.javaPackage)

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

if config.javaPackage == nil || config.javaPackage!.isEmpty {
translator.log.warning(
"Configured java package is '', consider specifying concrete package for generated sources."
)
}

guard let inputSwift = config.inputSwiftDirectory else {
fatalError("Missing '--swift-input' directory!")
}
Expand Down Expand Up @@ -126,7 +122,7 @@ public struct SwiftToJava {
let generator = FFMSwift2JavaGenerator(
config: self.config,
translator: translator,
javaPackage: config.javaPackage ?? "",
javaPackage: javaPackage,
swiftOutputDirectory: outputSwiftDirectory,
javaOutputDirectory: outputJavaDirectory
)
Expand All @@ -137,7 +133,7 @@ public struct SwiftToJava {
let generator = JNISwift2JavaGenerator(
config: self.config,
translator: translator,
javaPackage: config.javaPackage ?? "",
javaPackage: javaPackage,
swiftOutputDirectory: outputSwiftDirectory,
javaOutputDirectory: outputJavaDirectory,
javaClassLookupTable: wrappedJavaClassesLookupTable,
Expand Down
2 changes: 2 additions & 0 deletions Sources/SwiftJavaConfigurationShared/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public struct Configuration: Codable {

/// The Java package the generated Java code should be emitted into.
///
/// jextract requires a non-empty Java package name. The unnamed/default package is not supported.
///
/// Example:
/// ```swift
/// "com.example.mypackage"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ The minimum log level at which log messages will be printed at by swift-java.

The Java package the generated Java code should be emitted into.

jextract requires a non-empty Java package name. The unnamed/default package is not supported.

Example:
```swift
"com.example.mypackage"
Expand Down
84 changes: 84 additions & 0 deletions Tests/JExtractSwiftTests/JavaPackageValidationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//===----------------------------------------------------------------------===//
//
// 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 SwiftJavaConfigurationShared
import Testing

@testable import JExtractSwiftLib

@Suite
struct JavaPackageValidationTests {
@Test func rejectsNil() {
#expect(throws: JavaPackageError.missing) {
try validateJavaPackage(nil)
}
}

@Test func rejectsEmpty() {
#expect(throws: JavaPackageError.missing) {
try validateJavaPackage("")
}
}

@Test func rejectsWhitespaceOnly() {
#expect(throws: JavaPackageError.missing) {
try validateJavaPackage(" ")
}
}

@Test func rejectsSurroundingWhitespace() {
#expect(throws: JavaPackageError.missing) {
try validateJavaPackage(" com.example.foo ")
}
}

@Test func rejectsEmptySegment() {
#expect(throws: JavaPackageError.emptySegment("com..foo")) {
try validateJavaPackage("com..foo")
}
}

@Test func rejectsLeadingDot() {
#expect(throws: JavaPackageError.emptySegment(".foo")) {
try validateJavaPackage(".foo")
}
}

@Test func rejectsTrailingDot() {
#expect(throws: JavaPackageError.emptySegment("foo.")) {
try validateJavaPackage("foo.")
}
}

@Test func acceptsNonEmptyPackageUnchanged() throws {
#expect(try validateJavaPackage("com.example.foo") == "com.example.foo")
}

@Test func runThrowsOnNilJavaPackage() {
var config = Configuration()
config.swiftModule = "MySwift"
#expect(throws: JavaPackageError.missing) {
try SwiftToJava(config: config, dependencyConfigs: []).run()
}
}

@Test func runThrowsOnEmptyJavaPackage() {
var config = Configuration()
config.swiftModule = "MySwift"
config.javaPackage = ""
#expect(throws: JavaPackageError.missing) {
try SwiftToJava(config: config, dependencyConfigs: []).run()
}
}
}
Loading