diff --git a/Sources/JExtractSwiftLib/JavaPackageValidation.swift b/Sources/JExtractSwiftLib/JavaPackageValidation.swift new file mode 100644 index 000000000..8bc811eef --- /dev/null +++ b/Sources/JExtractSwiftLib/JavaPackageValidation.swift @@ -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 +} diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index feff73c5f..6952f3b1e 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -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!") } @@ -126,7 +122,7 @@ public struct SwiftToJava { let generator = FFMSwift2JavaGenerator( config: self.config, translator: translator, - javaPackage: config.javaPackage ?? "", + javaPackage: javaPackage, swiftOutputDirectory: outputSwiftDirectory, javaOutputDirectory: outputJavaDirectory ) @@ -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, diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 29c556afc..c45b00b2e 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -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" diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md index 1ec58498f..bacce7ccf 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -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" diff --git a/Tests/JExtractSwiftTests/JavaPackageValidationTests.swift b/Tests/JExtractSwiftTests/JavaPackageValidationTests.swift new file mode 100644 index 000000000..c4a97ad3c --- /dev/null +++ b/Tests/JExtractSwiftTests/JavaPackageValidationTests.swift @@ -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() + } + } +}