diff --git a/Package.swift b/Package.swift index 3f07a08c2..22656a0ac 100644 --- a/Package.swift +++ b/Package.swift @@ -43,6 +43,11 @@ let package = Package( targets: ["SwiftJavaConfigurationShared"] ), + .library( + name: "SwiftExtractConfigurationShared", + targets: ["SwiftExtractConfigurationShared"] + ), + .library( name: "JavaUtil", targets: ["JavaUtil"] @@ -108,6 +113,11 @@ let package = Package( targets: ["SwiftRuntimeFunctions"] ), + .library( + name: "SwiftExtract", + targets: ["SwiftExtract"] + ), + .library( name: "JExtractSwiftLib", targets: ["JExtractSwiftLib"] @@ -271,7 +281,14 @@ let package = Package( ), .target( - name: "SwiftJavaConfigurationShared" + name: "SwiftJavaConfigurationShared", + dependencies: [ + "SwiftExtractConfigurationShared" + ] + ), + + .target( + name: "SwiftExtractConfigurationShared" ), .target( @@ -336,6 +353,33 @@ let package = Package( ] ), + .target( + name: "SwiftExtract", + dependencies: [ + .product(name: "SwiftBasicFormat", package: "swift-syntax"), + .product(name: "SwiftIfConfig", package: "swift-syntax"), + .product(name: "SwiftLexicalLookup", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + .product(name: "Logging", package: "swift-log"), + "SwiftExtractConfigurationShared", + ], + path: "Sources/SwiftExtract", + resources: [ + // Holds the `dummy.json` placeholder so SwiftPM emits a `Bundle.module` + // for this target. The real `static-build-config.json` is generated at + // build time by the `_StaticBuildConfigPlugin` build tool below. + .process("Resources") + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ], + plugins: [ + .plugin(name: "_StaticBuildConfigPlugin") + ] + ), + .target( name: "JExtractSwiftLib", dependencies: [ @@ -347,19 +391,14 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "SwiftJavaJNICore", package: "swift-java-jni-core"), + "SwiftExtract", "SwiftJavaShared", "SwiftJavaConfigurationShared", "CodePrinting", ], - resources: [ - .process("Resources") - ], swiftSettings: [ .swiftLanguageMode(.v5), .enableUpcomingFeature("BareSlashRegexLiterals"), - ], - plugins: [ - .plugin(name: "_StaticBuildConfigPlugin") ] ), @@ -435,6 +474,7 @@ let package = Package( name: "JExtractSwiftTests", dependencies: [ "JExtractSwiftLib", + "SwiftExtract", "CodePrinting", ], swiftSettings: [ @@ -442,6 +482,15 @@ let package = Package( ] ), + .testTarget( + name: "SwiftExtractTests", + dependencies: [ + "SwiftExtract", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), + ] + ), + .testTarget( name: "SwiftRuntimeFunctionsTests", dependencies: [ diff --git a/Plugins/PluginsShared/SwiftExtractConfigurationShared b/Plugins/PluginsShared/SwiftExtractConfigurationShared new file mode 120000 index 000000000..263f08269 --- /dev/null +++ b/Plugins/PluginsShared/SwiftExtractConfigurationShared @@ -0,0 +1 @@ +../../Sources/SwiftExtractConfigurationShared \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/AnalysisResult.swift b/Sources/JExtractSwiftLib/AnalysisResult.swift deleted file mode 100644 index 4d33bea19..000000000 --- a/Sources/JExtractSwiftLib/AnalysisResult.swift +++ /dev/null @@ -1,19 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -struct AnalysisResult { - let importedTypes: [String: ImportedNominalType] - let importedGlobalVariables: [ImportedFunc] - let importedGlobalFuncs: [ImportedFunc] -} diff --git a/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift b/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift index 7f540e527..2c733cb0e 100644 --- a/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift +++ b/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift new file mode 100644 index 000000000..d55e59675 --- /dev/null +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 SwiftExtract +import SwiftJavaConfigurationShared + +/// Bridges swift-java's `Configuration` onto the language-neutral +/// `SwiftExtractConfiguration` surface consumed by `SwiftExtract`. +/// +/// Most members are satisfied directly by `Configuration`'s own properties. +/// `Configuration` shares `AccessLevelMode` with the analyzer (both pull it in +/// from `SwiftExtractConfigurationShared`), so `swiftExtractAccessLevel` is a +/// straight passthrough. Only `swiftExtractLogLevel` needs a mapping from +/// swift-java's `LogLevel` onto the neutral `Logger.Level`. +extension Configuration: SwiftExtractConfiguration { + public var swiftExtractAccessLevel: AccessLevelMode { + effectiveMinimumInputAccessLevelMode + } + + public var swiftExtractLogLevel: SwiftExtract.Logger.Level? { + guard let logLevel else { return nil } + switch logLevel { + case .trace: return .trace + case .debug: return .debug + case .info: return .info + case .notice: return .notice + case .warning: return .warning + case .error: return .error + case .critical: return .critical + } + } + + // swift-java targets Java, which cannot express Swift operators or + // construct open generic types directly: leave both knobs off + public var extractsOperators: Bool { false } + public var extractsGenericTypeInitializers: Bool { false } +} + +extension LogLevel { + /// Bridges from the analysis layer's neutral `Logger.Level` (used by the CLI's + /// `--log-level` option) onto swift-java's own `LogLevel`. + public init(_ level: SwiftExtract.Logger.Level) { + switch level { + case .trace: self = .trace + case .debug: self = .debug + case .info: self = .info + case .notice: self = .notice + case .warning: self = .warning + case .error: self = .error + case .critical: self = .critical + } + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index b2d5394a5..023910767 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaJNICore extension JavaType { diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+JNIExtensions.swift similarity index 65% rename from Sources/JExtractSwiftLib/Convenience/String+Extensions.swift rename to Sources/JExtractSwiftLib/Convenience/String+JNIExtensions.swift index 9fdb6a64c..d317d1b06 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+JNIExtensions.swift @@ -15,34 +15,7 @@ import SwiftJavaJNICore extension String { - - var firstCharacterUppercased: String { - guard let f = first else { - return self - } - - return "\(f.uppercased())\(String(dropFirst()))" - } - - var firstCharacterLowercased: String { - guard let f = first else { - return self - } - - return "\(f.lowercased())\(String(dropFirst()))" - } - - /// Returns whether the string is of the format `isX` - var hasJavaBooleanNamingConvention: Bool { - guard self.hasPrefix("is"), self.count > 2 else { - return false - } - - let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2) - return self[thirdCharacterIndex].isUppercase - } - - /// Returns a version of the string correctly escaped for a JNI + /// Returns a version of the string correctly escaped for a JNI identifier var escapedJNIIdentifier: String { self.map { if $0 == "_" { @@ -66,15 +39,6 @@ extension String { .joined() } - /// If the string ends with `.swift`, return it without that suffix; - /// otherwise return self unchanged - func dropSwiftFileSuffix() -> String { - if hasSuffix(".swift") { - return String(dropLast(".swift".count)) - } - return self - } - /// Looks up self as a SwiftJava wrapped class name and converts it /// into a `JavaType.class` if it exists in `lookupTable`. func parseJavaClassFromSwiftJavaName(in lookupTable: [String: String]) -> JavaType? { @@ -87,14 +51,6 @@ extension String { return .class(package: javaPackageName, name: javaClassName) } - - /// Unescapes the name if it is surrounded by backticks. - var unescapedSwiftName: String { - if count >= 2 && hasPrefix("`") && hasSuffix("`") { - return String(dropFirst().dropLast()) - } - return self - } } extension Array where Element == String { diff --git a/Sources/JExtractSwiftLib/Convenience/String+JavaNaming.swift b/Sources/JExtractSwiftLib/Convenience/String+JavaNaming.swift new file mode 100644 index 000000000..8d77383ef --- /dev/null +++ b/Sources/JExtractSwiftLib/Convenience/String+JavaNaming.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 +// +//===----------------------------------------------------------------------===// + +extension String { + /// Returns whether the string is of the format `isX` (Java Beans boolean + /// property naming convention) + package var hasJavaBooleanNamingConvention: Bool { + guard self.hasPrefix("is"), self.count > 2 else { + return false + } + + let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2) + return self[thirdCharacterIndex].isUppercase + } +} diff --git a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift new file mode 100644 index 000000000..d8de44734 --- /dev/null +++ b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 SwiftExtract + +// ==== ----------------------------------------------------------------------- +// MARK: Java name typealiases + +package typealias JavaClassName = String +package typealias JavaFullyQualifiedClassName = String +package typealias JavaPackageName = String + +// ==== ----------------------------------------------------------------------- +// MARK: Java-facing name aliases for ExtractedNominalType + +extension ExtractedNominalType { + package var effectiveJavaTypeName: SwiftQualifiedTypeName { effectiveOutputTypeName } + package var effectiveJavaName: String { effectiveOutputName } + package var effectiveJavaSimpleName: String { effectiveOutputSimpleName } + package var javaGenericClause: String { outputGenericClause } +} + +// ==== ----------------------------------------------------------------------- +// MARK: Java-facing name aliases for ExtractedFunc + +extension ExtractedFunc { + /// The Java getter name for a Swift property/subscript getter, following + /// Java Beans conventions: `get` for non-boolean, `is` for + /// boolean (unless the property already starts with `is`, in which case + /// the original name is preserved). + /// + /// Returns `nil` when the underlying declaration is not a getter — i.e. a + /// regular function, initializer, enum case, or setter — since those don't + /// have a Java getter name. + package var javaGetterName: String? { + switch apiKind { + case .getter, .subscriptGetter: break + case .setter, .subscriptSetter, .function, .initializer, .enumCase: return nil + } + + let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool + + if !returnsBoolean { + return "get\(self.name.firstCharacterUppercased)" + } else if !self.name.hasJavaBooleanNamingConvention { + return "is\(self.name.firstCharacterUppercased)" + } else { + return self.name + } + } + + /// The Java setter name for a Swift property/subscript setter. If the + /// property already starts with `is` (boolean naming), the `is` prefix is + /// stripped so the setter becomes `set` per Java Beans spec. + /// + /// Returns `nil` when the underlying declaration is not a setter — i.e. a + /// regular function, initializer, enum case, or getter — since those don't + /// have a Java setter name. + package var javaSetterName: String? { + switch apiKind { + case .setter, .subscriptSetter: break + case .getter, .subscriptGetter, .function, .initializer, .enumCase: return nil + } + + let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool + + if isBooleanSetter && self.name.hasJavaBooleanNamingConvention { + // Safe to force unwrap due to `hasJavaBooleanNamingConvention` check. + let propertyName = self.name.split(separator: "is", maxSplits: 1).last! + return "set\(propertyName)" + } else { + return "set\(self.name.firstCharacterUppercased)" + } + } +} diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index 58dc89263..d5956b424 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + extension CType { /// Lower the given Swift type down to a its corresponding C type. /// diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 565c5c9de..31172d11b 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaJNICore import SwiftSyntax @@ -364,10 +365,6 @@ struct CdeclLowering { case .foundationData, .essentialsData: break - case .swiftJavaError: - // SwiftJavaError is a class — treat as arbitrary nominal type below - break - default: // Unreachable? Should be handled by `CType(cdeclType:)` lowering above. throw LoweringError.unhandledType(type) diff --git a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift index 03541539c..a086828f2 100644 --- a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift +++ b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftSyntax import SwiftSyntaxBuilder diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift index 7dd054f22..a482041c5 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax @@ -23,7 +24,7 @@ import struct Foundation.URL extension FFMSwift2JavaGenerator { /// Print Java helper methods for Foundation.Data type - package func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + package func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { let typeName = decl.swiftNominal.name let thunkNameCopyBytes = "swiftjava_\(swiftModuleName)_\(typeName)_copyBytes__" diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 1357a2492..af5bb32e7 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -13,13 +13,14 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore extension FFMSwift2JavaGenerator { package func printFunctionDowncallMethods( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { guard let _ = translatedDecl(for: decl) else { // Failed to translate. Skip. @@ -39,7 +40,7 @@ extension FFMSwift2JavaGenerator { /// Print FFM Java binding descriptors for the imported Swift API. package func printJavaBindingDescriptorClass( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let thunkName = thunkNameRegistry.functionThunkName(decl: decl) let translated = self.translatedDecl(for: decl)! @@ -270,7 +271,7 @@ extension FFMSwift2JavaGenerator { /// * User-facing functional interfaces. func printJavaBindingWrapperHelperClass( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let translated = self.translatedDecl(for: decl)! let bindingDescriptorName = self.thunkNameRegistry.functionThunkName(decl: decl) @@ -359,7 +360,7 @@ extension FFMSwift2JavaGenerator { /// with adding `SwiftArena.ofAuto()` at the end. package func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let translated = self.translatedDecl(for: decl)! let methodName = translated.name @@ -420,7 +421,7 @@ extension FFMSwift2JavaGenerator { /// This assumes that all the parameters are passed-in with appropriate names. package func printDowncall( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { //=== Part 1: prepare temporary arena if needed. let translatedSignature = self.translatedDecl(for: decl)!.translatedSignature @@ -454,7 +455,7 @@ extension FFMSwift2JavaGenerator { let arena = if let className = type.className, - analysis.importedTypes[className] != nil + analysis.extractedTypes[className] != nil { // Use passed-in 'SwiftArena' for 'SwiftValue'. "swiftArena" diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index bfcc02efb..c3182c79d 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -12,12 +12,13 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore extension FFMSwift2JavaGenerator { func translatedDecl( - for decl: ImportedFunc + for decl: ExtractedFunc ) -> TranslatedFunctionDecl? { if let cached = translatedDecls[decl] { return cached @@ -175,7 +176,7 @@ extension FFMSwift2JavaGenerator { self.javaIdentifiers = javaIdentifiers } - func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { + func translate(_ decl: ExtractedFunc) throws -> TranslatedFunctionDecl { let lowering = CdeclLowering(knownTypes: knownTypes) let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature) @@ -450,10 +451,6 @@ extension FFMSwift2JavaGenerator { case .foundationData, .essentialsData: break - case .swiftJavaError: - // SwiftJavaError is a class — treat as arbitrary nominal type below - break - default: throw JavaTranslationError.unhandledType(swiftType) } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index ccecc17ee..9e39cefdc 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftSyntax import SwiftSyntaxBuilder @@ -73,20 +74,20 @@ extension FFMSwift2JavaGenerator { // We have to write all types to their corresponding output file that matches the file they were declared in, // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. - let filteredTypes: [String: ImportedNominalType] + let filteredTypes: [String: ExtractedNominalType] if let singleType = config.singleType { - filteredTypes = self.analysis.importedTypes.filter { $0.key == singleType } + filteredTypes = self.analysis.extractedTypes.filter { $0.key == singleType } } else { - filteredTypes = self.analysis.importedTypes + filteredTypes = self.analysis.extractedTypes } - for group: (key: String, value: [Dictionary.Element]) in Dictionary( + for group: (key: String, value: [Dictionary.Element]) in Dictionary( grouping: filteredTypes, by: { $0.value.sourceFilePath }, ) { log.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") - let importedTypesForThisFile = group.value + let extractedTypesForThisFile = group.value .map(\.value) .sorted(by: { $0.qualifiedName < $1.qualifiedName }) @@ -104,7 +105,7 @@ extension FFMSwift2JavaGenerator { ) self.lookupContext.symbolTable.printImportedModules(&printer) - for ty in importedTypesForThisFile { + for ty in extractedTypesForThisFile { log.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)") printer.printSeparator("Thunks for \(ty.qualifiedName)") @@ -147,7 +148,7 @@ extension FFMSwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) self.currentJavaIdentifiers = JavaIdentifierFactory( - self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + self.analysis.extractedGlobalFuncs + self.analysis.extractedGlobalVariables ) for thunk in stt.renderGlobalThunks() { @@ -156,7 +157,7 @@ extension FFMSwift2JavaGenerator { } } - public func printSwiftThunkSources(_ printer: inout CodePrinter, decl: ImportedFunc) { + public func printSwiftThunkSources(_ printer: inout CodePrinter, decl: ExtractedFunc) { let stt = SwiftThunkTranslator(self) for thunk in stt.render(forFunc: decl) { @@ -165,7 +166,7 @@ extension FFMSwift2JavaGenerator { } } - package func printSwiftThunkSources(_ printer: inout CodePrinter, ty: ImportedNominalType) throws { + package func printSwiftThunkSources(_ printer: inout CodePrinter, ty: ExtractedNominalType) throws { let stt = SwiftThunkTranslator(self) self.currentJavaIdentifiers = JavaIdentifierFactory( @@ -190,14 +191,14 @@ struct SwiftThunkTranslator { func renderGlobalThunks() -> [DeclSyntax] { var decls: [DeclSyntax] = [] decls.reserveCapacity( - st.analysis.importedGlobalVariables.count + st.analysis.importedGlobalFuncs.count + st.analysis.extractedGlobalVariables.count + st.analysis.extractedGlobalFuncs.count ) - for decl in st.analysis.importedGlobalVariables { + for decl in st.analysis.extractedGlobalVariables { decls.append(contentsOf: render(forFunc: decl)) } - for decl in st.analysis.importedGlobalFuncs { + for decl in st.analysis.extractedGlobalFuncs { decls.append(contentsOf: render(forFunc: decl)) } @@ -205,7 +206,7 @@ struct SwiftThunkTranslator { } /// Render all the thunks that make Swift methods accessible to Java. - func renderThunks(forType nominal: ImportedNominalType) -> [DeclSyntax] { + func renderThunks(forType nominal: ExtractedNominalType) -> [DeclSyntax] { var decls: [DeclSyntax] = [] decls.reserveCapacity( 1 + nominal.initializers.count + nominal.variables.count + nominal.methods.count @@ -232,7 +233,7 @@ struct SwiftThunkTranslator { } /// Accessor to get the `T.self` of the Swift type, without having to rely on mangled name lookups. - func renderSwiftTypeAccessor(_ nominal: ImportedNominalType) -> DeclSyntax { + func renderSwiftTypeAccessor(_ nominal: ExtractedNominalType) -> DeclSyntax { let funcName = SwiftKitPrinting.Names.getType( module: st.swiftModuleName, nominal: nominal, @@ -247,7 +248,7 @@ struct SwiftThunkTranslator { """ } - func render(forFunc decl: ImportedFunc) -> [DeclSyntax] { + func render(forFunc decl: ExtractedFunc) -> [DeclSyntax] { st.log.trace("Rendering thunks for: \(decl.displayName)") let thunkName = st.thunkNameRegistry.functionThunkName(decl: decl) @@ -264,7 +265,7 @@ struct SwiftThunkTranslator { } /// Render special thunks for known types like Foundation.Data - func renderSpecificTypeThunks(_ nominal: ImportedNominalType) -> [DeclSyntax] { + func renderSpecificTypeThunks(_ nominal: ExtractedNominalType) -> [DeclSyntax] { guard let knownType = nominal.swiftNominal.knownTypeKind else { return [] } @@ -278,7 +279,7 @@ struct SwiftThunkTranslator { } /// Render Swift thunks for Foundation.Data helper methods - private func renderFoundationDataThunks(_ nominal: ImportedNominalType) -> [DeclSyntax] { + private func renderFoundationDataThunks(_ nominal: ExtractedNominalType) -> [DeclSyntax] { let thunkName = "swiftjava_\(st.swiftModuleName)_\(nominal.swiftNominal.name)_copyBytes__" let qualifiedName = nominal.swiftNominal.qualifiedName diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 118f4ad20..4a9f899ce 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax @@ -37,7 +38,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { var thunkNameRegistry: ThunkNameRegistry = ThunkNameRegistry() /// Cached Java translation result. 'nil' indicates failed translation. - var translatedDecls: [ImportedFunc: TranslatedFunctionDecl?] = [:] + var translatedDecls: [ExtractedFunc: TranslatedFunctionDecl?] = [:] /// Duplicate identifier tracking for the current batch of methods being generated. var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() @@ -69,7 +70,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { package init( config: Configuration, - translator: Swift2JavaTranslator, + translator: SwiftAnalyzer, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, @@ -85,7 +86,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { // If we are forced to write empty files, construct the expected outputs. // It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway. - if translator.config.effectiveWriteEmptyFiles { + if config.effectiveWriteEmptyFiles { self.expectedOutputSwiftFileNames = Set( translator.inputs.compactMap { (input) -> String? in guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { @@ -151,8 +152,8 @@ extension FFMSwift2JavaGenerator { ] /// Returns the Java class name for a nominal type, applying known-type overrides - func javaClassName(for decl: ImportedNominalType) -> String { - if decl.swiftNominal.knownTypeKind == .swiftJavaError { + func javaClassName(for decl: ExtractedNominalType) -> String { + if decl.swiftNominal.isSwiftJavaErrorType { return JavaType.swiftJavaErrorException.className! } return decl.swiftNominal.name @@ -170,13 +171,13 @@ extension FFMSwift2JavaGenerator { /// Every imported public type becomes a public class in its own file in Java. package func writeExportedJavaSources(printer: inout CodePrinter) throws { - let typesToExport: [(key: String, value: ImportedNominalType)] + let typesToExport: [(key: String, value: ExtractedNominalType)] if let singleType = config.singleType { - typesToExport = analysis.importedTypes + typesToExport = analysis.extractedTypes .filter { $0.key == singleType } .sorted(by: { $0.key < $1.key }) } else { - typesToExport = analysis.importedTypes + typesToExport = analysis.extractedTypes .sorted(by: { $0.key < $1.key }) } @@ -226,24 +227,24 @@ extension FFMSwift2JavaGenerator { printImports(&printer) self.currentJavaIdentifiers = JavaIdentifierFactory( - self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + self.analysis.extractedGlobalFuncs + self.analysis.extractedGlobalVariables ) printModuleClass(&printer) { printer in - for decl in analysis.importedGlobalVariables { + for decl in analysis.extractedGlobalVariables { self.log.trace("Print imported decl: \(decl)") printFunctionDowncallMethods(&printer, decl) } - for decl in analysis.importedGlobalFuncs { + for decl in analysis.extractedGlobalFuncs { self.log.trace("Print imported decl: \(decl)") printFunctionDowncallMethods(&printer, decl) } } } - func printImportedNominal(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + func printImportedNominal(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printHeader(&printer) printPackage(&printer) printImports(&printer) // TODO: we could have some imports be driven from types used in the generated decl @@ -252,7 +253,7 @@ extension FFMSwift2JavaGenerator { decl.initializers + decl.variables + decl.methods ) - let isErrorType = decl.swiftNominal.knownTypeKind == .swiftJavaError + let isErrorType = decl.swiftNominal.isSwiftJavaErrorType self.currentSymbolLookup = isErrorType ? .swiftRuntime : .module printNominal(&printer, decl) { printer in @@ -386,10 +387,10 @@ extension FFMSwift2JavaGenerator { func printNominal( _ printer: inout CodePrinter, - _ decl: ImportedNominalType, + _ decl: ExtractedNominalType, body: (inout CodePrinter) -> Void, ) { - let isErrorType = decl.swiftNominal.knownTypeKind == .swiftJavaError + let isErrorType = decl.swiftNominal.isSwiftJavaErrorType let baseClass: String let parentProtocol: String @@ -424,8 +425,8 @@ extension FFMSwift2JavaGenerator { /// Returns a closure that prints the constructor and related extras for special nominal types /// (e.g. error types), or `nil` for normal types that use the default layout + constructor - func getSpecialNominalConstructorPrinting(_ decl: ImportedNominalType) -> ((inout CodePrinter) -> Void)? { - if decl.swiftNominal.knownTypeKind == .swiftJavaError { + func getSpecialNominalConstructorPrinting(_ decl: ExtractedNominalType) -> ((inout CodePrinter) -> Void)? { + if decl.swiftNominal.isSwiftJavaErrorType { return { printer in // Error constructor: wrap the opaque pointer so it becomes a pointer-to-reference // (matching the convention used by normal class instance thunks) @@ -448,8 +449,8 @@ extension FFMSwift2JavaGenerator { /// Returns a closure that prints post-members extras for special nominal types /// (e.g. `fetchDescription` for error types), or `nil` for normal types that use `toString()` - func getSpecialNominalPostMembersPrinting(_ decl: ImportedNominalType) -> ((inout CodePrinter) -> Void)? { - if decl.swiftNominal.knownTypeKind == .swiftJavaError { + func getSpecialNominalPostMembersPrinting(_ decl: ExtractedNominalType) -> ((inout CodePrinter) -> Void)? { + if decl.swiftNominal.isSwiftJavaErrorType { return { printer in // Error types inherit toString() from Exception; print fetchDescription helper instead self.printSwiftJavaErrorFetchDescriptionMethod(&printer, decl) @@ -568,7 +569,7 @@ extension FFMSwift2JavaGenerator { ) } - private func printClassMemoryLayout(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printClassMemoryLayout(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printer.print( """ public static final GroupLayout $LAYOUT = (GroupLayout) SwiftValueWitnessTable.layoutOfSwiftType(TYPE_METADATA.$memorySegment()); @@ -581,7 +582,7 @@ extension FFMSwift2JavaGenerator { func printToStringMethod( _ printer: inout CodePrinter, - _ decl: ImportedNominalType, + _ decl: ExtractedNominalType, ) { printer.print( """ @@ -598,7 +599,7 @@ extension FFMSwift2JavaGenerator { } /// Print special helper methods for known types like Foundation.Data - func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { guard let knownType = decl.swiftNominal.knownTypeKind else { return } @@ -614,7 +615,7 @@ extension FFMSwift2JavaGenerator { /// Print the `fetchDescription` static helper for SwiftJavaError. /// This calls the `errorDescription()` downcall to get the error message /// for the super constructor - func printSwiftJavaErrorFetchDescriptionMethod(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + func printSwiftJavaErrorFetchDescriptionMethod(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { // Find the errorDescription method's thunk name let errorDescMethod = decl.methods.first { $0.name == "errorDescription" } guard let errorDescMethod, let _ = translatedDecl(for: errorDescMethod) else { diff --git a/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift b/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift deleted file mode 100644 index 65adf8ff5..000000000 --- a/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift +++ /dev/null @@ -1,103 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 Foundation -import SwiftIfConfig -import SwiftSyntax - -/// A default, fixed build configuration during static analysis for interface extraction. -struct JExtractDefaultBuildConfiguration: BuildConfiguration { - static let shared = JExtractDefaultBuildConfiguration() - - private var base: StaticBuildConfiguration - - init() { - guard let url = Bundle.module.url(forResource: "static-build-config", withExtension: "json") else { - fatalError("static-build-config.json is not found in module bundle") - } - do { - let data = try Data(contentsOf: url) - let decoder = JSONDecoder() - base = try decoder.decode(StaticBuildConfiguration.self, from: data) - } catch { - fatalError("\(error)") - } - } - - func isCustomConditionSet(name: String) throws -> Bool { - base.isCustomConditionSet(name: name) - } - - func hasFeature(name: String) throws -> Bool { - base.hasFeature(name: name) - } - - func hasAttribute(name: String) throws -> Bool { - base.hasAttribute(name: name) - } - - func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { - try base.canImport(importPath: importPath, version: version) - } - - func isActiveTargetOS(name: String) throws -> Bool { - true - } - - func isActiveTargetArchitecture(name: String) throws -> Bool { - true - } - - func isActiveTargetEnvironment(name: String) throws -> Bool { - true - } - - func isActiveTargetRuntime(name: String) throws -> Bool { - true - } - - func isActiveTargetPointerAuthentication(name: String) throws -> Bool { - true - } - - func isActiveTargetObjectFormat(name: String) throws -> Bool { - true - } - - var targetPointerBitWidth: Int { - base.targetPointerBitWidth - } - - var targetAtomicBitWidths: [Int] { - base.targetAtomicBitWidths - } - - var endianness: Endianness { - base.endianness - } - - var languageVersion: VersionTuple { - base.languageVersion - } - - var compilerVersion: VersionTuple { - base.compilerVersion - } -} - -extension BuildConfiguration where Self == JExtractDefaultBuildConfiguration { - static var jextractDefault: JExtractDefaultBuildConfiguration { - .shared - } -} diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index 4ce16af9a..4173d26f7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -12,8 +12,10 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + enum JNICaching { - static func cacheName(for type: ImportedNominalType) -> String { + static func cacheName(for type: ExtractedNominalType) -> String { cacheName(for: type.effectiveJavaTypeName) } @@ -21,7 +23,7 @@ enum JNICaching { cacheName(for: type.nominalTypeDecl.qualifiedTypeName) } - static func bridgeName(for type: ImportedNominalType) -> String { + static func bridgeName(for type: ExtractedNominalType) -> String { bridgeName(for: type.swiftNominal.qualifiedTypeName) } @@ -37,7 +39,7 @@ enum JNICaching { "_JNIBridge_\(typeName.fullFlatName)" } - static func cacheMemberName(for enumCase: ImportedEnumCase) -> String { + static func cacheMemberName(for enumCase: ExtractedEnumCase) -> String { "\(enumCase.enumType.nominalTypeDecl.name.firstCharacterLowercased)\(enumCase.name.firstCharacterUppercased)Cache" } diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index 84fabeb77..3e7821d48 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore @@ -52,8 +53,7 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID, - .swiftJavaError: + .foundationUUID, .essentialsUUID: return nil } } @@ -79,8 +79,7 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID, - .swiftJavaError: + .foundationUUID, .essentialsUUID: nil } } @@ -106,8 +105,7 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID, - .swiftJavaError: + .foundationUUID, .essentialsUUID: nil } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 3d36ee34f..da4a6216f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax @@ -20,9 +21,9 @@ import SwiftSyntax extension JNISwift2JavaGenerator { func generateInterfaceWrappers( - _ types: [ImportedNominalType] - ) -> [ImportedNominalType: JavaInterfaceSwiftWrapper] { - var wrappers = [ImportedNominalType: JavaInterfaceSwiftWrapper]() + _ types: [ExtractedNominalType] + ) -> [ExtractedNominalType: JavaInterfaceSwiftWrapper] { + var wrappers = [ExtractedNominalType: JavaInterfaceSwiftWrapper]() for type in types where type.swiftNominal.kind == .protocol { // Skip protocols that have a known representative concrete type (e.g. DataProtocol). @@ -50,7 +51,7 @@ extension JNISwift2JavaGenerator { let protocolType: SwiftNominalType let functions: [Function] let variables: [Variable] - let importedType: ImportedNominalType + let importedType: ExtractedNominalType var wrapperName: String { protocolType.nominalTypeDecl.javaInterfaceSwiftProtocolWrapperName @@ -101,7 +102,7 @@ extension JNISwift2JavaGenerator { } struct JavaInterfaceProtocolWrapperGenerator { - func generate(for type: ImportedNominalType) throws -> JavaInterfaceSwiftWrapper { + func generate(for type: ExtractedNominalType) throws -> JavaInterfaceSwiftWrapper { if !type.initializers.isEmpty || type.methods.contains(where: \.isStatic) || type.variables.contains(where: \.isStatic) @@ -163,7 +164,7 @@ extension JNISwift2JavaGenerator { ) } - private func translate(function: ImportedFunc) throws -> JavaInterfaceSwiftWrapper.Function { + private func translate(function: ExtractedFunc) throws -> JavaInterfaceSwiftWrapper.Function { let parameters = try function.functionSignature.parameters.map { try self.translateParameter($0) } @@ -180,8 +181,8 @@ extension JNISwift2JavaGenerator { } private func translateVariable( - getter: ImportedFunc, - setter: ImportedFunc? + getter: ExtractedFunc, + setter: ExtractedFunc? ) throws -> JavaInterfaceSwiftWrapper.Variable { try JavaInterfaceSwiftWrapper.Variable( swiftDecl: getter.swiftDecl, // they should be the same diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index d6191a475..6a3ad1e5f 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 SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore @@ -42,13 +43,13 @@ extension JNISwift2JavaGenerator { } package func writeExportedJavaSources(_ printer: inout CodePrinter) throws { - let typesToExport: [(key: String, value: ImportedNominalType)] + let typesToExport: [(key: String, value: ExtractedNominalType)] if let singleType = config.singleType { - typesToExport = analysis.importedTypes + typesToExport = analysis.extractedTypes .filter { $0.key == singleType } .sorted(by: { $0.key < $1.key }) } else { - typesToExport = analysis.importedTypes + typesToExport = analysis.extractedTypes .sorted(by: { $0.key < $1.key }) } @@ -105,7 +106,7 @@ extension JNISwift2JavaGenerator { printImports(&printer) self.currentJavaIdentifiers = JavaIdentifierFactory( - self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + self.analysis.extractedGlobalFuncs + self.analysis.extractedGlobalVariables ) printModuleClass(&printer) { printer in @@ -139,13 +140,13 @@ extension JNISwift2JavaGenerator { ) } - for decl in analysis.importedGlobalFuncs { + for decl in analysis.extractedGlobalFuncs { self.logger.trace("Print global function: \(decl)") printFunctionDowncallMethods(&printer, decl) printer.println() } - for decl in analysis.importedGlobalVariables { + for decl in analysis.extractedGlobalVariables { self.logger.trace("Print global variable: \(decl)") printFunctionDowncallMethods(&printer, decl) printer.println() @@ -153,7 +154,7 @@ extension JNISwift2JavaGenerator { } } - private func printImportedNominal(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printImportedNominal(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printHeader(&printer) printPackage(&printer) printImports(&printer) @@ -170,7 +171,7 @@ extension JNISwift2JavaGenerator { } } - private func printProtocol(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printProtocol(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { var extends = self.inheritedProtocols(of: decl).map(\.effectiveJavaSimpleName) // If we cannot generate Swift wrappers @@ -206,7 +207,7 @@ extension JNISwift2JavaGenerator { } } - private func printConcreteType(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printConcreteType(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printNominal(&printer, decl) { printer in printer.print( """ @@ -244,7 +245,7 @@ extension JNISwift2JavaGenerator { ) } - let nestedTypes = self.analysis.importedTypes.filter { _, type in + let nestedTypes = self.analysis.extractedTypes.filter { _, type in type.parent == decl.swiftNominal } @@ -398,7 +399,7 @@ extension JNISwift2JavaGenerator { } /// Prints helpers for specific types like `Foundation.Date` - private func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { guard let knownType = decl.swiftNominal.knownTypeKind else { return } switch knownType { @@ -441,7 +442,7 @@ extension JNISwift2JavaGenerator { private func printNominal( _ printer: inout CodePrinter, - _ decl: ImportedNominalType, + _ decl: ExtractedNominalType, body: (inout CodePrinter) -> Void, ) { if decl.swiftNominal.isSendable { @@ -473,7 +474,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printEnumDiscriminator(&printer, decl) printer.println() printEnumCaseInterface(&printer, decl) @@ -483,7 +484,7 @@ extension JNISwift2JavaGenerator { printEnumCases(&printer, decl) } - private func printEnumDiscriminator(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumDiscriminator(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { if decl.cases.isEmpty { return } @@ -500,7 +501,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumCaseInterface(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumCaseInterface(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { if decl.cases.isEmpty { return } @@ -557,7 +558,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { let isEffectivelyGeneric = decl.swiftNominal.isGeneric && !decl.isSpecialization if !decl.cases.isEmpty && isEffectivelyGeneric { self.logger.debug("Skipping generic static initializers in '\(decl.effectiveJavaSimpleName)'") @@ -569,7 +570,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumCases(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumCases(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { let caseTypeParameters: [JavaType] = decl.genericParameterNames.map { .class(package: nil, name: $0) } @@ -625,7 +626,7 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncallMethods( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, skipMethodBody: Bool = false, ) { guard translatedDecl(for: decl) != nil else { @@ -649,7 +650,7 @@ extension JNISwift2JavaGenerator { /// * User-facing functional interfaces. private func printJavaBindingWrapperHelperClass( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let translated = self.translatedDecl(for: decl)! if translated.functionTypes.isEmpty { @@ -686,7 +687,7 @@ extension JNISwift2JavaGenerator { private func printNecessarySupportTypes( _ printer: inout CodePrinter, - _ decl: ImportedFunc + _ decl: ExtractedFunc ) { let translatedDecl = translatedDecl(for: decl)! @@ -697,7 +698,7 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, skipMethodBody: Bool, ) { guard let translatedDecl = translatedDecl(for: decl) else { @@ -709,7 +710,7 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, - importedFunc: ImportedFunc? = nil, + importedFunc: ExtractedFunc? = nil, skipMethodBody: Bool, ) { var modifiers = ["public"] @@ -878,7 +879,7 @@ extension JNISwift2JavaGenerator { } } - private func printTypeMetadataAddressFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printTypeMetadataAddressFunction(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization if isEffectivelyGeneric { printer.print("@Override") @@ -896,7 +897,7 @@ extension JNISwift2JavaGenerator { } } - private func printFoundationDateHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printFoundationDateHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printer.print( """ /** @@ -949,7 +950,7 @@ extension JNISwift2JavaGenerator { ) } - private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printer.print( """ /** diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 2d7762bef..3012fba79 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax @@ -29,12 +30,12 @@ extension JNISwift2JavaGenerator { protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, javaIdentifiers: self.currentJavaIdentifiers, - importedTypes: self.analysis.importedTypes, + extractedTypes: self.analysis.extractedTypes, ) } func translatedDecl( - for decl: ImportedFunc + for decl: ExtractedFunc ) -> TranslatedFunctionDecl? { if let cached = translatedDecls[decl] { return cached @@ -53,7 +54,7 @@ extension JNISwift2JavaGenerator { } func translatedEnumCase( - for decl: ImportedEnumCase + for decl: ExtractedEnumCase ) -> TranslatedEnumCase? { if let cached = translatedEnumCases[decl] { return cached @@ -71,7 +72,7 @@ extension JNISwift2JavaGenerator { protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, javaIdentifiers: self.currentJavaIdentifiers, - importedTypes: self.analysis.importedTypes, + extractedTypes: self.analysis.extractedTypes, ) translated = try translation.translate(enumCase: decl) } catch { @@ -90,12 +91,12 @@ extension JNISwift2JavaGenerator { let javaClassLookupTable: JavaClassLookupTable let moduleJavaPackages: ModuleJavaPackages var knownTypes: SwiftKnownTypes - let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger var javaIdentifiers: JavaIdentifierFactory - let importedTypes: [String: ImportedNominalType] + let extractedTypes: [String: ExtractedNominalType] - func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { + func translate(enumCase: ExtractedEnumCase) throws -> TranslatedEnumCase { let methodName = "" // TODO: Used for closures, replace with better name? let parameterResults = try enumCase.parameters.enumerated().map { idx, parameter in @@ -122,9 +123,9 @@ extension JNISwift2JavaGenerator { } ) ) - let getAsCaseFunction: ImportedFunc? = + let getAsCaseFunction: ExtractedFunc? = if !enumCase.parameters.isEmpty { - ImportedFunc( + ExtractedFunc( module: enumCase.enumType.nominalTypeDecl.moduleName, swiftDecl: DeclSyntax("func getAs\(raw: javaCaseClassName)() -> (\(raw: associatedValueTypes))?"), name: "getAs\(javaCaseClassName)", @@ -151,7 +152,7 @@ extension JNISwift2JavaGenerator { ) } - func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { + func translate(_ decl: ExtractedFunc) throws -> TranslatedFunctionDecl { let nativeTranslation = NativeJavaTranslation( config: self.config, javaPackage: self.javaPackage, @@ -1578,12 +1579,12 @@ extension JNISwift2JavaGenerator { let name: String /// The oringinal enum case. - let original: ImportedEnumCase + let original: ExtractedEnumCase /// A list of the translated associated values let parameters: [JavaParameter] - let getAsCaseFunction: ImportedFunc? + let getAsCaseFunction: ExtractedFunc? /// Returns whether the associated values require an arena let requiresSwiftArena: Bool diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index c0cdd7a06..1009f4c79 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore @@ -23,7 +24,7 @@ extension JNISwift2JavaGenerator { let javaPackage: String let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes - let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger /// Translates a Swift function into the native JNI method signature. diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 823fe7485..21063d3dc 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaJNICore import SwiftSyntax @@ -78,27 +79,27 @@ extension JNISwift2JavaGenerator { // We have to write all types to their corresponding output file that matches the file they were declared in, // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. - let filteredTypes: [String: ImportedNominalType] + let filteredTypes: [String: ExtractedNominalType] if let singleType = config.singleType { - filteredTypes = self.analysis.importedTypes.filter { $0.key == singleType } + filteredTypes = self.analysis.extractedTypes.filter { $0.key == singleType } } else { - filteredTypes = self.analysis.importedTypes + filteredTypes = self.analysis.extractedTypes } - for group: (key: String, value: [Dictionary.Element]) in Dictionary( + for group: (key: String, value: [Dictionary.Element]) in Dictionary( grouping: filteredTypes, by: { $0.value.sourceFilePath }, ) { logger.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") - let importedTypesForThisFile = group.value + let extractedTypesForThisFile = group.value .map(\.value) .sorted(by: { $0.qualifiedName < $1.qualifiedName }) let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift") - for ty in importedTypesForThisFile { + for ty in extractedTypesForThisFile { logger.info("Printing Swift thunks for type: \(ty.effectiveJavaName.bold)") printer.printSeparator("Thunks for \(ty.effectiveJavaName)") @@ -291,21 +292,21 @@ extension JNISwift2JavaGenerator { printHeader(&printer) self.currentJavaIdentifiers = JavaIdentifierFactory( - self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + self.analysis.extractedGlobalFuncs + self.analysis.extractedGlobalVariables ) - for decl in analysis.importedGlobalFuncs { + for decl in analysis.extractedGlobalFuncs { printSwiftFunctionThunk(&printer, decl) printer.println() } - for decl in analysis.importedGlobalVariables { + for decl in analysis.extractedGlobalVariables { printSwiftFunctionThunk(&printer, decl) printer.println() } } - private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { + private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) throws { printHeader(&printer) printer.println() @@ -321,7 +322,7 @@ extension JNISwift2JavaGenerator { } } - private func printConcreteTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printConcreteTypeThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { // Specialized types are treated as concrete even if the underlying Swift type is generic let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization @@ -370,7 +371,7 @@ extension JNISwift2JavaGenerator { printer.println() } - private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { + private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) throws { guard let protocolWrapper = self.interfaceProtocolWrappers[type] else { return } @@ -378,7 +379,7 @@ extension JNISwift2JavaGenerator { try printSwiftInterfaceWrapper(&printer, protocolWrapper) } - private func printEnumRawDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printEnumRawDiscriminator(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { if type.cases.isEmpty { return } @@ -394,7 +395,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumCase(_ printer: inout CodePrinter, _ enumType: ImportedNominalType, _ enumCase: ImportedEnumCase) { + private func printEnumCase(_ printer: inout CodePrinter, _ enumType: ExtractedNominalType, _ enumCase: ExtractedEnumCase) { guard let translatedCase = self.translatedEnumCase(for: enumCase) else { return } @@ -417,7 +418,7 @@ extension JNISwift2JavaGenerator { private func printEnumGetAsCaseThunk( _ printer: inout CodePrinter, - _ enumType: ImportedNominalType, + _ enumType: ExtractedNominalType, _ enumCase: TranslatedEnumCase, ) { if let getAsCaseFunction = enumCase.getAsCaseFunction { @@ -442,7 +443,7 @@ extension JNISwift2JavaGenerator { private func printSwiftFunctionThunk( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { guard let translatedDecl = translatedDecl(for: decl) else { // Failed to translate. Skip. @@ -465,7 +466,7 @@ extension JNISwift2JavaGenerator { private func printSwiftFunctionHelperClasses( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let protocolParameters = decl.functionSignature.parameters.compactMap { parameter in if let concreteType = parameter.type.typeIn( @@ -498,7 +499,7 @@ extension JNISwift2JavaGenerator { // we generate a Swift class that conforms to all of those. for (parameter, protocolTypes) in protocolParameters { let protocolWrappers: [JavaInterfaceSwiftWrapper] = protocolTypes.compactMap { protocolType in - guard let importedType = self.asImportedNominalTypeDecl(protocolType), + guard let importedType = self.asExtractedNominalTypeDecl(protocolType), let wrapper = self.interfaceProtocolWrappers[importedType] else { return nil @@ -544,8 +545,8 @@ extension JNISwift2JavaGenerator { } } - private func asImportedNominalTypeDecl(_ type: SwiftType) -> ImportedNominalType? { - self.analysis.importedTypes.first( + private func asExtractedNominalTypeDecl(_ type: SwiftType) -> ExtractedNominalType? { + self.analysis.extractedTypes.first( where: ({ name, nominalType in nominalType.swiftType == type }) @@ -556,7 +557,7 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncall( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { guard let translatedDecl = self.translatedDecl(for: decl) else { fatalError("Cannot print function downcall for a function that can't be translated: \(decl)") @@ -782,7 +783,7 @@ extension JNISwift2JavaGenerator { } } - private func printJNICache(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printJNICache(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let cacheName = JNICaching.cacheName(for: type) let jniClassName = "\(javaPackagePath)/\(type.effectiveJavaTypeName.jniEscapedName)" let isEffectivelyGeneric = type.swiftNominal.isGeneric && type.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName @@ -819,7 +820,7 @@ extension JNISwift2JavaGenerator { } } - private func printNominalJavaBridge(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printNominalJavaBridge(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let bridgeName = JNICaching.bridgeName(for: type) let cacheName = JNICaching.cacheName(for: type) let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization @@ -880,7 +881,7 @@ extension JNISwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) } - private func printTypeMetadataAddressThunk(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printTypeMetadataAddressThunk(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { // Specialized types are treated as concrete let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization if isEffectivelyGeneric { @@ -904,7 +905,7 @@ extension JNISwift2JavaGenerator { } /// Prints thunks for specific known types like Foundation.Date, Foundation.Data - private func printSpecificTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printSpecificTypeThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { guard let knownType = type.swiftNominal.knownTypeKind else { return } switch knownType { @@ -918,7 +919,7 @@ extension JNISwift2JavaGenerator { } /// Prints Swift thunks for Foundation.Data helper methods - private func printFoundationDataThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printFoundationDataThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) let parentName = type.qualifiedName @@ -964,7 +965,7 @@ extension JNISwift2JavaGenerator { } } - private func printFunctionOpenerCall(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + private func printFunctionOpenerCall(_ printer: inout CodePrinter, _ decl: ExtractedFunc) { guard let translatedDecl = self.translatedDecl(for: decl) else { fatalError("Cannot print function opener for a function that can't be translated: \(decl)") } @@ -1004,10 +1005,10 @@ extension JNISwift2JavaGenerator { "_\(swiftModuleName)_\(type.flatName)_opener" } - private func printOpenerProtocol(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printOpenerProtocol(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let protocolName = openerProtocolName(for: type.swiftNominal) - func printFunctionDecl(_ printer: inout CodePrinter, decl: ImportedFunc, skipMethodBody: Bool) { + func printFunctionDecl(_ printer: inout CodePrinter, decl: ExtractedFunc, skipMethodBody: Bool) { guard let translatedDecl = self.translatedDecl(for: decl) else { return } let nativeSignature = translatedDecl.nativeFunctionSignature @@ -1127,7 +1128,7 @@ extension SwiftNominalTypeDeclaration { } } -extension ImportedFunc { +extension ExtractedFunc { fileprivate var openerMethodName: String { let prefix = switch apiKind { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 208a3228f..b0379e2f4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore @@ -49,9 +50,9 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { var generatedCDeclSymbolNames: [String] = [] /// Cached Java translation result. 'nil' indicates failed translation. - var translatedDecls: [ImportedFunc: TranslatedFunctionDecl] = [:] - var translatedEnumCases: [ImportedEnumCase: TranslatedEnumCase] = [:] - var interfaceProtocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] = [:] + var translatedDecls: [ExtractedFunc: TranslatedFunctionDecl] = [:] + var translatedEnumCases: [ExtractedEnumCase: TranslatedEnumCase] = [:] + var interfaceProtocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] = [:] /// Duplicate identifier tracking for the current batch of methods being generated. var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() @@ -65,7 +66,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { package init( config: Configuration, - translator: Swift2JavaTranslator, + translator: SwiftAnalyzer, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, @@ -85,7 +86,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { // If we are forced to write empty files, construct the expected outputs. // It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway. - if translator.config.effectiveWriteEmptyFiles { + if config.effectiveWriteEmptyFiles { self.expectedOutputSwiftFileNames = Set( translator.inputs.compactMap { (input) -> String? in guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { @@ -116,11 +117,11 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { self.expectedOutputSwiftFileNames = [] } - if translator.config.enableJavaCallbacks ?? false { + if config.enableJavaCallbacks ?? false { // We translate all the protocol wrappers // as we need them to know what protocols we can allow the user to implement themselves // in Java. - self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.importedTypes.values)) + self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.extractedTypes.values)) } } @@ -142,12 +143,12 @@ extension JNISwift2JavaGenerator { "\(parameterName)$indirect" } - func inheritedProtocols(of type: ImportedNominalType) -> [ImportedNominalType] { + func inheritedProtocols(of type: ExtractedNominalType) -> [ExtractedNominalType] { type.inheritedTypes .compactMap(\.asNominalTypeDeclaration) .filter { $0.kind == .protocol } .compactMap { - self.analysis.importedTypes[$0.qualifiedName] + self.analysis.extractedTypes[$0.qualifiedName] } } } diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift new file mode 100644 index 000000000..a7687a381 --- /dev/null +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftExtract +import SwiftSyntax + +/// Java-specific extraction overrides applied on top of SwiftExtract's +/// built-in access-level filter: +/// +/// - `@JavaExport` forces extraction even of non-public decls +/// - `@JavaClass` / `@JavaInterface` / `@JavaField` / `@JavaStaticField` / +/// `@JavaMethod` / `@JavaStaticMethod` / `@JavaImplementation` are Swift +/// wrappers of Java types — skip them during extraction +public struct JavaExtractDecider: ExtractDecider { + public init() {} + + public func shouldExtract(decl: DeclSyntax, accessLevelPasses: Bool) -> Bool? { + let attrs = decl.asProtocol(WithAttributesSyntax.self)?.attributes + if attrs?.contains(where: { $0.isJavaExport }) == true { + return true + } + if attrs?.contains(where: { $0.isSwiftJavaMacro }) == true { + return false + } + return nil + } +} diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift index 2706d8ff9..823246305 100644 --- a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + /// Detects Java method name conflicts caused by Swift overloads that differ /// only in parameter labels. When a conflict is detected, the affected methods /// get a camelCase suffix derived from their parameter labels (e.g. `takeValueA`, @@ -21,20 +23,20 @@ package struct JavaIdentifierFactory { package init() {} - package init(_ methods: [ImportedFunc]) { + package init(_ methods: [ExtractedFunc]) { self.init() record(methods) } /// Analyze the given methods and record any base names that have conflicts. - private mutating func record(_ methods: [ImportedFunc]) { + private mutating func record(_ methods: [ExtractedFunc]) { // Group methods by their Java base name. - var methodsByBaseName: [String: [ImportedFunc]] = [:] + var methodsByBaseName: [String: [ExtractedFunc]] = [:] for method in methods { let baseName: String = switch method.apiKind { - case .getter, .subscriptGetter: method.javaGetterName - case .setter, .subscriptSetter: method.javaSetterName + case .getter, .subscriptGetter: method.javaGetterName! + case .setter, .subscriptSetter: method.javaSetterName! case .function, .initializer, .enumCase: method.name } methodsByBaseName[baseName, default: []].append(method) @@ -61,11 +63,11 @@ package struct JavaIdentifierFactory { } /// Compute the disambiguated Java method name for a declaration. - package func makeJavaMethodName(_ decl: ImportedFunc) -> String { + package func makeJavaMethodName(_ decl: ExtractedFunc) -> String { let baseName: String = switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName + case .getter, .subscriptGetter: decl.javaGetterName! + case .setter, .subscriptSetter: decl.javaSetterName! case .function, .initializer, .enumCase: decl.name } var methodName = baseName + paramsSuffix(decl, baseName: baseName) @@ -75,7 +77,7 @@ package struct JavaIdentifierFactory { return methodName } - private func paramsSuffix(_ decl: ImportedFunc, baseName: String) -> String { + private func paramsSuffix(_ decl: ExtractedFunc, baseName: String) -> String { switch decl.apiKind { case .getter, .subscriptGetter, .setter, .subscriptSetter: return "" diff --git a/Sources/JExtractSwiftLib/SourceDependencies.swift b/Sources/JExtractSwiftLib/JavaSourceDependencies.swift similarity index 55% rename from Sources/JExtractSwiftLib/SourceDependencies.swift rename to Sources/JExtractSwiftLib/JavaSourceDependencies.swift index a1128e51b..a9c64567f 100644 --- a/Sources/JExtractSwiftLib/SourceDependencies.swift +++ b/Sources/JExtractSwiftLib/JavaSourceDependencies.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import OrderedCollections +import SwiftExtract import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -22,50 +24,21 @@ import FoundationEssentials import Foundation #endif - -package typealias SwiftModuleName = String -package typealias SwiftTypeName = String -package typealias SwiftSourceText = String -package typealias JavaClassName = String -package typealias JavaFullyQualifiedClassName = String -package typealias JavaPackageName = String - -/// Holds the inputs jextract needs for symbol resolution but does not generate -/// bindings for. -/// -/// Two flavours of "dependency" are tracked: -/// - Wrapped Java classes referenced from this module's API (e.g. `JavaInteger`). -/// - Real Swift sources from dependency Swift modules (passed via `--depends-on`), -/// parsed once and registered as imported `SwiftModuleSymbolTable`s so that -/// cross-module type references in this module's API can resolve them. -package struct SourceDependencies { - /// Swift wrapper type names for Java classes referenced from this module's - /// API (by convention `Java`, e.g. `JavaVector`). - package var javaClasses: [SwiftTypeName] = [] - - /// Parsed Swift inputs from dependency modules, keyed by Swift module name. - package var swiftModuleInputs: [SwiftModuleName: [SwiftJavaInputFile]] = [:] - - package init() {} - - /// Names of all dependency modules with associated Swift sources. - package var swiftModuleNames: Dictionary.Keys { - swiftModuleInputs.keys - } - - /// Synthetic Swift source registering `@JavaClass public class {}` stubs - package var syntheticJavaWrappersSwiftSource: SwiftJavaInputFile? { - guard !javaClasses.isEmpty else { return nil } +extension SourceDependencies { + /// Inject synthetic `@JavaClass public class {}` stubs so the symbol + /// table can resolve Java wrapper types referenced in the Swift API. + package mutating func addJavaWrapperStubs(_ javaClasses: [SwiftTypeName]) { + guard !javaClasses.isEmpty else { return } let text = javaClasses .map { "@JavaClass public class \($0) {}" } .joined(separator: "\n") - return SwiftJavaInputFile( - syntax: Parser.parse(source: text), - path: ".swift" - ) + let stub = SwiftInputFile(syntax: SwiftParser.Parser.parse(source: text), path: ".swift") + syntheticStubInputs[""] = [stub] } + /// Load Swift sources from a dependency module described by `dependency` and + /// register them in `swiftModuleInputs` for cross-module type resolution. package mutating func loadSwiftSources(from dependency: DependencyConfig, log: Logger) { guard let moduleName = dependency.swiftModuleName else { log.debug( @@ -85,15 +58,15 @@ package struct SourceDependencies { in: dependency.swiftSourcePaths, log: log, ) - var inputs: [SwiftJavaInputFile] = [] + var inputs: [SwiftInputFile] = [] let fm = FileManager.default for url in files where canExtract(from: url) { guard let data = fm.contents(atPath: url.path), let text = String(data: data, encoding: .utf8) else { continue } - let syntax = Parser.parse(source: text) - inputs.append(SwiftJavaInputFile(syntax: syntax, path: url.path)) + let syntax = SwiftParser.Parser.parse(source: text) + inputs.append(SwiftInputFile(syntax: syntax, path: url.path)) } if inputs.isEmpty { diff --git a/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift b/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift new file mode 100644 index 000000000..d6e6a828f --- /dev/null +++ b/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 ArgumentParser +import SwiftExtract +import SwiftJavaConfigurationShared + +extension Logger.Level: ExpressibleByArgument { + public var defaultValueDescription: String { + "log level" + } + public private(set) static var allValueStrings: [String] = + ["trace", "debug", "info", "notice", "warning", "error", "critical"] + + public private(set) static var defaultCompletionKind: CompletionKind = .default +} diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 4350ffcde..2bf5ef037 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -14,6 +14,7 @@ import Foundation import OrderedCollections +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaShared import SwiftParser @@ -34,7 +35,7 @@ public struct SwiftToJava { fatalError("Missing '--swift-module' name.") } - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) let log = translator.log if config.javaPackage == nil || config.javaPackage!.isEmpty { @@ -73,7 +74,7 @@ public struct SwiftToJava { // Apply jextract include/exclude filters if configured if hasFilters { let relativePath = computeRelativePath(file: file, inputPaths: inputPaths) - guard shouldJExtractFile(relativePath: relativePath, config: config) else { + guard shouldExtractSwiftFile(relativePath: relativePath, config: config) else { log.info("Skipping file (filtered out): \(file.path)") translator.filteredOutPaths.append(file.path) continue @@ -113,7 +114,7 @@ public struct SwiftToJava { partialResult[moduleName] = javaPackage } - translator.sourceDependencies.javaClasses = Array(wrappedJavaClassesLookupTable.keys) + translator.sourceDependencies.addJavaWrapperStubs(Array(wrappedJavaClassesLookupTable.keys)) for config in dependencyConfigs { translator.sourceDependencies.loadSwiftSources(from: config, log: translator.log) } diff --git a/Sources/JExtractSwiftLib/SwiftKit+Printing.swift b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift index f5ac6a4a4..881d33899 100644 --- a/Sources/JExtractSwiftLib/SwiftKit+Printing.swift +++ b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift @@ -12,8 +12,10 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import Foundation import SwiftBasicFormat +import SwiftExtract import SwiftParser import SwiftSyntax @@ -21,7 +23,7 @@ import SwiftSyntax package struct SwiftKitPrinting { /// Forms syntax for a Java call to a swiftkit exposed function. - static func renderCallGetSwiftType(module: String, nominal: ImportedNominalType) -> String { + static func renderCallGetSwiftType(module: String, nominal: ExtractedNominalType) -> String { """ SwiftRuntime.swiftjava.getType("\(module)", "\(nominal.swiftNominal.qualifiedName)") """ @@ -51,7 +53,7 @@ extension SwiftKitPrinting { } extension SwiftKitPrinting.Names { - static func getType(module: String, nominal: ImportedNominalType) -> String { + static func getType(module: String, nominal: ExtractedNominalType) -> String { "swiftjava_getType_\(module)_\(nominal.swiftNominal.qualifiedTypeName.fullFlatName)" } diff --git a/Sources/JExtractSwiftLib/SwiftKnownTypes+Java.swift b/Sources/JExtractSwiftLib/SwiftKnownTypes+Java.swift new file mode 100644 index 000000000..8f1ba13bf --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftKnownTypes+Java.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 SwiftExtract + +extension SwiftNominalTypeDeclaration { + /// True if this is swift-java's runtime `SwiftJavaError` type, which jextract + /// maps to a thrown Java exception rather than an ordinary wrapped nominal + package var isSwiftJavaErrorType: Bool { + moduleName == "SwiftRuntimeFunctions" && name == "SwiftJavaError" + } +} + +extension SwiftKnownTypeDeclKind { + /// Indicates whether this known type is translated by `wrap-java` + /// into the same type as `jextract`. + /// + /// This means we do not have to perform any mapping when passing + /// this type between jextract and wrap-java + package var isDirectlyTranslatedToWrapJava: Bool { + switch self { + case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string, + .void: + return true + default: + return false + } + } +} diff --git a/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift new file mode 100644 index 000000000..74acf3fff --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 +// +//===----------------------------------------------------------------------===// + +@_spi(Testing) import SwiftExtract +import SwiftSyntax + +extension AttributeListSyntax.Element { + /// Whether this node has `SwiftJava` wrapping attributes (types that wrap Java classes). + /// These are skipped during jextract because they represent Java->Swift wrappers. + /// Note: `@JavaExport` is NOT included here — it forces export of Swift types to Java. + package var isSwiftJavaMacro: Bool { + guard case let .attribute(attr) = self else { + // FIXME: Handle #if. + return false + } + guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false } + switch attrName { + case "JavaClass", "JavaInterface", "JavaField", "JavaStaticField", "JavaMethod", "JavaStaticMethod", + "JavaImplementation": + return true + default: + return false + } + } + + /// Whether this is a `@JavaExport` attribute (used on typealiases for specialization, + /// or on struct/class/enum to force-include them even when excluded by filters) + package var isJavaExport: Bool { + guard case let .attribute(attr) = self else { return false } + guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false } + return attrName == "JavaExport" + } +} + +extension SwiftNominalType { + /// True iff the underlying Swift declaration uses one of the Java-wrapper + /// macros (`@JavaClass`, `@JavaInterface`, …) — meaning the type represents + /// a Java class wrapped for Swift, not a Swift type to be re-exported + public var isSwiftJavaWrapper: Bool { + nominalTypeDecl.syntax.attributes.contains(where: \.isSwiftJavaMacro) + } +} diff --git a/Sources/JExtractSwiftLib/SymbolTable+Printing.swift b/Sources/JExtractSwiftLib/SymbolTable+Printing.swift new file mode 100644 index 000000000..0f28ebffb --- /dev/null +++ b/Sources/JExtractSwiftLib/SymbolTable+Printing.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 CodePrinting +import SwiftExtract + +extension SwiftSymbolTable { + package func printImportedModules(_ printer: inout CodePrinter) { + let mainSymbolSourceModules = Set( + self.importedModules.values.filter { $0.alternativeModules?.isMainSourceOfSymbols ?? false }.map(\.moduleName) + ) + + for module in self.importedModules.keys.sorted() { + guard module != "Swift" else { + continue + } + + // Synthetic stub modules (e.g. ) exist purely for + // symbol-table resolution; they are not real Swift modules and must + // not be emitted as `import` statements. + guard !self.syntheticImportedModuleNames.contains(module) else { + continue + } + + guard let alternativeModules = self.importedModules[module]?.alternativeModules else { + printer.print("import \(module)") + continue + } + + // Only the main source of symbols emits the conditional import block. + // Secondary modules (e.g. FoundationEssentials when Foundation is the main source) + // are skipped when their main source is already present, because the main source's + // block already covers the import. If no main source is present, fall back to a + // plain import so the module is still imported. + guard alternativeModules.isMainSourceOfSymbols else { + if mainSymbolSourceModules.isDisjoint(with: alternativeModules.moduleNames) { + printer.print("import \(module)") + } + continue + } + + var importGroups: [String: [String]] = [:] + for name in alternativeModules.moduleNames { + guard let otherModule = self.importedModules[name] else { continue } + + let groupKey = otherModule.requiredAvailablityOfModuleWithName ?? otherModule.moduleName + importGroups[groupKey, default: []].append(otherModule.moduleName) + } + + for (index, group) in importGroups.keys.sorted().enumerated() { + if index > 0 && importGroups.keys.count > 1 { + printer.print("#elseif canImport(\(group))") + } else { + printer.print("#if canImport(\(group))") + } + + for groupModule in importGroups[group] ?? [] { + printer.print("import \(groupModule)") + } + } + + if importGroups.keys.isEmpty { + printer.print("import \(module)") + } else { + printer.print("#else") + printer.print("import \(module)") + printer.print("#endif") + } + } + printer.println() + } +} diff --git a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift index ac783f5c8..5beb710e3 100644 --- a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift +++ b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift @@ -12,18 +12,20 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + /// Registry of names we've already emitted as @_cdecl and must be kept unique. /// In order to avoid duplicate symbols, the registry can append some unique identifier to duplicated names package struct ThunkNameRegistry { /// Maps base names such as "swiftjava_Module_Type_method_a_b_c" to the number of times we've seen them. /// This is used to de-duplicate symbols as we emit them. - private var registry: [ImportedFunc: String] = [:] + private var registry: [ExtractedFunc: String] = [:] private var duplicateNames: [String: Int] = [:] package init() {} package mutating func functionThunkName( - decl: ImportedFunc, + decl: ExtractedFunc, file: String = #fileID, line: UInt = #line ) -> String { diff --git a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift index bab21fdd9..a6b01e08b 100644 --- a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift +++ b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift @@ -13,12 +13,13 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftSyntax enum TranslatedDocumentation { static func printDocumentation( - importedFunc: ImportedFunc, + importedFunc: ExtractedFunc, translatedDecl: FFMSwift2JavaGenerator.TranslatedFunctionDecl, config: Configuration, in printer: inout CodePrinter @@ -38,7 +39,7 @@ enum TranslatedDocumentation { } static func printDocumentation( - importedFunc: ImportedFunc, + importedFunc: ExtractedFunc, translatedDecl: JNISwift2JavaGenerator.TranslatedFunctionDecl, config: Configuration, in printer: inout CodePrinter diff --git a/Sources/SwiftExtract/AnalysisResult.swift b/Sources/SwiftExtract/AnalysisResult.swift new file mode 100644 index 000000000..6c6f0858e --- /dev/null +++ b/Sources/SwiftExtract/AnalysisResult.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +public struct AnalysisResult { + public let extractedTypes: [String: ExtractedNominalType] + public let extractedGlobalVariables: [ExtractedFunc] + public let extractedGlobalFuncs: [ExtractedFunc] + + public init( + extractedTypes: [String: ExtractedNominalType], + extractedGlobalVariables: [ExtractedFunc], + extractedGlobalFuncs: [ExtractedFunc] + ) { + self.extractedTypes = extractedTypes + self.extractedGlobalVariables = extractedGlobalVariables + self.extractedGlobalFuncs = extractedGlobalFuncs + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/Collection+Extensions.swift b/Sources/SwiftExtract/Convenience/Collection+Extensions.swift similarity index 91% rename from Sources/JExtractSwiftLib/Convenience/Collection+Extensions.swift rename to Sources/SwiftExtract/Convenience/Collection+Extensions.swift index 4286d9e16..e04341368 100644 --- a/Sources/JExtractSwiftLib/Convenience/Collection+Extensions.swift +++ b/Sources/SwiftExtract/Convenience/Collection+Extensions.swift @@ -27,8 +27,8 @@ extension Dictionary { } extension Collection { - typealias IsLastElement = Bool - var withIsLast: any Collection<(Element, IsLastElement)> { + package typealias IsLastElement = Bool + package var withIsLast: any Collection<(Element, IsLastElement)> { var i = 1 let totalCount = self.count diff --git a/Sources/SwiftExtract/Convenience/String+Extensions.swift b/Sources/SwiftExtract/Convenience/String+Extensions.swift new file mode 100644 index 000000000..443984b4e --- /dev/null +++ b/Sources/SwiftExtract/Convenience/String+Extensions.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +extension String { + + package var firstCharacterUppercased: String { + guard let f = first else { + return self + } + + return "\(f.uppercased())\(String(dropFirst()))" + } + + package var firstCharacterLowercased: String { + guard let f = first else { + return self + } + + return "\(f.lowercased())\(String(dropFirst()))" + } + + /// If the string ends with `.swift`, return it without that suffix; + /// otherwise return self unchanged + package func dropSwiftFileSuffix() -> String { + if hasSuffix(".swift") { + return String(dropLast(".swift".count)) + } + return self + } + + /// Unescapes the name if it is surrounded by backticks. + package var unescapedSwiftName: String { + if count >= 2 && hasPrefix("`") && hasSuffix("`") { + return String(dropFirst().dropLast()) + } + return self + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift similarity index 84% rename from Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift rename to Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift index 76e5ba688..3b0194db2 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift @@ -95,7 +95,7 @@ extension DeclModifierSyntax { } extension WithModifiersSyntax { - func isPublic(in type: NominalTypeDeclSyntaxNode?) -> Bool { + package func isPublic(in type: NominalTypeDeclSyntaxNode?) -> Bool { if let protocolDecl = type?.as(ProtocolDeclSyntax.self) { return protocolDecl.isPublic(in: nil) } @@ -105,13 +105,13 @@ extension WithModifiersSyntax { } } - var isAtLeastPackage: Bool { + package var isAtLeastPackage: Bool { self.modifiers.contains { modifier in modifier.isAtLeastPackage } } - var isAtLeastInternal: Bool { + package var isAtLeastInternal: Bool { if self.modifiers.isEmpty { // we assume that default access level is internal return true @@ -123,37 +123,9 @@ extension WithModifiersSyntax { } } -extension AttributeListSyntax.Element { - /// Whether this node has `SwiftJava` wrapping attributes (types that wrap Java classes). - /// These are skipped during jextract because they represent Java->Swift wrappers. - /// Note: `@JavaExport` is NOT included here — it forces export of Swift types to Java. - var isSwiftJavaMacro: Bool { - guard case let .attribute(attr) = self else { - // FIXME: Handle #if. - return false - } - guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false } - switch attrName { - case "JavaClass", "JavaInterface", "JavaField", "JavaStaticField", "JavaMethod", "JavaStaticMethod", - "JavaImplementation": - return true - default: - return false - } - } - - /// Whether this is a `@JavaExport` attribute (used on typealiases for specialization, - /// or on struct/class/enum to force-include them even when excluded by filters) - var isJavaExport: Bool { - guard case let .attribute(attr) = self else { return false } - guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false } - return attrName == "JavaExport" - } -} - extension DeclSyntaxProtocol { /// Find inner most "decl" node in ancestors. - var ancestorDecl: DeclSyntax? { + package var ancestorDecl: DeclSyntax? { var node: Syntax = Syntax(self) while let parent = node.parent { if let decl = parent.as(DeclSyntax.self) { @@ -165,7 +137,7 @@ extension DeclSyntaxProtocol { } /// Declaration name primarily for debugging. - var nameForDebug: String { + package var nameForDebug: String { switch DeclSyntax(self).as(DeclSyntaxEnum.self) { case .accessorDecl(let node): node.accessorSpecifier.text @@ -233,7 +205,7 @@ extension DeclSyntaxProtocol { } /// Qualified declaration name primarily for debugging. - var qualifiedNameForDebug: String { + package var qualifiedNameForDebug: String { if let parent = ancestorDecl { parent.qualifiedNameForDebug + "." + nameForDebug } else { @@ -242,7 +214,7 @@ extension DeclSyntaxProtocol { } /// Signature part of the declaration. I.e. without body or member block. - var signatureString: String { + package var signatureString: String { switch DeclSyntax(self.detached).as(DeclSyntaxEnum.self) { case .functionDecl(let node): node.with(\.body, nil).triviaSanitizedDescription diff --git a/Sources/SwiftExtract/ExtractDecider.swift b/Sources/SwiftExtract/ExtractDecider.swift new file mode 100644 index 000000000..9e576660c --- /dev/null +++ b/Sources/SwiftExtract/ExtractDecider.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftSyntax + +/// A pluggable extraction decision for downstream language generators +/// +/// The built-in analyzer always applies its access-level filter; a supplied +/// `ExtractDecider` can override that decision on a per-decl basis to encode +/// language-specific rules. For example, the Java target uses one to honor +/// `@JavaExport` (force-include even when access-level would skip) and to +/// skip Swift wrappers of Java types (`@JavaClass`, `@JavaInterface`, …) +public protocol ExtractDecider { + /// - Parameters: + /// - decl: the declaration being considered + /// - accessLevelPasses: whether the analyzer's built-in access-level + /// check admits the decl + /// - Returns: `true` to force-extract (even when `accessLevelPasses` + /// is `false`), `false` to skip (even when `accessLevelPasses` is + /// `true`), or `nil` to defer to the default behavior + func shouldExtract(decl: DeclSyntax, accessLevelPasses: Bool) -> Bool? +} diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift similarity index 65% rename from Sources/JExtractSwiftLib/ImportedDecls.swift rename to Sources/SwiftExtract/ExtractedDecls.swift index 1f6b80d28..af5011124 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -14,10 +14,10 @@ import SwiftSyntax -/// Any imported (Swift) declaration -protocol ImportedDecl: AnyObject {} +/// Any extracted Swift declaration +public protocol ExtractedSwiftDecl: AnyObject {} -package enum SwiftAPIKind: Equatable { +public enum SwiftAPIKind: Equatable { case function case initializer case getter @@ -28,57 +28,56 @@ package enum SwiftAPIKind: Equatable { } /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been -/// imported and is being translated into Java. +/// extracted by the analyzer for a downstream language target. /// /// When `base` is non-nil, this is a specialization of a generic type /// (e.g. `FishBox` specializing `Box` with `Element` = `Fish`). /// The specialization delegates its member collections to the base type /// so that extensions discovered later are visible through all specializations. -package final class ImportedNominalType: ImportedDecl { - let swiftNominal: SwiftNominalTypeDeclaration +public final class ExtractedNominalType: ExtractedSwiftDecl { + public let swiftNominal: SwiftNominalTypeDeclaration - /// If this type is a specialization (FishTank), then this points at the Tank base type of the specialization. - /// His allows simplified - package let specializationBaseType: ImportedNominalType? + /// If this type is a specialization (FishTank), it points at the Tank base type of the specialization + public let specializationBaseType: ExtractedNominalType? // The short path from module root to the file in which this nominal was originally declared. // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. - package var sourceFilePath: String { + public var sourceFilePath: String { self.swiftNominal.sourceFilePath } // Backing storage for member collections - package var initializers: [ImportedFunc] = [] - package var methods: [ImportedFunc] = [] - package var variables: [ImportedFunc] = [] - package var cases: [ImportedEnumCase] = [] - var inheritedTypes: [SwiftType] - package var parent: SwiftNominalTypeDeclaration? + public var initializers: [ExtractedFunc] = [] + public var methods: [ExtractedFunc] = [] + public var variables: [ExtractedFunc] = [] + public var cases: [ExtractedEnumCase] = [] + public var inheritedTypes: [SwiftType] + public var parent: SwiftNominalTypeDeclaration? /// The Swift base type name, e.g. "Box" — always the unparameterized name - package var baseTypeName: String { swiftNominal.qualifiedName } + public var baseTypeName: String { swiftNominal.qualifiedName } - /// The specialized/Java-facing name, e.g. "FishBox" — nil for base types - package private(set) var specializedTypeName: String? + /// The specialized output-facing name, e.g. "FishBox" — nil for base types + public private(set) var specializedTypeName: String? /// Whether this type is a specialization of a generic type - package var isSpecialization: Bool { specializationBaseType != nil } + public var isSpecialization: Bool { specializationBaseType != nil } /// Generic parameter names (e.g. ["Element"] for Box). Empty for non-generic types - package var genericParameterNames: [String] { + public var genericParameterNames: [String] { swiftNominal.genericParameters.map(\.name) } /// Maps generic parameter -> concrete type argument. Empty for unspecialized types /// e.g. {"Element": "Fish"} for FishBox - package var genericArguments: [String: String] = [:] + public var genericArguments: [String: String] = [:] /// True when all generic parameters have corresponding arguments - package var isFullySpecialized: Bool { + public var isFullySpecialized: Bool { !genericParameterNames.isEmpty && genericParameterNames.allSatisfy { genericArguments.keys.contains($0) } } - init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { + public init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { self.swiftNominal = swiftNominal self.specializationBaseType = nil self.inheritedTypes = @@ -90,7 +89,7 @@ package final class ImportedNominalType: ImportedDecl { } /// Init for creating a specialization - private init(base: ImportedNominalType, specializedTypeName: String, genericArguments: [String: String]) { + private init(base: ExtractedNominalType, specializedTypeName: String, genericArguments: [String: String]) { self.swiftNominal = base.swiftNominal self.specializationBaseType = base @@ -119,41 +118,55 @@ package final class ImportedNominalType: ImportedDecl { self.swiftType = selfType } - let swiftType: SwiftType + public let swiftType: SwiftType - /// Structured Java-facing type name — "FishBox" for specialized, "Box" for base - package var effectiveJavaTypeName: SwiftQualifiedTypeName { + /// Structured output-facing type name — "FishBox" for specialized, "Box" for base + public var effectiveOutputTypeName: SwiftQualifiedTypeName { if let specializedTypeName { return SwiftQualifiedTypeName(specializedTypeName) } return swiftNominal.qualifiedTypeName } - /// The effective Java-facing name — "FishBox" for specialized, "Box" for base - var effectiveJavaName: String { - effectiveJavaTypeName.fullName + /// The effective output-facing name — "FishBox" for specialized, "Box" for base + public var effectiveOutputName: String { + effectiveOutputTypeName.fullName } - /// The simple Java class name (no qualification) for file naming purposes - var effectiveJavaSimpleName: String { + /// The simple output-facing class name (no qualification) for file naming purposes + public var effectiveOutputSimpleName: String { specializedTypeName ?? swiftNominal.name } /// The Swift type for thunk generation — "Box" for specialized, "Box" for base /// Computed from baseTypeName + genericArguments - var effectiveSwiftTypeName: String { + public var effectiveSwiftTypeName: String { guard !genericArguments.isEmpty else { return baseTypeName } let orderedArgs = genericParameterNames.compactMap { genericArguments[$0] } guard !orderedArgs.isEmpty else { return baseTypeName } return "\(baseTypeName)<\(orderedArgs.joined(separator: ", "))>" } - var qualifiedName: String { + public var qualifiedName: String { self.swiftNominal.qualifiedName } - /// The Java generic clause, e.g. "" for generic base types, "" for specialized or non-generic - var javaGenericClause: String { + /// The attribute list on this type's declaration (e.g. `@resultBuilder`), + /// for language targets that key behavior off attributes. + /// Mirrors `ExtractedFunc.swiftDecl` being public for the function case. + public var declAttributes: AttributeListSyntax { + swiftNominal.syntax.attributes + } + + /// The declaration-group syntax for this type (protocol/struct/class/enum/ + /// actor), for language targets that need to inspect members or clauses the + /// neutral model doesn't surface (e.g. a protocol's primary associated types). + public var declGroupSyntax: any DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax { + swiftNominal.syntax + } + + /// The output generic clause, e.g. "" for generic base types, "" for specialized or non-generic + public var outputGenericClause: String { if isSpecialization { "" } else if genericParameterNames.isEmpty { @@ -164,10 +177,10 @@ package final class ImportedNominalType: ImportedDecl { } /// Create a specialized version of this generic type - package func specialize( + public func specialize( as specializedName: String, with substitutions: [String: String], - ) throws -> ImportedNominalType { + ) throws -> ExtractedNominalType { guard !genericParameterNames.isEmpty else { throw SpecializationError( message: "Unable to specialize non-generic type '\(baseTypeName)' as '\(specializedName)'" @@ -179,7 +192,7 @@ package final class ImportedNominalType: ImportedDecl { message: "Missing type arguments for: \(missingParams) when specializing \(baseTypeName) as \(specializedName)" ) } - return ImportedNominalType( + return ExtractedNominalType( base: self, specializedTypeName: specializedName, genericArguments: substitutions, @@ -187,14 +200,14 @@ package final class ImportedNominalType: ImportedDecl { } /// Checks if this type, or any of types it inherits from, conforms to the passed in protocol. - package func conformsTo(_ protocolName: String, in importedTypes: [String: ImportedNominalType]) -> Bool { + public func conformsTo(_ protocolName: String, in extractedTypes: [String: ExtractedNominalType]) -> Bool { var visited: Set = [] - var queue: [ImportedNominalType] = [self] + var queue: [ExtractedNominalType] = [self] while let current = queue.popLast() { for inherited in current.inheritedTypes { guard let name = inherited.asNominalTypeDeclaration?.name else { continue } if name == protocolName { return true } - if let next = importedTypes[name], visited.insert(ObjectIdentifier(next)).inserted { + if let next = extractedTypes[name], visited.insert(ObjectIdentifier(next)).inserted { queue.append(next) } } @@ -203,30 +216,30 @@ package final class ImportedNominalType: ImportedDecl { } } -struct SpecializationError: Error { - let message: String +public struct SpecializationError: Error { + public let message: String } -public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { +public final class ExtractedEnumCase: ExtractedSwiftDecl, CustomStringConvertible { /// The case name public let name: String /// The enum parameters - let parameters: [SwiftEnumCaseParameter] + public let parameters: [SwiftEnumCaseParameter] - let swiftDecl: any DeclSyntaxProtocol + public let swiftDecl: any DeclSyntaxProtocol - let enumType: SwiftNominalType + public let enumType: SwiftNominalType /// A function that represents the Swift static "initializer" for cases - let caseFunction: ImportedFunc + public let caseFunction: ExtractedFunc - init( + public init( name: String, parameters: [SwiftEnumCaseParameter], swiftDecl: any DeclSyntaxProtocol, enumType: SwiftNominalType, - caseFunction: ImportedFunc, + caseFunction: ExtractedFunc, ) { self.name = name self.parameters = parameters @@ -237,7 +250,7 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { public var description: String { """ - ImportedEnumCase { + ExtractedEnumCase { name: \(name), parameters: \(parameters), swiftDecl: \(swiftDecl), @@ -247,8 +260,8 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { """ } - func clone(for parent: SwiftType) -> ImportedEnumCase { - ImportedEnumCase( + public func clone(for parent: SwiftType) -> ExtractedEnumCase { + ExtractedEnumCase( name: name, parameters: parameters, swiftDecl: swiftDecl, @@ -258,16 +271,16 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { } } -extension ImportedEnumCase: Hashable { +extension ExtractedEnumCase: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } - public static func == (lhs: ImportedEnumCase, rhs: ImportedEnumCase) -> Bool { + public static func == (lhs: ExtractedEnumCase, rhs: ExtractedEnumCase) -> Bool { lhs === rhs } } -public final class ImportedFunc: ImportedDecl, CustomStringConvertible { +public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible { /// Swift module name (e.g. the target name where a type or function was declared) public let module: String @@ -277,26 +290,26 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { public let swiftDecl: any DeclSyntaxProtocol - package let apiKind: SwiftAPIKind + public let apiKind: SwiftAPIKind - let functionSignature: SwiftFunctionSignature + public let functionSignature: SwiftFunctionSignature public var signatureString: String { self.swiftDecl.signatureString } - var parentType: SwiftType? { + public var parentType: SwiftType? { functionSignature.selfParameter?.selfType } - var isStatic: Bool { + public var isStatic: Bool { if case .staticMethod = functionSignature.selfParameter { return true } return false } - var isInitializer: Bool { + public var isInitializer: Bool { if case .initializer = functionSignature.selfParameter { return true } @@ -305,8 +318,6 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { /// If this function/method is member of a class/struct/protocol, /// this will contain that declaration's imported name. - /// - /// This is necessary when rendering accessor Java code we need the type that "self" is expecting to have. public var hasParent: Bool { functionSignature.selfParameter != nil } /// A display name to use to refer to the Swift declaration with its @@ -332,15 +343,15 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { return prefix + context + self.name } - var isThrowing: Bool { + public var isThrowing: Bool { self.functionSignature.effectSpecifiers.contains(.throws) } - var isAsync: Bool { + public var isAsync: Bool { self.functionSignature.isAsync } - init( + public init( module: String, swiftDecl: any DeclSyntaxProtocol, name: String, @@ -356,7 +367,7 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { public var description: String { """ - ImportedFunc { + ExtractedFunc { apiKind: \(apiKind) module: \(module) name: \(name) @@ -365,11 +376,11 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { """ } - func clone(for parent: SwiftType) -> ImportedFunc { + public func clone(for parent: SwiftType) -> ExtractedFunc { var functionSignature = functionSignature assert(functionSignature.selfParameter?.selfType != nil) functionSignature.selfParameter?.selfType = parent - return ImportedFunc( + return ExtractedFunc( module: module, swiftDecl: swiftDecl, name: name, @@ -379,48 +390,20 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { } } -extension ImportedFunc: Hashable { +extension ExtractedFunc: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } - public static func == (lhs: ImportedFunc, rhs: ImportedFunc) -> Bool { + public static func == (lhs: ExtractedFunc, rhs: ExtractedFunc) -> Bool { lhs === rhs } } -extension ImportedFunc { - var javaGetterName: String { - let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool - - if !returnsBoolean { - return "get\(self.name.firstCharacterUppercased)" - } else if !self.name.hasJavaBooleanNamingConvention { - return "is\(self.name.firstCharacterUppercased)" - } else { - return self.name - } - } - - var javaSetterName: String { - let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool - - // If the variable is already named "isX", then we make - // the setter "setX" to match beans spec. - if isBooleanSetter && self.name.hasJavaBooleanNamingConvention { - // Safe to force unwrap due to `hasJavaBooleanNamingConvention` check. - let propertyName = self.name.split(separator: "is", maxSplits: 1).last! - return "set\(propertyName)" - } else { - return "set\(self.name.firstCharacterUppercased)" - } - } -} - -extension ImportedNominalType: Hashable { +extension ExtractedNominalType: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } - public static func == (lhs: ImportedNominalType, rhs: ImportedNominalType) -> Bool { + public static func == (lhs: ExtractedNominalType, rhs: ExtractedNominalType) -> Bool { lhs === rhs } } diff --git a/Sources/JExtractSwiftLib/Logger.swift b/Sources/SwiftExtract/Logger.swift similarity index 86% rename from Sources/JExtractSwiftLib/Logger.swift rename to Sources/SwiftExtract/Logger.swift index 5c4267830..5a89412db 100644 --- a/Sources/JExtractSwiftLib/Logger.swift +++ b/Sources/SwiftExtract/Logger.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,9 +12,7 @@ // //===----------------------------------------------------------------------===// -import ArgumentParser import Foundation -import SwiftJavaConfigurationShared import SwiftSyntax // Placeholder for some better logger, we could depend on swift-log @@ -114,17 +112,19 @@ public struct Logger { } extension Logger { - public typealias Level = SwiftJavaConfigurationShared.LogLevel -} - -extension Logger.Level: ExpressibleByArgument { - public var defaultValueDescription: String { - "log level" + /// Log verbosity levels for the analysis layer's lightweight logger. + /// + /// Language-neutral; language-specific configuration modules map their own + /// log-level enums onto this via `SwiftExtractConfiguration`. + public enum Level: String, Codable, Hashable, Sendable { + case trace + case debug + case info + case notice + case warning + case error + case critical } - public private(set) static var allValueStrings: [String] = - ["trace", "debug", "info", "notice", "warning", "error", "critical"] - - public private(set) static var defaultCompletionKind: CompletionKind = .default } extension Logger.Level { diff --git a/Sources/JExtractSwiftLib/Resources/dummy.json b/Sources/SwiftExtract/Resources/dummy.json similarity index 100% rename from Sources/JExtractSwiftLib/Resources/dummy.json rename to Sources/SwiftExtract/Resources/dummy.json diff --git a/Sources/SwiftExtract/SourceDependencies.swift b/Sources/SwiftExtract/SourceDependencies.swift new file mode 100644 index 000000000..6d97a9637 --- /dev/null +++ b/Sources/SwiftExtract/SourceDependencies.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 +// +//===----------------------------------------------------------------------===// + +public typealias SwiftModuleName = String +public typealias SwiftTypeName = String +public typealias SwiftSourceText = String + +/// Holds inputs for symbol resolution that are not themselves the primary +/// extraction target: real Swift sources from dependency modules. +/// +/// Dependency inputs are parsed once and registered as imported +/// `SwiftModuleSymbolTable`s so that cross-module type references in the +/// analysed module's API can resolve them. +public struct SourceDependencies { + /// Parsed Swift inputs from dependency modules, keyed by Swift module name. + public var swiftModuleInputs: [SwiftModuleName: [SwiftInputFile]] = [:] + + /// Synthetic stub inputs keyed by a synthetic module name (e.g. for + /// generated `@JavaClass` placeholders). These are needed for symbol-table + /// resolution but must NOT be emitted as `import ` statements in + /// generated Swift code, because their names are not real Swift modules. + public var syntheticStubInputs: [SwiftModuleName: [SwiftInputFile]] = [:] + + public init() {} + + /// Names of all dependency modules (real + synthetic) with associated Swift + /// sources. Used by callers that need to resolve types belonging to either. + public var swiftModuleNames: Set { + Set(swiftModuleInputs.keys).union(syntheticStubInputs.keys) + } + + /// Names of synthetic stub modules. These should be skipped at Swift import + /// printing time. + public var syntheticModuleNames: Set { + Set(syntheticStubInputs.keys) + } +} diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift similarity index 81% rename from Sources/JExtractSwiftLib/Swift2JavaVisitor.swift rename to Sources/SwiftExtract/SwiftAnalysisVisitor.swift index ed6b4ed8b..d7c0a43f1 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,18 +13,18 @@ //===----------------------------------------------------------------------===// import Foundation +import Logging import SwiftIfConfig -import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -final class Swift2JavaVisitor { - let translator: Swift2JavaTranslator - var config: Configuration { +final class SwiftAnalysisVisitor { + let translator: SwiftAnalyzer + var config: any SwiftExtractConfiguration { self.translator.config } - init(translator: Swift2JavaTranslator) { + init(translator: SwiftAnalyzer) { self.translator = translator } @@ -38,7 +38,7 @@ final class Swift2JavaVisitor { } private var deferredConstrainedExtensions: [DeferredConstrainedExtension] = [] - func visit(inputFile: SwiftJavaInputFile) { + func visit(inputFile: SwiftInputFile) { let node = inputFile.syntax for codeItem in node.statements { if let declNode = codeItem.item.as(DeclSyntax.self) { @@ -47,7 +47,7 @@ final class Swift2JavaVisitor { } } - func visit(decl node: DeclSyntax, in parent: ImportedNominalType?, sourceFilePath: String) { + func visit(decl node: DeclSyntax, in parent: ExtractedNominalType?, sourceFilePath: String) { switch node.as(DeclSyntaxEnum.self) { case .actorDecl(let node): self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) @@ -86,24 +86,24 @@ final class Swift2JavaVisitor { func visit( nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String, ) { - guard let importedNominalType = translator.importedNominalType(node, parent: parent) else { + guard let extractedNominalType = translator.extractedNominalType(node, parent: parent) else { return } // Check if there's a specialization entry for this type - applySpecialization(to: importedNominalType) + applySpecialization(to: extractedNominalType) for memberItem in node.memberBlock.members { - self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) + self.visit(decl: memberItem.decl, in: extractedNominalType, sourceFilePath: sourceFilePath) } } func visit( enumDecl node: EnumDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String, ) { self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) @@ -113,14 +113,14 @@ final class Swift2JavaVisitor { func visit( extensionDecl node: ExtensionDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String, ) { guard parent == nil else { // 'extension' in a nominal type is invalid. Ignore return } - guard let importedNominalType = translator.importedNominalType(node.extendedType) else { + guard let extractedNominalType = translator.extractedNominalType(node.extendedType) else { return } @@ -133,12 +133,12 @@ final class Swift2JavaVisitor { guard !constraints.isEmpty else { // The extension is unconstrained: add to the base type (visible through all specializations) - importedNominalType.inheritedTypes += + extractedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap { try? SwiftType($0.type, lookupContext: translator.lookupContext) } ?? [] for memberItem in node.memberBlock.members { - self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) + self.visit(decl: memberItem.decl, in: extractedNominalType, sourceFilePath: sourceFilePath) } return } @@ -155,7 +155,7 @@ final class Swift2JavaVisitor { } let matchingSpecializations = findMatchingSpecializations( - extendedType: importedNominalType, + extendedType: extractedNominalType, whereConstraints: constraints, ) if matchingSpecializations.isEmpty { @@ -176,17 +176,23 @@ final class Swift2JavaVisitor { func visit( functionDecl node: FunctionDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return } switch node.name.tokenKind { case .binaryOperator, .prefixOperator, .postfixOperator: - self.log.debug("Skip importing: '\(node.qualifiedNameForDebug)'; Operators are not supported.") - return + // Operators are extracted as ordinary `.function`s only when the target + // opts in (other language code generators may map them to language + // constructs in a post-pass). Most targets (e.g. Java) cannot express + // Swift operators and skip them. + guard config.extractsOperators else { + self.log.debug("Skip importing: '\(node.qualifiedNameForDebug)'; Operators are not supported.") + return + } default: break } @@ -209,7 +215,7 @@ final class Swift2JavaVisitor { return } - let imported = ImportedFunc( + let imported = ExtractedFunc( module: translator.swiftModuleName, swiftDecl: node, name: node.name.text.unescapedSwiftName, @@ -221,13 +227,13 @@ final class Swift2JavaVisitor { if let typeContext { typeContext.methods.append(imported) } else { - translator.importedGlobalFuncs.append(imported) + translator.extractedGlobalFuncs.append(imported) } } func visit( enumCaseDecl node: EnumCaseDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, ) { guard let typeContext else { self.log.info("Enum case must be within a current type; \(node)") @@ -249,7 +255,7 @@ final class Swift2JavaVisitor { ) let caseName = caseElement.name.text.unescapedSwiftName - let caseFunction = ImportedFunc( + let caseFunction = ExtractedFunc( module: translator.swiftModuleName, swiftDecl: node, name: caseName, @@ -257,7 +263,7 @@ final class Swift2JavaVisitor { functionSignature: signature, ) - let importedCase = ImportedEnumCase( + let importedCase = ExtractedEnumCase( name: caseName, parameters: parameters ?? [], swiftDecl: node, @@ -278,10 +284,10 @@ final class Swift2JavaVisitor { func visit( variableDecl node: VariableDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return } @@ -322,17 +328,19 @@ final class Swift2JavaVisitor { func visit( initializerDecl node: InitializerDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, ) { guard let typeContext else { self.log.info("Initializer must be within a current type; \(node)") return } - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return } - if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization { + if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization + && !config.extractsGenericTypeInitializers + { log.debug("Skip Importing generic type initializer \(node.kind) '\(node.qualifiedNameForDebug)'") return } @@ -354,7 +362,7 @@ final class Swift2JavaVisitor { ) return } - let imported = ImportedFunc( + let imported = ExtractedFunc( module: translator.swiftModuleName, swiftDecl: node, name: "init", @@ -367,9 +375,9 @@ final class Swift2JavaVisitor { private func visit( subscriptDecl node: SubscriptDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return } @@ -408,7 +416,7 @@ final class Swift2JavaVisitor { private func visit( ifConfigDecl node: IfConfigDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String ) { let (clause, _) = node.activeClause(in: translator.buildConfig) @@ -432,7 +440,7 @@ final class Swift2JavaVisitor { private func importAccessor( from node: DeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, kind: SwiftAPIKind, name: String, ) throws { @@ -458,7 +466,7 @@ final class Swift2JavaVisitor { return } - let imported = ImportedFunc( + let imported = ExtractedFunc( module: translator.swiftModuleName, swiftDecl: node, name: name, @@ -472,15 +480,15 @@ final class Swift2JavaVisitor { if let typeContext { typeContext.variables.append(imported) } else { - translator.importedGlobalVariables.append(imported) + translator.extractedGlobalVariables.append(imported) } } private func synthesizeRawRepresentableConformance( enumDecl node: EnumDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, ) { - guard let imported = translator.importedNominalType(node, parent: parent) else { + guard let imported = translator.extractedNominalType(node, parent: parent) else { return } @@ -514,10 +522,10 @@ final class Swift2JavaVisitor { func visit( typeAliasDecl node: TypeAliasDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - let javaName = node.name.text + let outputName = node.name.text let rhsType = node.initializer.value let genericArgs: [String] @@ -533,13 +541,13 @@ final class Swift2JavaVisitor { guard !genericArgs.isEmpty else { return } // Resolve the base type through the symbol table - guard let baseType = translator.importedNominalType(rhsType) else { + guard let baseType = translator.extractedNominalType(rhsType) else { log.debug("Could not resolve base type for specialization: \(rhsType.trimmedDescription)") return } registerSpecialization( - javaName: javaName, + outputName: outputName, baseType: baseType, genericArgs: genericArgs, rhsDescription: rhsType.trimmedDescription, @@ -548,8 +556,8 @@ final class Swift2JavaVisitor { /// Register a specialization from a typealias that specializes a generic type private func registerSpecialization( - javaName: String, - baseType: ImportedNominalType, + outputName: String, + baseType: ExtractedNominalType, genericArgs: [String], rhsDescription: String, ) { @@ -564,29 +572,29 @@ final class Swift2JavaVisitor { } } - let specialized: ImportedNominalType + let specialized: ExtractedNominalType do { - specialized = try baseType.specialize(as: javaName, with: substitutions) + specialized = try baseType.specialize(as: outputName, with: substitutions) } catch { - log.warning("Failed to specialize \(baseType.baseTypeName) as \(javaName): \(error)") + log.warning("Failed to specialize \(baseType.baseTypeName) as \(outputName): \(error)") return } translator.specializations[baseType, default: []].insert(specialized) - log.info("Registered specialization: \(javaName) = \(rhsDescription)") + log.info("Registered specialization: \(outputName) = \(rhsDescription)") } // ==== ----------------------------------------------------------------------- // MARK: Specialization support /// Apply specializations to a type if matching entries exist - func applySpecialization(to importedType: ImportedNominalType) { + func applySpecialization(to importedType: ExtractedNominalType) { guard let specializations = translator.specializations[importedType] else { return } for specialized in specializations { - translator.importedTypes[specialized.effectiveJavaName] = specialized - log.info("Applied specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + translator.extractedTypes[specialized.effectiveOutputName] = specialized + log.info("Applied specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } @@ -595,17 +603,17 @@ final class Swift2JavaVisitor { func applyPendingSpecializations() { for (_, specializations) in translator.specializations { for specialized in specializations { - if translator.importedTypes[specialized.effectiveJavaName] != nil { + if translator.extractedTypes[specialized.effectiveOutputName] != nil { continue } - translator.importedTypes[specialized.effectiveJavaName] = specialized - log.info("Applied pending specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + translator.extractedTypes[specialized.effectiveOutputName] = specialized + log.info("Applied pending specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } // Process constrained extensions that were deferred for deferred in deferredConstrainedExtensions { - guard let baseType = translator.importedNominalType(deferred.node.extendedType) else { + guard let baseType = translator.extractedNominalType(deferred.node.extendedType) else { continue } let matchingSpecializations = findMatchingSpecializations( @@ -667,9 +675,9 @@ final class Swift2JavaVisitor { /// Find specializations whose type args match the given where-clause constraints private func findMatchingSpecializations( - extendedType: ImportedNominalType, + extendedType: ExtractedNominalType, whereConstraints: [ParsedWhereConstraint], - ) -> [ImportedNominalType] { + ) -> [ExtractedNominalType] { guard let specializations = translator.specializations[extendedType] else { return [] } @@ -682,7 +690,7 @@ final class Swift2JavaVisitor { /// Where-clauses are conjunctive: every constraint must hold. private func constraintsMatchSpecialization( _ constraints: [ParsedWhereConstraint], - specialized: ImportedNominalType, + specialized: ExtractedNominalType, ) -> Bool { for constraint in constraints { switch constraint { @@ -695,10 +703,10 @@ final class Swift2JavaVisitor { guard let concreteName = specialized.genericArguments[typeParam] else { return false } - guard let concreteType = translator.importedTypes[concreteName] else { + guard let concreteType = translator.extractedTypes[concreteName] else { return false } - guard concreteType.conformsTo(proto, in: translator.importedTypes) else { + guard concreteType.conformsTo(proto, in: translator.extractedTypes) else { return false } } @@ -712,30 +720,40 @@ final class Swift2JavaVisitor { } extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { - func shouldExtract(config: Configuration, log: Logger, in parent: ImportedNominalType?) -> Bool { - // @JavaExport overrides all filters — always extract - if attributes.contains(where: { $0.isJavaExport }) { - return true - } - - let meetsRequiredAccessLevel: Bool = - switch config.effectiveMinimumInputAccessLevelMode { + /// Decide whether this declaration should be extracted + /// + /// Built-in logic checks only the access level required by `config`. An + /// optional `decider` supplied by a downstream language target can override + /// the result on a per-decl basis (e.g. Java honors `@JavaExport` / + /// `@JavaClass` here) + func shouldExtract( + config: any SwiftExtractConfiguration, + log: Logger, + in parent: ExtractedNominalType?, + decider: (any ExtractDecider)? + ) -> Bool { + let accessLevelPasses: Bool = + switch config.swiftExtractAccessLevel { case .public: self.isPublic(in: parent?.swiftNominal.syntax) case .package: self.isAtLeastPackage case .internal: self.isAtLeastInternal } - guard meetsRequiredAccessLevel else { + if let override = decider?.shouldExtract( + decl: DeclSyntax(self), + accessLevelPasses: accessLevelPasses + ) { + if !override { + log.debug("Skip import '\(self.qualifiedNameForDebug)': decider rejected") + } + return override + } + + if !accessLevelPasses { log.debug( - "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)" + "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.swiftExtractAccessLevel)" ) - return false } - guard !attributes.contains(where: { $0.isSwiftJavaMacro }) else { - log.debug("Skip import '\(self.qualifiedNameForDebug)': is Java") - return false - } - - return true + return accessLevelPasses } } diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift similarity index 64% rename from Sources/JExtractSwiftLib/Swift2JavaTranslator.swift rename to Sources/SwiftExtract/SwiftAnalyzer.swift index 6afa5b385..ccea5c0e4 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,35 +13,38 @@ //===----------------------------------------------------------------------===// import Foundation -import SwiftBasicFormat +import Logging import SwiftIfConfig -import SwiftJavaConfigurationShared -import SwiftJavaJNICore import SwiftParser import SwiftSyntax -/// Takes swift interfaces and translates them into Java used to access those. -public final class Swift2JavaTranslator { +/// Drives the analysis of Swift source code into an `AnalysisResult` that +/// downstream language generators can consume. +/// +/// The analysis output is language-neutral; language-specific extraction rules +/// (such as honoring Java's `@JavaExport` or skipping `@JavaClass`-wrapped +/// types) are layered in via an optional `ExtractDecider` +public final class SwiftAnalyzer { static let SWIFT_INTERFACE_SUFFIX = ".swiftinterface" package var log: Logger - let config: Configuration + package let config: any SwiftExtractConfiguration /// The build configuration used to resolve #if conditional compilation blocks. - let buildConfig: any BuildConfiguration + package let buildConfig: any BuildConfiguration /// The name of the Swift module being translated. - let swiftModuleName: String + package let swiftModuleName: String // ==== Input - var inputs: [SwiftJavaInputFile] = [] + package var inputs: [SwiftInputFile] = [] /// File paths that were skipped by swift filters but still need empty output /// files written (when --write-empty-files is set) so SwiftPM doesn't /// complain about missing declared outputs - var filteredOutPaths: [String] = [] + package var filteredOutPaths: [String] = [] /// Sources jextract needs for symbol resolution but does not generate bindings /// for: wrapped Java classes plus real Swift sources from dependency modules. @@ -50,64 +53,84 @@ public final class Swift2JavaTranslator { // ==== Output state - package var importedGlobalVariables: [ImportedFunc] = [] + package var extractedGlobalVariables: [ExtractedFunc] = [] - package var importedGlobalFuncs: [ImportedFunc] = [] + package var extractedGlobalFuncs: [ExtractedFunc] = [] /// A mapping from Swift type names (e.g., A.B) over to the imported nominal /// type representation. - package var importedTypes: [String: ImportedNominalType] = [:] + package var extractedTypes: [String: ExtractedNominalType] = [:] /// Specializations of generic types that will get their concrete Java declarations, "as if" they were independent types - package var specializations: [ImportedNominalType: Set] = [:] + package var specializations: [ExtractedNominalType: Set] = [:] - var lookupContext: SwiftTypeLookupContext! = nil + package var lookupContext: SwiftTypeLookupContext! = nil - var symbolTable: SwiftSymbolTable! { + package var symbolTable: SwiftSymbolTable! { lookupContext?.symbolTable } + /// Optional language-specific extraction decider that can override the + /// built-in access-level filter on a per-decl basis + package let extractDecider: (any ExtractDecider)? + public init( - config: Configuration + config: any SwiftExtractConfiguration, + moduleName: String? = nil, + extractDecider: (any ExtractDecider)? = nil ) { - guard let swiftModule = config.swiftModule else { + guard let swiftModule = moduleName ?? config.swiftModule else { fatalError("Missing 'swiftModule' name.") // FIXME: can we make it required in config? but we shared config for many cases } - self.log = Logger(label: "translator", logLevel: config.logLevel ?? .info) + self.log = Logger(label: "translator", logLevel: config.swiftExtractLogLevel ?? .info) self.config = config self.swiftModuleName = swiftModule + self.extractDecider = extractDecider if let staticBuildConfigPath = config.staticBuildConfigurationFile { do { let data = try Data(contentsOf: URL(fileURLWithPath: staticBuildConfigPath)) let decoder = JSONDecoder() - self.buildConfig = try decoder.decode(StaticBuildConfiguration.self, from: data) + let staticConfig = try decoder.decode(StaticBuildConfiguration.self, from: data) + self.buildConfig = SwiftAnalyzer.overlayingAvailableModules(staticConfig, config.availableImportModules) self.log.info("Using custom static build configuration from: \(staticBuildConfigPath)") } catch { fatalError("Failed to load static build configuration from '\(staticBuildConfigPath)': \(error)") } } else { - self.buildConfig = .jextractDefault + self.buildConfig = SwiftAnalyzer.overlayingAvailableModules(.swiftExtractDefault, config.availableImportModules) } } + + /// Overlay the configured extra importable modules onto a base build config + /// (returns the base unchanged when none are configured). + private static func overlayingAvailableModules( + _ base: Base, + _ availableImportModules: Set + ) -> any BuildConfiguration { + availableImportModules.isEmpty + ? base + : ImportOverlayBuildConfiguration(base: base, availableImportModules: availableImportModules) + } } // ===== -------------------------------------------------------------------------------------------------------------- // MARK: Analysis -extension Swift2JavaTranslator { - var result: AnalysisResult { +extension SwiftAnalyzer { + /// Snapshot of the analysis state as a value-typed `AnalysisResult`. + public var result: AnalysisResult { AnalysisResult( - importedTypes: self.importedTypes, - importedGlobalVariables: self.importedGlobalVariables, - importedGlobalFuncs: self.importedGlobalFuncs, + extractedTypes: self.extractedTypes, + extractedGlobalVariables: self.extractedGlobalVariables, + extractedGlobalFuncs: self.extractedGlobalFuncs, ) } package func add(filePath: String, text: String) { log.debug("Adding: \(filePath)") let sourceFileSyntax = Parser.parse(source: text) - self.inputs.append(SwiftJavaInputFile(syntax: sourceFileSyntax, path: filePath)) + self.inputs.append(SwiftInputFile(syntax: sourceFileSyntax, path: filePath)) } /// Convenient method for analyzing single file. @@ -117,10 +140,10 @@ extension Swift2JavaTranslator { } /// Analyze registered inputs. - func analyze() throws { + package func analyze() throws { prepareForTranslation() - let visitor = Swift2JavaVisitor(translator: self) + let visitor = SwiftAnalysisVisitor(translator: self) for input in self.inputs { log.trace("Analyzing \(input.path)") @@ -133,7 +156,26 @@ extension Swift2JavaTranslator { self.visitFoundationDeclsIfNeeded(with: visitor) } - private func visitFoundationDeclsIfNeeded(with visitor: Swift2JavaVisitor) { + /// Top-level convenience: run analysis on the given Swift sources and return + /// the resulting `AnalysisResult`. + public static func analyze( + sources: [(path: String, text: String)], + moduleName: String, + config: (any SwiftExtractConfiguration)? = nil, + sourceDependencies: SourceDependencies = SourceDependencies(), + extractDecider: (any ExtractDecider)? = nil + ) throws -> AnalysisResult { + let effectiveConfig = config ?? DefaultSwiftExtractConfiguration(swiftModule: moduleName) + let translator = SwiftAnalyzer(config: effectiveConfig, moduleName: moduleName, extractDecider: extractDecider) + translator.sourceDependencies = sourceDependencies + for source in sources { + translator.add(filePath: source.path, text: source.text) + } + try translator.analyze() + return translator.result + } + + private func visitFoundationDeclsIfNeeded(with visitor: SwiftAnalysisVisitor) { // Each entry pairs a Foundation/FoundationEssentials counterpart so the // user-code reference can match either. Entries within the same group are // visited together when any one of the candidates is referenced — so using @@ -206,7 +248,6 @@ extension Swift2JavaTranslator { config: self.config, sourceDependencies: self.sourceDependencies, buildConfig: self.buildConfig, - log: self.log, ) self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) } @@ -236,7 +277,7 @@ extension Swift2JavaTranslator { } } - func check(_ fn: ImportedFunc) -> Bool { + func check(_ fn: ExtractedFunc) -> Bool { if check(fn.functionSignature.result.type) { return true } @@ -246,13 +287,13 @@ extension Swift2JavaTranslator { return false } - if self.importedGlobalFuncs.contains(where: check) { + if self.extractedGlobalFuncs.contains(where: check) { return true } - if self.importedGlobalVariables.contains(where: check) { + if self.extractedGlobalVariables.contains(where: check) { return true } - for importedType in self.importedTypes.values { + for importedType in self.extractedTypes.values { if importedType.initializers.contains(where: check) { return true } @@ -269,26 +310,26 @@ extension Swift2JavaTranslator { // ==== ---------------------------------------------------------------------------------------------------------------- // MARK: Type translation -extension Swift2JavaTranslator { +extension SwiftAnalyzer { /// Try to resolve the given nominal declaration node into its imported representation. - func importedNominalType( + func extractedNominalType( _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, - parent: ImportedNominalType?, - ) -> ImportedNominalType? { - if !nominalNode.shouldExtract(config: config, log: log, in: parent) { + parent: ExtractedNominalType?, + ) -> ExtractedNominalType? { + if !nominalNode.shouldExtract(config: config, log: log, in: parent, decider: extractDecider) { return nil } guard let nominal = symbolTable.lookupType(nominalNode.name.text, parent: parent?.swiftNominal) else { return nil } - return self.importedNominalType(nominal) + return self.extractedNominalType(nominal) } /// Try to resolve the given nominal type node into its imported representation. - func importedNominalType( + func extractedNominalType( _ typeNode: TypeSyntax - ) -> ImportedNominalType? { + ) -> ExtractedNominalType? { guard let swiftType = try? SwiftType(typeNode, lookupContext: lookupContext) else { return nil } @@ -303,28 +344,28 @@ extension Swift2JavaTranslator { return nil } - guard swiftNominalDecl.syntax.shouldExtract(config: config, log: log, in: nil) else { + guard swiftNominalDecl.syntax.shouldExtract(config: config, log: log, in: nil as ExtractedNominalType?, decider: extractDecider) else { return nil } - return importedNominalType(swiftNominalDecl) + return extractedNominalType(swiftNominalDecl) } - func importedNominalType(_ nominal: SwiftNominalTypeDeclaration) -> ImportedNominalType? { + func extractedNominalType(_ nominal: SwiftNominalTypeDeclaration) -> ExtractedNominalType? { let fullName = nominal.qualifiedName - guard shouldJExtractType(qualifiedName: fullName, config: config) else { + guard shouldExtractSwiftType(qualifiedName: fullName, config: config) else { log.debug("Skip import '\(fullName)': filtered by swiftFilterInclude/swiftFilterExclude") return nil } - if let alreadyImported = importedTypes[fullName] { + if let alreadyImported = extractedTypes[fullName] { return alreadyImported } - let importedNominal = try? ImportedNominalType(swiftNominal: nominal, lookupContext: lookupContext) + let importedNominal = try? ExtractedNominalType(swiftNominal: nominal, lookupContext: lookupContext) - importedTypes[fullName] = importedNominal + extractedTypes[fullName] = importedNominal return importedNominal } } @@ -332,7 +373,7 @@ extension Swift2JavaTranslator { // ==== ----------------------------------------------------------------------- // MARK: Errors -public struct Swift2JavaTranslatorError: Error { +public struct SwiftAnalyzerError: Error { let message: String public init(message: String) { diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift new file mode 100644 index 000000000..ba3e20557 --- /dev/null +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 +// +//===----------------------------------------------------------------------===// + +@_exported import SwiftExtractConfigurationShared + +/// The configuration surface required by the language-neutral `SwiftExtract` +/// analysis layer. +/// +/// `SwiftExtract` deliberately does NOT depend on any language-specific +/// configuration module. Instead, each language layer makes its own +/// `Configuration` type conform to this protocol, mapping its settings onto the +/// neutral surface below. This keeps the analysis layer reusable across targets +/// (e.g. Java/JNI/FFM, or other language code generators) without pulling +/// target-specific config types into `SwiftExtract`. +/// +/// `AccessLevelMode` lives in the small `SwiftExtractConfigurationShared` +/// target so language-specific configuration shared modules (e.g. +/// `SwiftJavaConfigurationShared`) can use the same enum directly without +/// taking a dependency on SwiftSyntax. +/// +/// The enum-typed `swiftExtractLogLevel` member uses a `swiftExtract`-prefixed +/// name so a conforming type can keep its own, differently-typed `logLevel` +/// member without a name collision. +public protocol SwiftExtractConfiguration { + /// Name of the Swift module being analyzed. + var swiftModule: String? { get } + + /// Optional path to a JSON `StaticBuildConfiguration` used to resolve `#if`. + var staticBuildConfigurationFile: String? { get } + + /// Glob patterns selecting which Swift files/types to extract. + var swiftFilterInclude: [String]? { get } + + /// Glob patterns excluding Swift files/types from extraction. + var swiftFilterExclude: [String]? { get } + + /// Stub declarations for imported modules whose source is unavailable to the + /// analyzer. Keyed by module name; values are Swift declaration strings parsed + /// as if they belonged to that module. + var importedModuleStubs: [String: [String]]? { get } + + /// Minimum access level required for a declaration to be extracted. + var swiftExtractAccessLevel: AccessLevelMode { get } + + /// Whether operator declarations (e.g. `static func + (…)`) should be + /// extracted as ordinary `.function`s. Most targets (e.g. Java) cannot express + /// Swift operators and leave this `false`; other language code generators that + /// map operators to language constructs set it `true` and recognize the + /// operator functions in a post-analysis pass. + var extractsOperators: Bool { get } + + /// Verbosity for the analyzer's logger; `nil` falls back to `.info`. + var swiftExtractLogLevel: Logger.Level? { get } + + /// Whether to extract initializers of *generic* nominal types even when they + /// are not (yet) specialized. swift-java skips these by default (an open + /// generic isn't directly constructible); other language code generators that + /// specialize generics in a post-analysis pass set this `true` so the base + /// type's initializers are available to clone onto the specialization. + /// Default: false. + var extractsGenericTypeInitializers: Bool { get } + + /// Module names that should be treated as importable when resolving + /// `#if canImport()` conditions, in addition to whatever the build + /// configuration already knows. Lets a target opt-in to extracting code + /// guarded behind `#if canImport(MyModule)` (e.g. another language code + /// generator can declare its runtime module importable here). Default: empty. + var availableImportModules: Set { get } + + /// Whether the given module name has stub declarations configured. + func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool +} + +extension SwiftExtractConfiguration { + public var availableImportModules: Set { [] } + + public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool { + importedModuleStubs?.keys.contains(moduleName) ?? false + } +} + +/// A minimal, self-contained `SwiftExtractConfiguration` for callers that only +/// need analysis (tests, tools) and don't have a richer language-specific +/// configuration to supply. +public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { + public var swiftModule: String? + public var staticBuildConfigurationFile: String? + public var swiftFilterInclude: [String]? + public var swiftFilterExclude: [String]? + public var importedModuleStubs: [String: [String]]? + public var swiftExtractAccessLevel: AccessLevelMode + public var swiftExtractLogLevel: Logger.Level? + public var extractsOperators: Bool + public var extractsGenericTypeInitializers: Bool + public var availableImportModules: Set + + public init( + swiftModule: String? = nil, + accessLevel: AccessLevelMode = .public, + logLevel: Logger.Level? = nil, + extractsOperators: Bool = false, + extractsGenericTypeInitializers: Bool = false, + staticBuildConfigurationFile: String? = nil, + swiftFilterInclude: [String]? = nil, + swiftFilterExclude: [String]? = nil, + importedModuleStubs: [String: [String]]? = nil, + availableImportModules: Set = [] + ) { + self.swiftModule = swiftModule + self.swiftExtractAccessLevel = accessLevel + self.swiftExtractLogLevel = logLevel + self.extractsOperators = extractsOperators + self.extractsGenericTypeInitializers = extractsGenericTypeInitializers + self.staticBuildConfigurationFile = staticBuildConfigurationFile + self.swiftFilterInclude = swiftFilterInclude + self.swiftFilterExclude = swiftFilterExclude + self.importedModuleStubs = importedModuleStubs + self.availableImportModules = availableImportModules + } +} diff --git a/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift new file mode 100644 index 000000000..a11d76d73 --- /dev/null +++ b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import SwiftIfConfig +import SwiftSyntax + +/// A default, fixed build configuration during static analysis for interface extraction. +package struct SwiftExtractDefaultBuildConfiguration: BuildConfiguration { + package static let shared = SwiftExtractDefaultBuildConfiguration() + + private var base: StaticBuildConfiguration + + package init() { + guard let url = Bundle.module.url(forResource: "static-build-config", withExtension: "json") else { + fatalError("static-build-config.json is not found in module bundle") + } + do { + let data = try Data(contentsOf: url) + let decoder = JSONDecoder() + base = try decoder.decode(StaticBuildConfiguration.self, from: data) + } catch { + fatalError("\(error)") + } + } + + package func isCustomConditionSet(name: String) throws -> Bool { + base.isCustomConditionSet(name: name) + } + + package func hasFeature(name: String) throws -> Bool { + base.hasFeature(name: name) + } + + package func hasAttribute(name: String) throws -> Bool { + base.hasAttribute(name: name) + } + + package func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { + try base.canImport(importPath: importPath, version: version) + } + + package func isActiveTargetOS(name: String) throws -> Bool { + true + } + + package func isActiveTargetArchitecture(name: String) throws -> Bool { + true + } + + package func isActiveTargetEnvironment(name: String) throws -> Bool { + true + } + + package func isActiveTargetRuntime(name: String) throws -> Bool { + true + } + + package func isActiveTargetPointerAuthentication(name: String) throws -> Bool { + true + } + + package func isActiveTargetObjectFormat(name: String) throws -> Bool { + true + } + + package var targetPointerBitWidth: Int { + base.targetPointerBitWidth + } + + package var targetAtomicBitWidths: [Int] { + base.targetAtomicBitWidths + } + + package var endianness: Endianness { + base.endianness + } + + package var languageVersion: VersionTuple { + base.languageVersion + } + + package var compilerVersion: VersionTuple { + base.compilerVersion + } +} + +extension BuildConfiguration where Self == SwiftExtractDefaultBuildConfiguration { + package static var swiftExtractDefault: SwiftExtractDefaultBuildConfiguration { + .shared + } +} + +/// Wraps any `BuildConfiguration` and additionally reports a configured set of +/// module names as importable. Used so a target can extract declarations guarded +/// behind `#if canImport()` for modules the static build configuration +/// does not otherwise know about (e.g. another language code generator can +/// declare its own runtime module importable for extraction). +package struct ImportOverlayBuildConfiguration: BuildConfiguration { + package var base: Base + package var availableImportModules: Set + + package init(base: Base, availableImportModules: Set) { + self.base = base + self.availableImportModules = availableImportModules + } + + package func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { + // `importPath` is the dotted module path; the leading component is the module. + if let moduleName = importPath.first?.1, availableImportModules.contains(moduleName) { + return true + } + return try base.canImport(importPath: importPath, version: version) + } + + package func isCustomConditionSet(name: String) throws -> Bool { try base.isCustomConditionSet(name: name) } + package func hasFeature(name: String) throws -> Bool { try base.hasFeature(name: name) } + package func hasAttribute(name: String) throws -> Bool { try base.hasAttribute(name: name) } + package func isActiveTargetOS(name: String) throws -> Bool { try base.isActiveTargetOS(name: name) } + package func isActiveTargetArchitecture(name: String) throws -> Bool { try base.isActiveTargetArchitecture(name: name) } + package func isActiveTargetEnvironment(name: String) throws -> Bool { try base.isActiveTargetEnvironment(name: name) } + package func isActiveTargetRuntime(name: String) throws -> Bool { try base.isActiveTargetRuntime(name: name) } + package func isActiveTargetPointerAuthentication(name: String) throws -> Bool { try base.isActiveTargetPointerAuthentication(name: name) } + package func isActiveTargetObjectFormat(name: String) throws -> Bool { try base.isActiveTargetObjectFormat(name: name) } + package var targetPointerBitWidth: Int { base.targetPointerBitWidth } + package var targetAtomicBitWidths: [Int] { base.targetAtomicBitWidths } + package var endianness: Endianness { base.endianness } + package var languageVersion: VersionTuple { base.languageVersion } + package var compilerVersion: VersionTuple { base.compilerVersion } +} diff --git a/Sources/JExtractSwiftLib/JExtractFileFilter.swift b/Sources/SwiftExtract/SwiftFileFilter.swift similarity index 92% rename from Sources/JExtractSwiftLib/JExtractFileFilter.swift rename to Sources/SwiftExtract/SwiftFileFilter.swift index aef41240f..e0e0e7741 100644 --- a/Sources/JExtractSwiftLib/JExtractFileFilter.swift +++ b/Sources/SwiftExtract/SwiftFileFilter.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,15 +12,13 @@ // //===----------------------------------------------------------------------===// -import SwiftJavaConfigurationShared - // ==== ----------------------------------------------------------------------- // MARK: Swift filter pattern classification /// A filter pattern is either a file-path pattern (uses `/` or a Swift file /// extension) or a type-name pattern (uses `.`). Plain names with neither /// match both -enum SwiftFilterPatternKind { +package enum SwiftFilterPatternKind { /// Pattern contains `/`, ends in a Swift source extension, or is a bare `**` /// glob — matches against relative file paths case filePath @@ -31,7 +29,7 @@ enum SwiftFilterPatternKind { case plain } -func classifyPattern(_ pattern: String) -> SwiftFilterPatternKind { +package func classifyPattern(_ pattern: String) -> SwiftFilterPatternKind { // Directory separator is the strongest signal for a file path if pattern.contains("/") { return .filePath @@ -167,7 +165,7 @@ private func matchSegment(_ segment: String, against pattern: String) -> Bool { /// - `**` matches zero or more path segments /// - `*` at the end of a segment matches any suffix (e.g. `Us*` matches `User.swift`) /// - exact segment match otherwise -func matchesFilePathFilter(relativePath: String, pattern: String) -> Bool { +package func matchesFilePathFilter(relativePath: String, pattern: String) -> Bool { matchesGlob(value: relativePath, pattern: pattern, separator: "/") } @@ -181,7 +179,7 @@ func matchesFilePathFilter(relativePath: String, pattern: String) -> Bool { /// - `**` matches zero or more name components /// - `*` at the end of a component matches any suffix /// - exact component match otherwise -func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bool { +package func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bool { matchesGlob(value: qualifiedName, pattern: pattern, separator: ".") } @@ -189,13 +187,13 @@ func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bool { // MARK: Combined filter application /// Determine whether a file at the given `relativePath` (including `.swift` -/// extension) should be included in jextract processing, based on the -/// include/exclude filters in `config`. +/// extension) should be included in extraction, based on the include/exclude +/// filters in `config`. /// /// Only file-path patterns (containing `/`) and plain patterns (no `/` or `.`) -/// are checked here. Type-name patterns are skipped — use `shouldJExtractType` -/// for those -func shouldJExtractFile(relativePath: String, config: Configuration) -> Bool { +/// are checked here. Type-name patterns are skipped — use +/// `shouldExtractSwiftType` for those +package func shouldExtractSwiftFile(relativePath: String, config: any SwiftExtractConfiguration) -> Bool { if let includeFilters = config.swiftFilterInclude, !includeFilters.isEmpty { // Must match at least one file-level include pattern. // If all include patterns are type-name patterns, don't filter at file level @@ -244,9 +242,9 @@ private func matchesFilePattern(relativePath: String, pattern: String) -> Bool { /// `config`. /// /// Only type-name patterns (containing `.`) and plain patterns (no `/` or `.`) -/// are checked here. File-path patterns are skipped — use `shouldJExtractFile` +/// are checked here. File-path patterns are skipped — use `shouldExtractSwiftFile` /// for those -func shouldJExtractType(qualifiedName: String, config: Configuration) -> Bool { +package func shouldExtractSwiftType(qualifiedName: String, config: any SwiftExtractConfiguration) -> Bool { if let includeFilters = config.swiftFilterInclude, !includeFilters.isEmpty { let typePatterns = includeFilters.filter { classifyPattern($0) != .filePath } if !typePatterns.isEmpty { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift b/Sources/SwiftExtract/SwiftTypes/ExtractedSwiftModule.swift similarity index 73% rename from Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift rename to Sources/SwiftExtract/SwiftTypes/ExtractedSwiftModule.swift index 8db800d63..f8c1a6d6f 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift +++ b/Sources/SwiftExtract/SwiftTypes/ExtractedSwiftModule.swift @@ -12,13 +12,13 @@ // //===----------------------------------------------------------------------===// -struct ImportedSwiftModule: Hashable { - let name: String - let availableWithModuleName: String? - var alternativeModuleNames: Set - var isMainSourceOfSymbols: Bool +public struct ExtractedSwiftModule: Hashable { + public let name: String + public let availableWithModuleName: String? + public var alternativeModuleNames: Set + public var isMainSourceOfSymbols: Bool - init( + public init( name: String, availableWithModuleName: String? = nil, alternativeModuleNames: Set = [], @@ -30,11 +30,11 @@ struct ImportedSwiftModule: Hashable { self.isMainSourceOfSymbols = isMainSourceOfSymbols } - static func == (lhs: Self, rhs: Self) -> Bool { + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.name == rhs.name } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(name) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift b/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift similarity index 87% rename from Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift index a36475c7b..ed20722e6 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift @@ -15,15 +15,15 @@ import SwiftSyntax /// Scan importing modules. -func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftModule] { - var importingModuleNames: [ImportedSwiftModule] = [] +package func importingModules(sourceFile: SourceFileSyntax) -> [ExtractedSwiftModule] { + var importingModuleNames: [ExtractedSwiftModule] = [] for item in sourceFile.statements { if let importDecl = item.item.as(ImportDeclSyntax.self) { guard let moduleName = importDecl.path.first?.name.text else { continue } importingModuleNames.append( - ImportedSwiftModule(name: moduleName, availableWithModuleName: nil, alternativeModuleNames: []) + ExtractedSwiftModule(name: moduleName, availableWithModuleName: nil, alternativeModuleNames: []) ) } else if let ifConfigDecl = item.item.as(IfConfigDeclSyntax.self) { importingModuleNames.append(contentsOf: modules(from: ifConfigDecl)) @@ -32,7 +32,7 @@ func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftModule] { return importingModuleNames } -private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ImportedSwiftModule] { +private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ExtractedSwiftModule] { guard let firstClause = ifConfigDecl.clauses.first, let calledExpression = firstClause.condition?.as(FunctionCallExprSyntax.self)?.calledExpression.as( @@ -43,7 +43,7 @@ private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ImportedSwiftMod return [] } - var modules: [ImportedSwiftModule] = [] + var modules: [ExtractedSwiftModule] = [] modules.reserveCapacity(ifConfigDecl.clauses.count) for (index, clause) in ifConfigDecl.clauses.enumerated() { @@ -65,7 +65,7 @@ private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ImportedSwiftMod } let clauseModules = importedModuleNames.map { - ImportedSwiftModule( + ExtractedSwiftModule( name: $0, availableWithModuleName: importModuleName, alternativeModuleNames: [] @@ -76,7 +76,7 @@ private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ImportedSwiftMod if clauseModules.count == 1 && index == (ifConfigDecl.clauses.count - 1) && clause.poundKeyword.tokenKind == .poundElse { - var fallbackModule: ImportedSwiftModule = clauseModules[0] + var fallbackModule: ExtractedSwiftModule = clauseModules[0] var moduleNames: [String] = [] moduleNames.reserveCapacity(modules.count) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift b/Sources/SwiftExtract/SwiftTypes/SwiftEffectSpecifier.swift similarity index 92% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftEffectSpecifier.swift index fb28586b1..c59d316d0 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftEffectSpecifier.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -enum SwiftEffectSpecifier: Equatable { +public enum SwiftEffectSpecifier: Equatable { case `throws` case `async` } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift b/Sources/SwiftExtract/SwiftTypes/SwiftEnumCaseParameter.swift similarity index 86% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftEnumCaseParameter.swift index 55682152d..335739397 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftEnumCaseParameter.swift @@ -14,13 +14,13 @@ import SwiftSyntax -struct SwiftEnumCaseParameter: Equatable { - var name: String? - var type: SwiftType +public struct SwiftEnumCaseParameter: Equatable { + public var name: String? + public var type: SwiftType } extension SwiftEnumCaseParameter { - init( + public init( _ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext ) throws { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift similarity index 92% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index a957c0ea1..477fb5f2e 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -15,7 +15,7 @@ import SwiftSyntax import SwiftSyntaxBuilder -enum SwiftGenericRequirement: Equatable { +public enum SwiftGenericRequirement: Equatable { case inherits(SwiftType, SwiftType) case equals(SwiftType, SwiftType) } @@ -23,22 +23,27 @@ enum SwiftGenericRequirement: Equatable { /// Provides a complete signature for a Swift function, which includes its /// parameters and return type. public struct SwiftFunctionSignature: Equatable { - var selfParameter: SwiftSelfParameter? - var parameters: [SwiftParameter] - var result: SwiftResult - var effectSpecifiers: [SwiftEffectSpecifier] - var genericParameters: [SwiftGenericParameterDeclaration] - var genericRequirements: [SwiftGenericRequirement] - - var isAsync: Bool { + public var selfParameter: SwiftSelfParameter? + public var parameters: [SwiftParameter] + public var result: SwiftResult + public var effectSpecifiers: [SwiftEffectSpecifier] + public var genericParameters: [SwiftGenericParameterDeclaration] + public var genericRequirements: [SwiftGenericRequirement] + + public var isAsync: Bool { effectSpecifiers.contains(.async) } - var isThrowing: Bool { + public var isThrowing: Bool { effectSpecifiers.contains(.throws) } - init( + /// Whether any parameter is variadic (`T...`). + public var hasVariadicParams: Bool { + parameters.contains(where: \.isVariadic) + } + + public init( selfParameter: SwiftSelfParameter? = nil, parameters: [SwiftParameter], result: SwiftResult, @@ -56,7 +61,7 @@ public struct SwiftFunctionSignature: Equatable { } /// Describes the "self" parameter of a Swift function signature. -enum SwiftSelfParameter: Equatable { +public enum SwiftSelfParameter: Equatable { /// 'self' is an instance parameter. case instance(convention: SwiftParameterConvention, swiftType: SwiftType) @@ -68,7 +73,7 @@ enum SwiftSelfParameter: Equatable { /// to form the call. case initializer(SwiftType) - var selfType: SwiftType { + public var selfType: SwiftType { get { switch self { case .instance(_, let swiftType), .staticMethod(let swiftType), .initializer(let swiftType): @@ -89,7 +94,7 @@ enum SwiftSelfParameter: Equatable { } extension SwiftFunctionSignature { - init( + public init( _ node: InitializerDeclSyntax, enclosingType: SwiftType?, lookupContext: SwiftTypeLookupContext @@ -126,7 +131,7 @@ extension SwiftFunctionSignature { ) } - init( + public init( _ node: EnumCaseElementSyntax, enclosingType: SwiftType, lookupContext: SwiftTypeLookupContext @@ -145,7 +150,7 @@ extension SwiftFunctionSignature { ) } - init( + public init( _ node: FunctionDeclSyntax, enclosingType: SwiftType?, lookupContext: SwiftTypeLookupContext @@ -212,7 +217,7 @@ extension SwiftFunctionSignature { ) } - static func translateGenericParameters( + public static func translateGenericParameters( parameterClause: GenericParameterClauseSyntax?, whereClause: GenericWhereClauseSyntax?, lookupContext: SwiftTypeLookupContext @@ -270,7 +275,7 @@ extension SwiftFunctionSignature { /// Translate the function signature, returning the list of translated /// parameters and effect specifiers. - static func translateFunctionSignature( + public static func translateFunctionSignature( _ signature: FunctionSignatureSyntax, lookupContext: SwiftTypeLookupContext ) throws -> ([SwiftParameter], [SwiftEffectSpecifier]) { @@ -289,7 +294,7 @@ extension SwiftFunctionSignature { return (parameters, effectSpecifiers) } - init( + public init( _ varNode: VariableDeclSyntax, isSet: Bool, enclosingType: SwiftType?, @@ -340,7 +345,7 @@ extension SwiftFunctionSignature { self.genericRequirements = [] } - init( + public init( _ subscriptNode: SubscriptDeclSyntax, isSet: Bool, enclosingType: SwiftType?, @@ -443,7 +448,7 @@ extension VariableDeclSyntax { /// /// - Parameters: /// - binding the pattern binding in this declaration. - func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { + public func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { if self.bindingSpecifier.tokenKind == .keyword(.let) { return [.get] } @@ -457,15 +462,19 @@ extension VariableDeclSyntax { } extension AccessorBlockSyntax { - struct SupportedAccessorKinds: OptionSet { - var rawValue: UInt8 + public struct SupportedAccessorKinds: OptionSet { + public var rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } - static var get: Self = .init(rawValue: 1 << 0) - static var set: Self = .init(rawValue: 1 << 1) + public static var get: Self = .init(rawValue: 1 << 0) + public static var set: Self = .init(rawValue: 1 << 1) } /// Determine what operations (i.e. get and/or set) supported in this `AccessorBlockSyntax` - func supportedAccessorKinds() -> SupportedAccessorKinds { + public func supportedAccessorKinds() -> SupportedAccessorKinds { switch self.accessors { case .getter: return [.get] @@ -485,7 +494,7 @@ extension AccessorBlockSyntax { } } -enum SwiftFunctionTranslationError: Error { +public enum SwiftFunctionTranslationError: Error { case `throws`(ThrowsClauseSyntax) case async(TokenSyntax) case classMethod(TokenSyntax) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift similarity index 77% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift index 628cbc7a1..b127e6fb7 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift @@ -14,20 +14,32 @@ import SwiftSyntax -struct SwiftFunctionType: Equatable { - enum Convention: Equatable { +public struct SwiftFunctionType: Equatable { + public enum Convention: Equatable { case swift case c } - var convention: Convention - var parameters: [SwiftParameter] - var resultType: SwiftType - var isEscaping: Bool = false + public var convention: Convention + public var parameters: [SwiftParameter] + public var resultType: SwiftType + public var isEscaping: Bool = false + + public init( + convention: Convention, + parameters: [SwiftParameter], + resultType: SwiftType, + isEscaping: Bool = false + ) { + self.convention = convention + self.parameters = parameters + self.resultType = resultType + self.isEscaping = isEscaping + } } extension SwiftFunctionType: CustomStringConvertible { - var description: String { + public var description: String { let parameterString = parameters.map { $0.descriptionInType }.joined(separator: ", ") let conventionPrefix = switch convention { @@ -40,7 +52,7 @@ extension SwiftFunctionType: CustomStringConvertible { } extension SwiftFunctionType { - init( + public init( _ node: FunctionTypeSyntax, convention: Convention, isEscaping: Bool = false, diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift similarity index 94% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift index 696b1f1a5..689c24f61 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift @@ -15,16 +15,16 @@ import SwiftSyntax import SwiftSyntaxBuilder -enum SwiftKnownModule: String { +public enum SwiftKnownModule: String { case swift = "Swift" case foundation = "Foundation" case foundationEssentials = "FoundationEssentials" - var name: String { + public var name: String { self.rawValue } - var symbolTable: SwiftModuleSymbolTable { + public var symbolTable: SwiftModuleSymbolTable { switch self { case .swift: swiftSymbolTable case .foundation: foundationSymbolTable @@ -32,7 +32,7 @@ enum SwiftKnownModule: String { } } - var sourceFile: SourceFileSyntax { + public var sourceFile: SourceFileSyntax { switch self { case .swift: swiftSourceFile case .foundation: foundationEssentialsSourceFile @@ -130,6 +130,11 @@ private let foundationEssentialsSourceFile: SourceFileSyntax = """ } public struct UUID {} + + public struct URL { + public init?(string: String) + public var absoluteString: String { get } + } """ private var foundationSourceFile: SourceFileSyntax { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift similarity index 87% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift index 5b1e28933..7b4ea12ea 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift @@ -14,7 +14,7 @@ import SwiftSyntax -enum SwiftKnownType: Equatable { +public enum SwiftKnownType: Equatable { case bool case int case uint @@ -53,10 +53,7 @@ enum SwiftKnownType: Equatable { case foundationUUID case essentialsUUID - // SwiftRuntimeFunctions - case swiftJavaError - - init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { + public init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { switch kind { case .bool: self = .bool case .int: self = .int @@ -109,11 +106,10 @@ enum SwiftKnownType: Equatable { case .essentialsDate: self = .essentialsDate case .foundationUUID: self = .foundationUUID case .essentialsUUID: self = .essentialsUUID - case .swiftJavaError: self = .swiftJavaError } } - var kind: SwiftKnownTypeDeclKind { + public var kind: SwiftKnownTypeDeclKind { switch self { case .bool: .bool case .int: .int @@ -150,12 +146,11 @@ enum SwiftKnownType: Equatable { case .essentialsDate: .essentialsDate case .foundationUUID: .foundationUUID case .essentialsUUID: .essentialsUUID - case .swiftJavaError: .swiftJavaError } } } -enum SwiftKnownTypeDeclKind: String, Hashable { +public enum SwiftKnownTypeDeclKind: String, Hashable { // Swift case bool = "Swift.Bool" case int = "Swift.Int" @@ -195,10 +190,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { case foundationUUID = "Foundation.UUID" case essentialsUUID = "FoundationEssentials.UUID" - // SwiftRuntimeFunctions - case swiftJavaError = "SwiftRuntimeFunctions.SwiftJavaError" - - var moduleAndName: (module: String, name: String) { + public var moduleAndName: (module: String, name: String) { let qualified = self.rawValue let period = qualified.firstIndex(of: ".")! return ( @@ -207,7 +199,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { ) } - var isPointer: Bool { + public var isPointer: Bool { switch self { case .unsafePointer, .unsafeMutablePointer, .unsafeRawPointer, .unsafeMutableRawPointer: return true @@ -215,19 +207,4 @@ enum SwiftKnownTypeDeclKind: String, Hashable { return false } } - - /// Indicates whether this known type is translated by `wrap-java` - /// into the same type as `jextract`. - /// - /// This means we do not have to perform any mapping when passing - /// this type between jextract and wrap-java - var isDirectlyTranslatedToWrapJava: Bool { - switch self { - case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string, - .void: - return true - default: - return false - } - } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypes.swift similarity index 51% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownTypes.swift index 198a1c14d..6c9c93a5d 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypes.swift @@ -12,50 +12,50 @@ // //===----------------------------------------------------------------------===// -struct SwiftKnownTypes { +public struct SwiftKnownTypes { private let symbolTable: SwiftSymbolTable - init(symbolTable: SwiftSymbolTable) { + public init(symbolTable: SwiftSymbolTable) { self.symbolTable = symbolTable } - var bool: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.bool])) } - var int: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int])) } - var uint: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint])) } - var int8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int8])) } - var uint8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint8])) } - var int16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int16])) } - var uint16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint16])) } - var int32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int32])) } - var uint32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint32])) } - var int64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int64])) } - var uint64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint64])) } - var float: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.float])) } - var double: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.double])) } - var unsafeRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawPointer])) } - var unsafeRawBufferPointer: SwiftType { + public var bool: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.bool])) } + public var int: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int])) } + public var uint: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint])) } + public var int8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int8])) } + public var uint8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint8])) } + public var int16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int16])) } + public var uint16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint16])) } + public var int32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int32])) } + public var uint32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint32])) } + public var int64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int64])) } + public var uint64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint64])) } + public var float: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.float])) } + public var double: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.double])) } + public var unsafeRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawPointer])) } + public var unsafeRawBufferPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawBufferPointer])) } - var unsafeMutableRawPointer: SwiftType { + public var unsafeMutableRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeMutableRawPointer])) } - var string: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.string])) } + public var string: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.string])) } - var foundationDataProtocol: SwiftType { + public var foundationDataProtocol: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationDataProtocol])) } - var foundationData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationData])) } - var essentialsDataProtocol: SwiftType { + public var foundationData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationData])) } + public var essentialsDataProtocol: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsDataProtocol])) } - var essentialsData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsData])) } - var foundationUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationUUID])) } - var essentialsUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsUUID])) } + public var essentialsData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsData])) } + public var foundationUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationUUID])) } + public var essentialsUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsUUID])) } /// `(UnsafeRawPointer, Long) -> ()` function type. /// /// Commonly used to initialize a buffer using the passed bytes and length. - var functionInitializeByteBuffer: SwiftType { + public var functionInitializeByteBuffer: SwiftType { .function( SwiftFunctionType( convention: .c, @@ -68,7 +68,7 @@ struct SwiftKnownTypes { ) } - func unsafePointer(_ pointeeType: SwiftType) -> SwiftType { + public func unsafePointer(_ pointeeType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafePointer], @@ -77,7 +77,7 @@ struct SwiftKnownTypes { ) } - func unsafeMutablePointer(_ pointeeType: SwiftType) -> SwiftType { + public func unsafeMutablePointer(_ pointeeType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafeMutablePointer], @@ -86,7 +86,7 @@ struct SwiftKnownTypes { ) } - func unsafeBufferPointer(_ elementType: SwiftType) -> SwiftType { + public func unsafeBufferPointer(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafeBufferPointer], @@ -95,7 +95,7 @@ struct SwiftKnownTypes { ) } - func unsafeMutableBufferPointer(_ elementType: SwiftType) -> SwiftType { + public func unsafeMutableBufferPointer(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafeMutableBufferPointer], @@ -104,7 +104,7 @@ struct SwiftKnownTypes { ) } - func optionalSugar(_ wrappedType: SwiftType) -> SwiftType { + public func optionalSugar(_ wrappedType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( sugarName: .optional, @@ -114,7 +114,7 @@ struct SwiftKnownTypes { ) } - func arraySugar(_ elementType: SwiftType) -> SwiftType { + public func arraySugar(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( sugarName: .array, @@ -124,7 +124,7 @@ struct SwiftKnownTypes { ) } - func dictionarySugar(_ keyType: SwiftType, _ valueType: SwiftType) -> SwiftType { + public func dictionarySugar(_ keyType: SwiftType, _ valueType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( sugarName: .dictionary, @@ -134,7 +134,7 @@ struct SwiftKnownTypes { ) } - func set(_ elementType: SwiftType) -> SwiftType { + public func set(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.set], @@ -145,13 +145,13 @@ struct SwiftKnownTypes { /// Returns the known representative concrete type if there is one for the /// given protocol kind. E.g. `Data` for `DataProtocol` - func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftType? { + public func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftType? { guard let kind = Self.representativeType(of: knownProtocol) else { return nil } return .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[kind])) } /// Returns the representative concrete type kind for a protocol, if one exists - static func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftKnownTypeDeclKind? { + public static func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftKnownTypeDeclKind? { switch knownProtocol { case .foundationDataProtocol: return .foundationData case .essentialsDataProtocol: return .essentialsData diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift b/Sources/SwiftExtract/SwiftTypes/SwiftModuleSymbolTable.swift similarity index 63% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftModuleSymbolTable.swift index c77038596..5e4f5ba6a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftModuleSymbolTable.swift @@ -15,63 +15,63 @@ import SwiftSyntax import SwiftSyntaxBuilder -struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { +public struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { /// The name of this module. - let moduleName: String + public let moduleName: String /// The name of module required to be imported and checked via canImport statement. - let requiredAvailablityOfModuleWithName: String? + public let requiredAvailablityOfModuleWithName: String? /// Data about alternative modules which provides desired symbos e.g. FoundationEssentials is non-Darwin platform alternative for Foundation - let alternativeModules: AlternativeModuleNamesData? + public let alternativeModules: AlternativeModuleNamesData? /// The top-level nominal types, found by name. - var topLevelTypes: [String: SwiftNominalTypeDeclaration] = [:] + public var topLevelTypes: [String: SwiftNominalTypeDeclaration] = [:] /// The top-level typealias declarations, found by name. - var topLevelTypeAliases: [String: SwiftTypeAliasDeclaration] = [:] + public var topLevelTypeAliases: [String: SwiftTypeAliasDeclaration] = [:] /// The nested types defined within this module. The map itself is indexed by the /// identifier of the nominal type declaration, and each entry is a map from the nested /// type name to the nominal type declaration. - var nestedTypes: [SwiftNominalTypeDeclaration: [String: SwiftNominalTypeDeclaration]] = [:] + public var nestedTypes: [SwiftNominalTypeDeclaration: [String: SwiftNominalTypeDeclaration]] = [:] /// The nested typealias declarations defined within this module. The map itself is indexed /// by the nominal type declaration, and each entry is a map from the nested typealias /// name to the typealias declaration. - var nestedTypeAliases: [SwiftNominalTypeDeclaration: [String: SwiftTypeAliasDeclaration]] = [:] + public var nestedTypeAliases: [SwiftNominalTypeDeclaration: [String: SwiftTypeAliasDeclaration]] = [:] /// Look for a top-level type with the given name. - func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { + public func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { topLevelTypes[name] } /// Look for a top-level typealias with the given name. - func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { + public func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { topLevelTypeAliases[name] } // Look for a nested type with the given name. - func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { + public func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { nestedTypes[parent]?[name] } // Look for a nested typealias with the given name. - func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { + public func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { nestedTypeAliases[parent]?[name] } - func isAlternative(for moduleName: String) -> Bool { + public func isAlternative(for moduleName: String) -> Bool { alternativeModules.flatMap { $0.moduleNames.contains(moduleName) } ?? false } } extension SwiftModuleSymbolTable { - struct AlternativeModuleNamesData { + public struct AlternativeModuleNamesData { /// Flag indicating module should be used as source of symbols to avoid duplication of symbols. - let isMainSourceOfSymbols: Bool + public let isMainSourceOfSymbols: Bool /// Names of modules which are alternative for currently checked module. - let moduleNames: Set + public let moduleNames: Set } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift similarity index 77% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift index 07543bf23..b6f6399e8 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -19,21 +19,21 @@ import SwiftSyntax public typealias NominalTypeDeclSyntaxNode = any DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax -package class SwiftTypeDeclaration { +public class SwiftTypeDeclaration { // The short path from module root to the file in which this nominal was originally declared. // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. - let sourceFilePath: String + public let sourceFilePath: String /// The module in which this nominal type is defined. If this is a nested type, the /// module might be different from that of the parent type, if this nominal type /// is defined in an extension within another module. - let moduleName: String + public let moduleName: String /// The name of this nominal type, e.g. 'MyCollection'. - let name: String + public let name: String - init(sourceFilePath: String, moduleName: String, name: String) { + public init(sourceFilePath: String, moduleName: String, name: String) { self.sourceFilePath = sourceFilePath self.moduleName = moduleName self.name = name @@ -41,11 +41,11 @@ package class SwiftTypeDeclaration { } /// A syntax node paired with a simple file path -package struct SwiftJavaInputFile { - let syntax: SourceFileSyntax +public struct SwiftInputFile { + public let syntax: SourceFileSyntax /// Simple file path of the file from which the syntax node was parsed. - let path: String - package init(syntax: SourceFileSyntax, path: String) { + public let path: String + public init(syntax: SourceFileSyntax, path: String) { self.syntax = syntax self.path = path } @@ -53,8 +53,8 @@ package struct SwiftJavaInputFile { /// Describes a nominal type declaration, which can be of any kind (class, struct, etc.) /// and has a name, parent type (if nested), and owning module. -package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { - enum Kind { +public class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { + public enum Kind { case actor case `class` case `enum` @@ -63,27 +63,27 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } /// The syntax node this declaration is derived from. - let syntax: NominalTypeDeclSyntaxNode + @_spi(Testing) public let syntax: NominalTypeDeclSyntaxNode /// The kind of nominal type. - let kind: Kind + public let kind: Kind /// The parent nominal type when this nominal type is nested inside another type, e.g., /// MyCollection.Iterator. - let parent: SwiftNominalTypeDeclaration? + public let parent: SwiftNominalTypeDeclaration? /// The generic parameters of this nominal type. - let genericParameters: [SwiftGenericParameterDeclaration] + public let genericParameters: [SwiftGenericParameterDeclaration] /// Identify this nominal declaration as one of the known standard library /// types, like 'Swift.Int[. - private(set) lazy var knownTypeKind: SwiftKnownTypeDeclKind? = { + public private(set) lazy var knownTypeKind: SwiftKnownTypeDeclKind? = { self.computeKnownStandardLibraryType() }() /// Create a nominal type declaration from the syntax node for a nominal type /// declaration. - init( + @_spi(Testing) public init( name: String, sourceFilePath: String, moduleName: String, @@ -109,7 +109,7 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: name) } - private(set) lazy var firstInheritanceType: TypeSyntax? = { + public private(set) lazy var firstInheritanceType: TypeSyntax? = { guard let firstInheritanceType = self.syntax.inheritanceClause?.inheritedTypes.first else { return nil } @@ -117,16 +117,16 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { return firstInheritanceType.type }() - var inheritanceTypes: InheritedTypeListSyntax? { + public var inheritanceTypes: InheritedTypeListSyntax? { self.syntax.inheritanceClause?.inheritedTypes } - var genericWhereClause: GenericWhereClauseSyntax? { + public var genericWhereClause: GenericWhereClauseSyntax? { self.syntax.asProtocol(WithGenericParametersSyntax.self)?.genericWhereClause } /// Returns true if this type conforms to `Sendable` and therefore is "threadsafe". - private(set) lazy var isSendable: Bool = { + public private(set) lazy var isSendable: Bool = { // Check if Sendable is in the inheritance list guard let inheritanceClause = self.syntax.inheritanceClause else { return false @@ -152,7 +152,7 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } /// Structured qualified type name built from the parent chain - package var qualifiedTypeName: SwiftQualifiedTypeName { + public var qualifiedTypeName: SwiftQualifiedTypeName { if let parent = self.parent { return SwiftQualifiedTypeName(parent.qualifiedTypeName.components + [name]) } else { @@ -160,17 +160,17 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } } - package var qualifiedName: String { + public var qualifiedName: String { qualifiedTypeName.fullName } /// Like `qualifiedName` but with dots replaced by underscores, suitable for /// use in C symbol names and Java identifiers - package var flatName: String { + public var flatName: String { qualifiedTypeName.fullFlatName } - var isReferenceType: Bool { + public var isReferenceType: Bool { switch kind { case .actor, .class: return true @@ -179,13 +179,13 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } } - var isGeneric: Bool { + public var isGeneric: Bool { !genericParameters.isEmpty } } extension SwiftNominalTypeDeclaration: CustomStringConvertible { - package var description: String { + public var description: String { if isGeneric { "\(qualifiedName)<\(genericParameters.map(\.name).joined(separator: ", "))>" } else { @@ -194,10 +194,10 @@ extension SwiftNominalTypeDeclaration: CustomStringConvertible { } } -package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { - let syntax: GenericParameterSyntax +public class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { + public let syntax: GenericParameterSyntax - init( + public init( sourceFilePath: String, moduleName: String, node: GenericParameterSyntax @@ -206,11 +206,11 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: node.name.text) } - var hasEach: Bool { + public var hasEach: Bool { syntax.specifier?.tokenKind == .keyword(.each) } - var packReferenceName: String { + public var packReferenceName: String { if hasEach { "each \(name)" } else { @@ -218,7 +218,7 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { } } - var packExpansionName: String { + public var packExpansionName: String { if hasEach { "repeat each \(name)" } else { @@ -232,10 +232,10 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { /// A typealias used as a specialization of a generic type will be emitted as /// a new concrete type in the Java. This way we can specialize `FishBox` from /// `Box` by doing `typealias FishBox = Box`. -package final class SwiftTypeAliasDeclaration: SwiftTypeDeclaration { - let syntax: TypeAliasDeclSyntax +public final class SwiftTypeAliasDeclaration: SwiftTypeDeclaration { + public let syntax: TypeAliasDeclSyntax - init( + public init( sourceFilePath: String, moduleName: String, node: TypeAliasDeclSyntax @@ -246,13 +246,13 @@ package final class SwiftTypeAliasDeclaration: SwiftTypeDeclaration { } extension SwiftTypeDeclaration: Equatable { - package static func == (lhs: SwiftTypeDeclaration, rhs: SwiftTypeDeclaration) -> Bool { + public static func == (lhs: SwiftTypeDeclaration, rhs: SwiftTypeDeclaration) -> Bool { lhs === rhs } } extension SwiftTypeDeclaration: Hashable { - package func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift similarity index 59% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift index 56fe3cb9c..71db093df 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift @@ -14,22 +14,62 @@ import SwiftSyntax -struct SwiftParameter: Equatable { - var convention: SwiftParameterConvention - var argumentLabel: String? - var parameterName: String? - var type: SwiftType +public struct SwiftParameter: Equatable { + public var convention: SwiftParameterConvention + public var argumentLabel: String? + public var parameterName: String? + public var type: SwiftType + + /// Whether this is a variadic parameter (`T...`). + public var isVariadic: Bool + /// Whether the parameter declares a default value. + public var hasDefaultValue: Bool + /// The default-value expression source, if any (e.g. `42`, `[]`). + public var defaultValueExpression: String? + + public init( + convention: SwiftParameterConvention, + argumentLabel: String? = nil, + parameterName: String? = nil, + type: SwiftType, + isVariadic: Bool = false, + hasDefaultValue: Bool = false, + defaultValueExpression: String? = nil + ) { + self.convention = convention + self.argumentLabel = argumentLabel + self.parameterName = parameterName + self.type = type + self.isVariadic = isVariadic + self.hasDefaultValue = hasDefaultValue + self.defaultValueExpression = defaultValueExpression + } + + /// The simple parameter name, falling back to the argument label. + public var name: String { + parameterName ?? argumentLabel ?? "_" + } + + /// The external argument label, falling back to the parameter name. + public var effectiveLabel: String { + argumentLabel ?? name + } + + /// Whether this parameter is passed `inout`. + public var isInout: Bool { + convention == .inout + } } extension SwiftParameter: CustomStringConvertible { - var description: String { + public var description: String { let argumentLabel = self.argumentLabel ?? "_" let parameterName = self.parameterName ?? "_" return "\(argumentLabel) \(parameterName): \(descriptionInType)" } - var descriptionInType: String { + public var descriptionInType: String { let conventionString: String switch convention { case .byValue: @@ -47,7 +87,7 @@ extension SwiftParameter: CustomStringConvertible { } /// Describes how a parameter is passed. -enum SwiftParameterConvention: Equatable { +public enum SwiftParameterConvention: Equatable { /// The parameter is passed by-value or borrowed. case byValue /// The parameter is passed by-value but consumed. @@ -57,17 +97,20 @@ enum SwiftParameterConvention: Equatable { } extension SwiftParameter { - init(_ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { + public init(_ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { self.convention = .byValue self.type = try SwiftType(node.type, lookupContext: lookupContext) self.argumentLabel = nil self.parameterName = node.firstName?.identifier?.name self.argumentLabel = node.firstName?.identifier?.name + self.isVariadic = false + self.hasDefaultValue = node.defaultValue != nil + self.defaultValueExpression = node.defaultValue?.value.trimmedDescription } } extension SwiftParameter { - init(_ node: FunctionParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { + public init(_ node: FunctionParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { // Determine the convention. The default is by-value, but there are // specifiers on the type for other conventions (like `inout`). var type = node.type @@ -101,6 +144,11 @@ extension SwiftParameter { // Determine the type. self.type = try SwiftType(type, lookupContext: lookupContext) + // Variadic / default-value information. + self.isVariadic = node.ellipsis != nil + self.hasDefaultValue = node.defaultValue != nil + self.defaultValueExpression = node.defaultValue?.value.trimmedDescription + // FIXME: swift-syntax itself should have these utilities based on identifiers. if let secondName = node.secondName { self.argumentLabel = node.firstName.identifier?.name diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift b/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift similarity index 90% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift index 8691cdfa1..560ed9c0c 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift @@ -12,30 +12,31 @@ // //===----------------------------------------------------------------------===// +import Logging import SwiftIfConfig import SwiftSyntax -struct SwiftParsedModuleSymbolTableBuilder { - let log: Logger? +package struct SwiftParsedModuleSymbolTableBuilder { + package let log: Logger? /// The symbol table being built. - var symbolTable: SwiftModuleSymbolTable + package var symbolTable: SwiftModuleSymbolTable /// Imported modules to resolve type syntax. - let importedModules: [String: SwiftModuleSymbolTable] + package let importedModules: [String: SwiftModuleSymbolTable] /// The build configuration used to resolve #if conditional compilation blocks. - let buildConfig: any BuildConfiguration + package let buildConfig: any BuildConfiguration /// Extension decls their extended type hasn't been resolved. - var unresolvedExtensions: [ExtensionDeclSyntax] + package var unresolvedExtensions: [ExtensionDeclSyntax] - init( + package init( moduleName: String, requiredAvailablityOfModuleWithName: String? = nil, alternativeModules: SwiftModuleSymbolTable.AlternativeModuleNamesData? = nil, importedModules: [String: SwiftModuleSymbolTable], - buildConfig: any BuildConfiguration = .jextractDefault, + buildConfig: any BuildConfiguration = .swiftExtractDefault, log: Logger? = nil ) { self.log = log @@ -49,14 +50,14 @@ struct SwiftParsedModuleSymbolTableBuilder { self.unresolvedExtensions = [] } - var moduleName: String { + package var moduleName: String { symbolTable.moduleName } } extension SwiftParsedModuleSymbolTableBuilder { - mutating func handle( + package mutating func handle( sourceFile: SourceFileSyntax, sourceFilePath: String ) { @@ -66,7 +67,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( codeBlockItem node: CodeBlockItemSyntax.Item, sourceFilePath: String ) { @@ -88,7 +89,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( typeAliasDecl node: TypeAliasDeclSyntax, sourceFilePath: String ) { @@ -108,7 +109,7 @@ extension SwiftParsedModuleSymbolTableBuilder { /// Add a nominal type declaration and all of the nested types within it to the symbol /// table. - mutating func handle( + package mutating func handle( sourceFilePath: String, nominalTypeDecl node: NominalTypeDeclSyntaxNode, parent: SwiftNominalTypeDeclaration? @@ -118,7 +119,7 @@ extension SwiftParsedModuleSymbolTableBuilder { if symbolTable.lookupType(node.name.text, parent: parent) != nil || symbolTable.lookupTypealias(node.name.text, parent: parent) != nil { - log?.debug("Failed to add a decl into symbol table: redeclaration; " + node.nameForDebug) + log?.debug("Failed to add a decl into symbol table: redeclaration; \(node.nameForDebug)") return } @@ -142,7 +143,7 @@ extension SwiftParsedModuleSymbolTableBuilder { self.handle(sourceFilePath: sourceFilePath, memberBlock: node.memberBlock, parent: nominalTypeDecl) } - mutating func handle( + package mutating func handle( sourceFilePath: String, typeAliasDecl node: TypeAliasDeclSyntax, parent: SwiftNominalTypeDeclaration? @@ -150,7 +151,7 @@ extension SwiftParsedModuleSymbolTableBuilder { if symbolTable.lookupType(node.name.text, parent: parent) != nil || symbolTable.lookupTypealias(node.name.text, parent: parent) != nil { - log?.debug("Failed to add a decl into symbol table: redeclaration; " + node.nameForDebug) + log?.debug("Failed to add a decl into symbol table: redeclaration; \(node.nameForDebug)") return } @@ -167,7 +168,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( sourceFilePath: String, memberBlock node: MemberBlockSyntax, parent: SwiftNominalTypeDeclaration @@ -182,7 +183,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( extensionDecl node: ExtensionDeclSyntax, sourceFilePath: String ) { @@ -193,7 +194,7 @@ extension SwiftParsedModuleSymbolTableBuilder { /// Add any nested types within the given extension to the symbol table. /// If the extended nominal type can't be resolved, returns false. - mutating func tryHandle( + package mutating func tryHandle( extension node: ExtensionDeclSyntax, sourceFilePath: String ) -> Bool { @@ -217,7 +218,7 @@ extension SwiftParsedModuleSymbolTableBuilder { return true } - mutating func handle( + package mutating func handle( ifConfig node: IfConfigDeclSyntax, sourceFilePath: String ) { @@ -235,7 +236,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } /// Finalize the symbol table and return it. - mutating func finalize() -> SwiftModuleSymbolTable { + package mutating func finalize() -> SwiftModuleSymbolTable { // Handle the unresolved extensions. // The work queue is required because, the extending type might be declared // in another extension that hasn't been processed. E.g.: @@ -264,7 +265,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } extension DeclSyntaxProtocol { - var asNominal: NominalTypeDeclSyntaxNode? { + package var asNominal: NominalTypeDeclSyntaxNode? { switch DeclSyntax(self).as(DeclSyntaxEnum.self) { case .actorDecl(let actorDecl): actorDecl case .classDecl(let classDecl): classDecl diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftQualifiedTypeName.swift b/Sources/SwiftExtract/SwiftTypes/SwiftQualifiedTypeName.swift similarity index 71% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftQualifiedTypeName.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftQualifiedTypeName.swift index 2c170ddb7..71cf956ca 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftQualifiedTypeName.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftQualifiedTypeName.swift @@ -18,33 +18,33 @@ /// - **qualifiedName** (`Logger.Message`) - for Swift source /// - **flatName** (`Logger_Message`) - for C symbols / `@_cdecl` and Java identifiers /// - **leafName** (`Message`) - innermost component only -package struct SwiftQualifiedTypeName: Hashable, Sendable, CustomStringConvertible { +public struct SwiftQualifiedTypeName: Hashable, Sendable, CustomStringConvertible { /// Name components from outermost to innermost, e.g. ["Logger", "Message"] - let components: [String] + public let components: [String] - init(_ components: [String]) { + public init(_ components: [String]) { precondition(!components.isEmpty) self.components = components } - init(_ leafName: String) { + public init(_ leafName: String) { self.components = [leafName] } /// Leaf name (innermost), e.g. "Message" - var leafName: String { components.last! } + public var leafName: String { components.last! } /// Dot-separated for Swift source, e.g. "Logger.Message" - var fullName: String { components.joined(separator: ".") } + public var fullName: String { components.joined(separator: ".") } /// Underscore-separated for C symbols and Java identifiers, e.g. "Logger_Message" - var fullFlatName: String { components.joined(separator: "_") } + public var fullFlatName: String { components.joined(separator: "_") } /// Dollar-separated for JNI C symbol parent names, e.g. "Logger$Message" - var jniEscapedName: String { components.joined(separator: "$") } + public var jniEscapedName: String { components.joined(separator: "$") } /// CustomStringConvertible - uses fullName - package var description: String { fullName } + public var description: String { fullName } - var isNested: Bool { components.count > 1 } + public var isNested: Bool { components.count > 1 } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftResult.swift b/Sources/SwiftExtract/SwiftTypes/SwiftResult.swift similarity index 64% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftResult.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftResult.swift index b7aa7b568..87fb57ea6 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftResult.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftResult.swift @@ -14,18 +14,23 @@ import SwiftSyntax -struct SwiftResult: Equatable { - var convention: SwiftResultConvention // currently not used. - var type: SwiftType +public struct SwiftResult: Equatable { + public var convention: SwiftResultConvention // currently not used. + public var type: SwiftType + + public init(convention: SwiftResultConvention, type: SwiftType) { + self.convention = convention + self.type = type + } } -enum SwiftResultConvention: Equatable { +public enum SwiftResultConvention: Equatable { case direct case indirect } extension SwiftResult { - static var void: Self { + public static var void: Self { Self(convention: .direct, type: .void) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift similarity index 65% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift index a4c4de73b..45557b7ac 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift @@ -12,13 +12,12 @@ // //===----------------------------------------------------------------------===// -import CodePrinting +import Logging import SwiftIfConfig -import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -package protocol SwiftSymbolTableProtocol { +public protocol SwiftSymbolTableProtocol { /// The module name that this symbol table describes. var moduleName: String { get } @@ -38,7 +37,7 @@ package protocol SwiftSymbolTableProtocol { extension SwiftSymbolTableProtocol { /// Look for a type - package func lookupType(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftNominalTypeDeclaration? { + public func lookupType(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftNominalTypeDeclaration? { if let parent { return lookupNestedType(name, parent: parent) } @@ -46,7 +45,7 @@ extension SwiftSymbolTableProtocol { return lookupTopLevelNominalType(name) } - package func lookupTypealias(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftTypeAliasDeclaration? { + public func lookupTypealias(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftTypeAliasDeclaration? { if let parent { return lookupNestedTypealias(name, parent: parent) } @@ -55,9 +54,14 @@ extension SwiftSymbolTableProtocol { } } -package class SwiftSymbolTable { - let importedModules: [String: SwiftModuleSymbolTable] - let parsedModule: SwiftModuleSymbolTable +public class SwiftSymbolTable { + public let importedModules: [String: SwiftModuleSymbolTable] + public let parsedModule: SwiftModuleSymbolTable + + /// Module names within `importedModules` that are synthetic — they exist + /// purely to drive type resolution and must NOT be emitted as + /// `import ` statements in generated Swift code. + public let syntheticImportedModuleNames: Set private var knownTypeToNominal: [SwiftKnownTypeDeclKind: SwiftNominalTypeDeclaration] = [:] private var prioritySortedImportedModules: [SwiftModuleSymbolTable] { @@ -72,12 +76,17 @@ package class SwiftSymbolTable { }) } - init(parsedModule: SwiftModuleSymbolTable, importedModules: [String: SwiftModuleSymbolTable]) { + public init( + parsedModule: SwiftModuleSymbolTable, + importedModules: [String: SwiftModuleSymbolTable], + syntheticImportedModuleNames: Set = [] + ) { self.parsedModule = parsedModule self.importedModules = importedModules + self.syntheticImportedModuleNames = syntheticImportedModuleNames } - func isModuleName(_ name: String) -> Bool { + public func isModuleName(_ name: String) -> Bool { if name == moduleName { return true } @@ -88,16 +97,17 @@ package class SwiftSymbolTable { extension SwiftSymbolTable { package static func setup( moduleName: String, - _ inputFiles: some Collection, - config: Configuration?, + _ inputFiles: some Collection, + additionalInputFiles: [SwiftInputFile] = [], + config: (any SwiftExtractConfiguration)?, sourceDependencies: SourceDependencies, - buildConfig: any BuildConfiguration = .jextractDefault, - log: Logger, + buildConfig: any BuildConfiguration = .swiftExtractDefault, + log: Logger? = nil, ) -> SwiftSymbolTable { // Prepare imported modules. // FIXME: Support arbitrary dependencies. - var modules: Set = [] + var modules: Set = [] for inputFile in inputFiles { let importedModules = importingModules(sourceFile: inputFile.syntax) modules.formUnion(importedModules) @@ -122,7 +132,10 @@ extension SwiftSymbolTable { guard importedModules[dependencyModuleName] == nil else { continue } - let dependencyInputs = sourceDependencies.swiftModuleInputs[dependencyModuleName] ?? [] + let dependencyInputs = + sourceDependencies.swiftModuleInputs[dependencyModuleName] + ?? sourceDependencies.syntheticStubInputs[dependencyModuleName] + ?? [] // TODO: build a `dependencyImportedModules` dict by scanning the dep's // own source files with `importingModules(sourceFile:)`, instead of // reusing the primary's `importedModules`. The current set is too broad @@ -138,9 +151,8 @@ extension SwiftSymbolTable { } let dependencyModule = dependencyModuleBuilder.finalize() importedModules[dependencyModuleName] = dependencyModule - log.info( - "Loaded dependency module '\(dependencyModuleName)' from \(dependencyInputs.count) source(s); " - + "top-level types [\(dependencyModule.topLevelTypes.count)]: \(dependencyModule.topLevelTypes.keys.sorted())" + log?.info( + "Loaded dependency module '\(dependencyModuleName)' from \(dependencyInputs.count) source(s); top-level types [\(dependencyModule.topLevelTypes.count)]: \(dependencyModule.topLevelTypes.keys.sorted())" ) } @@ -160,13 +172,13 @@ extension SwiftSymbolTable { stubBuilder.handle(sourceFile: sourceFile, sourceFilePath: "\(stubModuleName)_stub.swift") let stubModule = stubBuilder.finalize() importedModules[stubModuleName] = stubModule - log.info("Loaded module stub for '\(stubModuleName)' with \(declarations.count) declaration(s), top-level types: \(stubModule.topLevelTypes.keys.sorted())") + log?.info("Loaded module stub for '\(stubModuleName)' with \(declarations.count) declaration(s), top-level types: \(stubModule.topLevelTypes.keys.sorted())") } else { - log.info("Module '\(stubModuleName)' already known, skipping stub") + log?.info("Module '\(stubModuleName)' already known, skipping stub") } } } else { - log.debug("No importedModuleStubs in config") + log?.debug("No importedModuleStubs in config") } // FIXME: Support granular lookup context (file, type context). @@ -181,20 +193,24 @@ extension SwiftSymbolTable { for sourceFile in inputFiles { builder.handle(sourceFile: sourceFile.syntax, sourceFilePath: sourceFile.path) } - if let stubs = sourceDependencies.syntheticJavaWrappersSwiftSource { - builder.handle(sourceFile: stubs.syntax, sourceFilePath: stubs.path) + for sourceFile in additionalInputFiles { + builder.handle(sourceFile: sourceFile.syntax, sourceFilePath: sourceFile.path) } let parsedModule = builder.finalize() - return SwiftSymbolTable(parsedModule: parsedModule, importedModules: importedModules) + return SwiftSymbolTable( + parsedModule: parsedModule, + importedModules: importedModules, + syntheticImportedModuleNames: sourceDependencies.syntheticModuleNames, + ) } } extension SwiftSymbolTable: SwiftSymbolTableProtocol { - package var moduleName: String { parsedModule.moduleName } + public var moduleName: String { parsedModule.moduleName } /// Look for a top-level nominal type with the given name. This should only /// return nominal types within this module. - package func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { + public func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { if let parsedResult = parsedModule.lookupTopLevelNominalType(name) { return parsedResult } @@ -209,7 +225,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } /// Look for a top-level nominal type in a specific module by name - package func lookupTopLevelNominalType(_ name: String, inModule moduleName: String) -> SwiftNominalTypeDeclaration? { + public func lookupTopLevelNominalType(_ name: String, inModule moduleName: String) -> SwiftNominalTypeDeclaration? { if moduleName == self.moduleName { return parsedModule.lookupTopLevelNominalType(name) } @@ -217,7 +233,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } /// Look for a top-level typealias with the given name. - package func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { + public func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { if let parsedResult = parsedModule.lookupTopLevelTypealias(name) { return parsedResult } @@ -232,7 +248,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } // Look for a nested type with the given name. - package func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { + public func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { if let parsedResult = parsedModule.lookupNestedType(name, parent: parent) { return parsedResult } @@ -247,7 +263,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } // Look for a nested typealias with the given name. - package func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { + public func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { if let parsedResult = parsedModule.lookupNestedTypealias(name, parent: parent) { return parsedResult } @@ -264,7 +280,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { extension SwiftSymbolTable { /// Map 'SwiftKnownTypeDeclKind' to the declaration. - subscript(knownType: SwiftKnownTypeDeclKind) -> SwiftNominalTypeDeclaration! { + public subscript(knownType: SwiftKnownTypeDeclKind) -> SwiftNominalTypeDeclaration! { if let known = knownTypeToNominal[knownType] { return known } @@ -279,63 +295,3 @@ extension SwiftSymbolTable { return found } } - -extension SwiftSymbolTable { - func printImportedModules(_ printer: inout CodePrinter) { - let mainSymbolSourceModules = Set( - self.importedModules.values.filter { $0.alternativeModules?.isMainSourceOfSymbols ?? false }.map(\.moduleName) - ) - - for module in self.importedModules.keys.sorted() { - guard module != "Swift" else { - continue - } - - guard let alternativeModules = self.importedModules[module]?.alternativeModules else { - printer.print("import \(module)") - continue - } - - // Only the main source of symbols emits the conditional import block. - // Secondary modules (e.g. FoundationEssentials when Foundation is the main source) - // are skipped when their main source is already present, because the main source's - // block already covers the import. If no main source is present, fall back to a - // plain import so the module is still imported. - guard alternativeModules.isMainSourceOfSymbols else { - if mainSymbolSourceModules.isDisjoint(with: alternativeModules.moduleNames) { - printer.print("import \(module)") - } - continue - } - - var importGroups: [String: [String]] = [:] - for name in alternativeModules.moduleNames { - guard let otherModule = self.importedModules[name] else { continue } - - let groupKey = otherModule.requiredAvailablityOfModuleWithName ?? otherModule.moduleName - importGroups[groupKey, default: []].append(otherModule.moduleName) - } - - for (index, group) in importGroups.keys.sorted().enumerated() { - if index > 0 && importGroups.keys.count > 1 { - printer.print("#elseif canImport(\(group))") - } else { - printer.print("#if canImport(\(group))") - } - - for groupModule in importGroups[group] ?? [] { - printer.print("import \(groupModule)") - } - } - - if importGroups.keys.isEmpty { - printer.print("import \(module)") - } else { - printer.print("#else") - printer.print("import \(module)") - printer.print("#endif") - } - } - printer.println() - } -} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType+GenericTypes.swift similarity index 97% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftType+GenericTypes.swift index f6a8c5871..7949bf489 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType+GenericTypes.swift @@ -15,7 +15,7 @@ extension SwiftType { /// Returns a concrete type if this is a generic parameter in the list and it /// conforms to a protocol with representative concrete type. - func representativeConcreteTypeIn( + package func representativeConcreteTypeIn( knownTypes: SwiftKnownTypes, genericParameters: [SwiftGenericParameterDeclaration], genericRequirements: [SwiftGenericRequirement] @@ -29,7 +29,7 @@ extension SwiftType { } /// Returns the protocol type if this is a generic parameter in the list - func typeIn( + package func typeIn( genericParameters: [SwiftGenericParameterDeclaration], genericRequirements: [SwiftGenericRequirement] ) -> SwiftType? { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift similarity index 91% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 49bea52d7..d1b17a20e 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -15,11 +15,16 @@ import SwiftSyntax /// An element of a Swift tuple type, preserving the optional label. -struct SwiftTupleElement: Equatable, CustomStringConvertible { - var label: String? - var type: SwiftType +public struct SwiftTupleElement: Equatable, CustomStringConvertible { + public var label: String? + public var type: SwiftType - var description: String { + public init(label: String? = nil, type: SwiftType) { + self.label = label + self.type = type + } + + public var description: String { if let label { return "\(label): \(type)" } @@ -28,7 +33,7 @@ struct SwiftTupleElement: Equatable, CustomStringConvertible { } /// Describes a type in the Swift type system. -enum SwiftType: Equatable { +public enum SwiftType: Equatable { case nominal(SwiftNominalType) case genericParameter(SwiftGenericParameterDeclaration) @@ -50,11 +55,11 @@ enum SwiftType: Equatable { /// `type1` & `type2` indirect case composite([SwiftType]) - static var void: Self { + public static var void: Self { .tuple([]) } - var asNominalType: SwiftNominalType? { + public var asNominalType: SwiftNominalType? { switch self { case .nominal(let nominal): nominal case .tuple(let elements): elements.count == 1 ? elements[0].type.asNominalType : nil @@ -62,12 +67,12 @@ enum SwiftType: Equatable { } } - var asNominalTypeDeclaration: SwiftNominalTypeDeclaration? { + public var asNominalTypeDeclaration: SwiftNominalTypeDeclaration? { asNominalType?.nominalTypeDecl } /// Whether this is the "Void" type, which is actually an empty tuple. - var isVoid: Bool { + public var isVoid: Bool { switch self { case .tuple([]): return true @@ -80,7 +85,7 @@ enum SwiftType: Equatable { } /// Whether this is a pointer type. I.e 'Unsafe[Mutable][Raw]Pointer' - var isPointer: Bool { + public var isPointer: Bool { switch self { case .nominal(let nominal): if let knownType = nominal.nominalTypeDecl.knownTypeKind { @@ -96,7 +101,7 @@ enum SwiftType: Equatable { /// /// * Mutations don't require 'inout' convention. /// * The value is a pointer of the instance data, - var isReferenceType: Bool { + public var isReferenceType: Bool { switch self { case .nominal(let nominal): return nominal.nominalTypeDecl.isReferenceType @@ -107,7 +112,7 @@ enum SwiftType: Equatable { } } - var isUnsignedInteger: Bool { + public var isUnsignedInteger: Bool { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { @@ -118,7 +123,7 @@ enum SwiftType: Equatable { } } - var isArchDependingInteger: Bool { + public var isArchDependingInteger: Bool { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { @@ -129,7 +134,7 @@ enum SwiftType: Equatable { } } - var isRawTypeCompatible: Bool { + public var isRawTypeCompatible: Bool { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { @@ -153,7 +158,7 @@ extension SwiftType: CustomStringConvertible { } } - var description: String { + public var description: String { switch self { case .nominal(let nominal): return nominal.description case .genericParameter(let genericParam): return genericParam.packExpansionName @@ -186,23 +191,23 @@ extension SwiftType: CustomStringConvertible { } } -struct SwiftNominalType: Equatable { - indirect enum Parent: Equatable { +public struct SwiftNominalType: Equatable { + public indirect enum Parent: Equatable { case nominal(SwiftNominalType) } - enum SugarName: Equatable { + public enum SugarName: Equatable { case optional case array case dictionary } private var storedParent: Parent? - var sugarName: SugarName? - var nominalTypeDecl: SwiftNominalTypeDeclaration - var genericArguments: [SwiftType] + public var sugarName: SugarName? + public var nominalTypeDecl: SwiftNominalTypeDeclaration + public var genericArguments: [SwiftType] - init( + public init( parent: SwiftNominalType? = nil, sugarName: SugarName? = nil, nominalTypeDecl: SwiftNominalTypeDeclaration, @@ -215,7 +220,7 @@ struct SwiftNominalType: Equatable { self.genericArguments = genericArguments } - var parent: SwiftNominalType? { + public var parent: SwiftNominalType? { if case .nominal(let parent) = storedParent ?? .none { return parent } @@ -223,13 +228,13 @@ struct SwiftNominalType: Equatable { return nil } - package var asKnownType: SwiftKnownType? { + public var asKnownType: SwiftKnownType? { nominalTypeDecl.knownTypeKind.flatMap { SwiftKnownType(kind: $0, genericArguments: genericArguments) } } - var hasGenericParameter: Bool { + public var hasGenericParameter: Bool { genericArguments.contains { if case .genericParameter = $0 { return true @@ -240,7 +245,7 @@ struct SwiftNominalType: Equatable { } extension SwiftNominalType: CustomStringConvertible { - var description: String { + public var description: String { var resultString: String if let parent { resultString = parent.description + "." @@ -267,7 +272,7 @@ extension SwiftNominalType: CustomStringConvertible { } extension SwiftNominalType.Parent: CustomStringConvertible { - var description: String { + public var description: String { switch self { case .nominal(let nominal): return nominal.description @@ -276,17 +281,13 @@ extension SwiftNominalType.Parent: CustomStringConvertible { } extension SwiftNominalType { - var isSwiftJavaWrapper: Bool { - nominalTypeDecl.syntax.attributes.contains(where: \.isSwiftJavaMacro) - } - - var isProtocol: Bool { + public var isProtocol: Bool { nominalTypeDecl.kind == .protocol } } extension SwiftType { - init(_ type: TypeSyntax, lookupContext: SwiftTypeLookupContext) throws { + public init(_ type: TypeSyntax, lookupContext: SwiftTypeLookupContext) throws { var knownTypes: SwiftKnownTypes { SwiftKnownTypes(symbolTable: lookupContext.symbolTable) } @@ -438,7 +439,7 @@ extension SwiftType { } } - init( + public init( originalType: TypeSyntax, parent: SwiftType?, name: TokenSyntax, @@ -505,7 +506,7 @@ extension SwiftType { } } - init?( + public init?( nominalDecl: NamedDeclSyntax & DeclGroupSyntax, parent: SwiftType?, symbolTable: SwiftSymbolTable @@ -532,7 +533,7 @@ extension SwiftType { /// /// This is used e.g. by typealiases like `typealias Ano = Array`, /// so usages like `Ano` become `Array`. - func substituting(genericParameters substitutions: [String: SwiftType]) -> SwiftType { + public func substituting(genericParameters substitutions: [String: SwiftType]) -> SwiftType { guard !substitutions.isEmpty else { return self } switch self { @@ -579,7 +580,7 @@ extension SwiftType { /// Produce an expression that creates the metatype for this type in /// Swift source code. - var metatypeReferenceExprSyntax: ExprSyntax { + public var metatypeReferenceExprSyntax: ExprSyntax { let type: ExprSyntax = "\(raw: description)" if postfixRequiresParentheses { return "(\(type)).self" @@ -588,7 +589,7 @@ extension SwiftType { } } -enum TypeTranslationError: Error { +public enum TypeTranslationError: Error { /// We haven't yet implemented support for this type. case unimplementedType(TypeSyntax, file: StaticString = #file, line: Int = #line) @@ -600,7 +601,7 @@ enum TypeTranslationError: Error { } extension SwiftNominalTypeDeclaration { - var asSwiftNominalType: SwiftNominalType { + public var asSwiftNominalType: SwiftNominalType { let genericArguments = genericParameters.map { SwiftType.genericParameter($0) } return SwiftNominalType( parent: parent?.asSwiftNominalType, @@ -609,7 +610,7 @@ extension SwiftNominalTypeDeclaration { ) } - var asSwiftType: SwiftType { + public var asSwiftType: SwiftType { .nominal(asSwiftNominalType) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift similarity index 92% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift index 664fb7463..bee0ba061 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift @@ -19,8 +19,8 @@ import SwiftSyntax /// All type lookups should be done via this instance. This caches the /// association of `Syntax.ID` to `SwiftTypeDeclaration`, and guarantees that /// there's only one `SwiftTypeDeclaration` per declaration `Syntax`. -class SwiftTypeLookupContext { - var symbolTable: SwiftSymbolTable +public class SwiftTypeLookupContext { + public var symbolTable: SwiftSymbolTable private var typeDecls: [Syntax.ID: SwiftTypeDeclaration] = [:] @@ -28,7 +28,7 @@ class SwiftTypeLookupContext { /// cycles like `typealias A = B; typealias B = A`. private var resolvingAliases: Set = [] - init(symbolTable: SwiftSymbolTable) { + public init(symbolTable: SwiftSymbolTable) { self.symbolTable = symbolTable } @@ -37,7 +37,7 @@ class SwiftTypeLookupContext { /// - Parameters: /// - name: name to lookup /// - moduleName: the module to look in - func moduleQualifiedLookup(name: String, in moduleName: String) -> SwiftTypeDeclaration? { + public func moduleQualifiedLookup(name: String, in moduleName: String) -> SwiftTypeDeclaration? { symbolTable.lookupTopLevelNominalType(name, inModule: moduleName) } @@ -46,7 +46,7 @@ class SwiftTypeLookupContext { /// - Parameters: /// - name: name to lookup /// - node: `Syntax` node the lookup happened - func unqualifiedLookup(name: Identifier, from node: some SyntaxProtocol) throws -> SwiftTypeDeclaration? { + public func unqualifiedLookup(name: Identifier, from node: some SyntaxProtocol) throws -> SwiftTypeDeclaration? { for result in node.lookup(name) { switch result { @@ -110,7 +110,7 @@ class SwiftTypeLookupContext { /// Returns the type declaration object associated with the `Syntax` node. /// If there's no declaration created, create an instance on demand, and cache it. - func typeDeclaration(for node: some SyntaxProtocol, sourceFilePath: String) throws -> SwiftTypeDeclaration? { + public func typeDeclaration(for node: some SyntaxProtocol, sourceFilePath: String) throws -> SwiftTypeDeclaration? { if let found = typeDecls[node.id] { return found } @@ -207,7 +207,7 @@ class SwiftTypeLookupContext { } /// Resolve a typealias to the `SwiftType` of its right-hand side. - func resolve(typeAlias decl: SwiftTypeAliasDeclaration) throws -> SwiftType { + public func resolve(typeAlias decl: SwiftTypeAliasDeclaration) throws -> SwiftType { let id = decl.syntax.id guard !resolvingAliases.contains(id) else { throw TypeTranslationError.unimplementedType(TypeSyntax(decl.syntax.initializer.value)) @@ -218,6 +218,6 @@ class SwiftTypeLookupContext { } } -enum TypeLookupError: Error { +public enum TypeLookupError: Error { case notType(Syntax) } diff --git a/Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift b/Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift new file mode 100644 index 000000000..89d4325b3 --- /dev/null +++ b/Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 +// +//===----------------------------------------------------------------------===// + +/// Minimum access level a declaration must have to be considered for extraction. +/// +/// Lives in the small `SwiftExtractConfigurationShared` target so the analysis +/// layer and language-specific configuration layers (e.g. swift-java's +/// `SwiftJavaConfigurationShared`) can both depend on it without dragging +/// SwiftSyntax into the latter. +#if compiler(>=6.2) +@nonexhaustive +#endif +public enum AccessLevelMode: String, Codable, Sendable { + case `public` + case `package` + case `internal` +} + +extension AccessLevelMode { + public static var `default`: Self { + .public + } +} diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 1562b4bf6..61af95a33 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -14,6 +14,13 @@ import Foundation +// In a real module build this resolves to a separate target. In plugin builds +// the file is inlined (via symlink) alongside `AccessLevelMode.swift`, so the +// module isn't a discoverable import — guard with canImport. +#if canImport(SwiftExtractConfigurationShared) +@_exported import SwiftExtractConfigurationShared +#endif + //////////////////////////////////////////////////////////////////////////////// // This file is only supposed to be edited in `Shared/` and must be symlinked // // from everywhere else! We cannot share dependencies with or between plugins // @@ -62,8 +69,8 @@ public struct Configuration: Codable { writeEmptyFiles ?? false } - public var minimumInputAccessLevelMode: JExtractMinimumAccessLevelMode? - public var effectiveMinimumInputAccessLevelMode: JExtractMinimumAccessLevelMode { + public var minimumInputAccessLevelMode: AccessLevelMode? + public var effectiveMinimumInputAccessLevelMode: AccessLevelMode { minimumInputAccessLevelMode ?? .default } diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift deleted file mode 100644 index 22fead577..000000000 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift +++ /dev/null @@ -1,26 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 minimum access level which -public enum JExtractMinimumAccessLevelMode: String, Codable { - case `public` - case `package` - case `internal` -} - -extension JExtractMinimumAccessLevelMode { - public static var `default`: Self { - .public - } -} diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index f901eb731..020ae8202 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -66,7 +66,7 @@ extension SwiftJava { var writeEmptyFiles: Bool? @Option(help: "The lowest access level of Swift declarations that should be extracted, defaults to 'public'.") - var minimumInputAccessLevelMode: JExtractMinimumAccessLevelMode? + var minimumInputAccessLevelMode: AccessLevelMode? @Option( help: @@ -222,6 +222,6 @@ struct IllegalModeCombinationError: Error { } extension JExtractGenerationMode: ExpressibleByArgument {} -extension JExtractMinimumAccessLevelMode: ExpressibleByArgument {} +extension AccessLevelMode: ExpressibleByArgument {} extension JExtractMemoryManagementMode: ExpressibleByArgument {} extension JExtractAsyncFuncMode: ExpressibleByArgument {} diff --git a/Sources/SwiftJavaTool/CommonOptions.swift b/Sources/SwiftJavaTool/CommonOptions.swift index 8f5e7ccc1..552cfea74 100644 --- a/Sources/SwiftJavaTool/CommonOptions.swift +++ b/Sources/SwiftJavaTool/CommonOptions.swift @@ -18,6 +18,7 @@ import JExtractSwiftLib import JavaNet import JavaUtilJar import Logging +import SwiftExtract import SwiftJava import SwiftJavaConfigurationShared import SwiftJavaShared @@ -63,7 +64,7 @@ extension SwiftJava { var inputSwift: String? = nil @Option(name: .shortAndLong, help: "Configure the level of logs that should be printed") - var logLevel: JExtractSwiftLib.Logger.Level = .info + var logLevel: SwiftExtract.Logger.Level = .info @Option(help: "A path to a custom swift-java.config to use") var config: String? = nil diff --git a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift index 19e3761b9..95eaf7d0a 100644 --- a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -19,6 +19,7 @@ import JavaLangReflect import JavaNet import JavaUtilJar import Logging +import SwiftExtract import SwiftJava import SwiftJavaConfigurationShared import SwiftJavaShared @@ -30,7 +31,7 @@ protocol SwiftJavaBaseAsyncParsableCommand: AsyncParsableCommand { var log: Logging.Logger { get } - var logLevel: JExtractSwiftLib.Logger.Level { get set } + var logLevel: SwiftExtract.Logger.Level { get set } /// Must be implemented with an `@OptionGroup` in Command implementations var commonOptions: SwiftJava.CommonOptions { get set } @@ -103,7 +104,7 @@ extension SwiftJavaBaseAsyncParsableCommand { .init(label: "swift-java") } - var logLevel: JExtractSwiftLib.Logger.Level { + var logLevel: SwiftExtract.Logger.Level { get { self.commonOptions.logLevel } @@ -182,7 +183,7 @@ extension SwiftJavaBaseAsyncParsableCommand { config = Configuration() } // override configuration with options from command line - config.logLevel = command.logLevel + config.logLevel = LogLevel(command.logLevel) return config } } diff --git a/Sources/SwiftJavaToolLib/JavaTranslator.swift b/Sources/SwiftJavaToolLib/JavaTranslator.swift index 46682bae3..354d37cf5 100644 --- a/Sources/SwiftJavaToolLib/JavaTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator.swift @@ -60,7 +60,7 @@ package class JavaTranslator { /// The set of Swift modules that need to be imported to make the generated /// code compile. Use `getImportDecls()` to format this into a list of /// import declarations. - package var importedSwiftModules: Set = JavaTranslator.defaultImportedSwiftModules + package var importedSwiftModules: Set = JavaTranslator.defaultExtractedSwiftModules /// The canonical names of Java classes whose declared 'native' /// methods will be implemented in Swift. @@ -97,7 +97,7 @@ package class JavaTranslator { /// Clear out any per-file state when we want to start a new file. package func startNewFile() { - importedSwiftModules = Self.defaultImportedSwiftModules + importedSwiftModules = Self.defaultExtractedSwiftModules } /// Simplistic logging for all entities that couldn't be translated. @@ -111,7 +111,7 @@ extension JavaTranslator { private static let defaultFormat = BasicFormat(indentationWidth: .spaces(2)) /// Default set of modules that will always be imported. - private static let defaultImportedSwiftModules: Set = [ + private static let defaultExtractedSwiftModules: Set = [ "SwiftJava", "SwiftJavaJNICore", ] diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index 06d1d5553..adc338058 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// @_spi(Testing) import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import SwiftSyntax import Testing @@ -34,7 +35,7 @@ func assertLoweredFunction( ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) if let sourceFile { translator.add(filePath: "Fake.swift", text: sourceFile) @@ -120,7 +121,7 @@ func assertLoweredVariableAccessor( ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) if let sourceFile { translator.add(filePath: "Fake.swift", text: sourceFile) diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index a4632f8ba..baeefe26d 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -51,11 +52,11 @@ func assertOutput( ) throws { var config = config ?? Configuration() config.swiftModule = swiftModuleName - let translator = Swift2JavaTranslator(config: config) - translator.sourceDependencies.javaClasses = Array(javaClassLookupTable.keys) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + translator.sourceDependencies.addJavaWrapperStubs(Array(javaClassLookupTable.keys)) for (depModule, depSource) in dependencySwiftSources { let syntax = Parser.parse(source: depSource) - let input = SwiftJavaInputFile(syntax: syntax, path: "/fake/\(depModule).swift") + let input = SwiftInputFile(syntax: syntax, path: "/fake/\(depModule).swift") translator.sourceDependencies.swiftModuleInputs[depModule] = [input] } diff --git a/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift index 4ed587812..f8b17c893 100644 --- a/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift +++ b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -33,7 +34,7 @@ final class FFMNestedTypesTests { func test_nested_in_extension() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -46,6 +47,6 @@ final class FFMNestedTypesTests { javaOutputDirectory: "/fake" ) - #expect(st.importedTypes["MyNamespace.MyNestedStruct"] != nil, "Didn't import nested type!") + #expect(st.extractedTypes["MyNamespace.MyNestedStruct"] != nil, "Didn't import nested type!") } } diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index fd9203750..32be4c646 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -41,12 +42,12 @@ final class FuncCallbackImportTests { func func_callMeFunc_callback() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) - let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMe" }! + let funcDecl = st.extractedGlobalFuncs.first { $0.name == "callMe" }! let generator = FFMSwift2JavaGenerator( config: config, @@ -131,11 +132,11 @@ final class FuncCallbackImportTests { func func_callMeMoreFunc_callback() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) - let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMeMore" }! + let funcDecl = st.extractedGlobalFuncs.first { $0.name == "callMeMore" }! let generator = FFMSwift2JavaGenerator( config: config, @@ -246,12 +247,12 @@ final class FuncCallbackImportTests { func func_withBuffer_body() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) - let funcDecl = st.importedGlobalFuncs.first { $0.name == "withBuffer" }! + let funcDecl = st.extractedGlobalFuncs.first { $0.name == "withBuffer" }! let generator = FFMSwift2JavaGenerator( config: config, diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index c514a12fd..6cdacab12 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -237,12 +238,12 @@ extension FunctionDescriptorTests { ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = logLevel try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) - let funcDecl = st.importedGlobalFuncs.first { + let funcDecl = st.extractedGlobalFuncs.first { $0.name == methodIdentifier }! @@ -271,7 +272,7 @@ extension FunctionDescriptorTests { ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = logLevel try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) @@ -284,8 +285,8 @@ extension FunctionDescriptorTests { javaOutputDirectory: "/fake" ) - let accessorDecl: ImportedFunc? = - st.importedTypes.values.compactMap { + let accessorDecl: ExtractedFunc? = + st.extractedTypes.values.compactMap { $0.variables.first { $0.name == identifier && $0.apiKind == accessorKind } diff --git a/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift b/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift index b89e574f7..077db64ce 100644 --- a/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift +++ b/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -146,16 +147,16 @@ struct JExtractFileFilterTests { } // ==== ------------------------------------------------------------------- - // MARK: shouldJExtractFile tests + // MARK: shouldExtractSwiftFile tests @Test("No filters means everything passes") func noFilters() { var config = Configuration() - #expect(shouldJExtractFile(relativePath: "Anything.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Anything.swift", config: config)) config.swiftFilterInclude = [] config.swiftFilterExclude = [] - #expect(shouldJExtractFile(relativePath: "Anything.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Anything.swift", config: config)) } @Test("File include filter only") @@ -163,9 +164,9 @@ struct JExtractFileFilterTests { var config = Configuration() config.swiftFilterInclude = ["Models/**"] - #expect(shouldJExtractFile(relativePath: "Models/User.swift", config: config)) - #expect(shouldJExtractFile(relativePath: "Models/Sub/Deep.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Other/Thing.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/User.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/Sub/Deep.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Other/Thing.swift", config: config)) } @Test("File exclude filter only") @@ -173,8 +174,8 @@ struct JExtractFileFilterTests { var config = Configuration() config.swiftFilterExclude = ["Internal/*"] - #expect(shouldJExtractFile(relativePath: "Models/User.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Internal/Secret.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/User.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Internal/Secret.swift", config: config)) } @Test("File include and exclude combined") @@ -183,28 +184,28 @@ struct JExtractFileFilterTests { config.swiftFilterInclude = ["Models/**"] config.swiftFilterExclude = ["Models/Internal*"] - #expect(shouldJExtractFile(relativePath: "Models/User.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Models/InternalHelper.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Other/Thing.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/User.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Models/InternalHelper.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Other/Thing.swift", config: config)) } - @Test("Type-name patterns are ignored by shouldJExtractFile") + @Test("Type-name patterns are ignored by shouldExtractSwiftFile") func typeNamePatternsIgnoredByFileFilter() { var config = Configuration() config.swiftFilterInclude = ["Something.Other"] // Type-name-only includes should not restrict file-level filtering - #expect(shouldJExtractFile(relativePath: "Anything.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Anything.swift", config: config)) } // ==== ------------------------------------------------------------------- - // MARK: shouldJExtractType tests + // MARK: shouldExtractSwiftType tests @Test("No filters means all types pass") func noFiltersAllTypesPass() { let config = Configuration() - #expect(shouldJExtractType(qualifiedName: "Anything", config: config)) - #expect(shouldJExtractType(qualifiedName: "A.B.C", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Anything", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "A.B.C", config: config)) } @Test("Type include filter") @@ -212,8 +213,8 @@ struct JExtractFileFilterTests { var config = Configuration() config.swiftFilterInclude = ["Something.Other"] - #expect(shouldJExtractType(qualifiedName: "Something.Other", config: config)) - #expect(!shouldJExtractType(qualifiedName: "Something.Wrong", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Something.Other", config: config)) + #expect(!shouldExtractSwiftType(qualifiedName: "Something.Wrong", config: config)) } @Test("Type exclude filter") @@ -221,17 +222,17 @@ struct JExtractFileFilterTests { var config = Configuration() config.swiftFilterExclude = ["Something.Internal*"] - #expect(shouldJExtractType(qualifiedName: "Something.Other", config: config)) - #expect(!shouldJExtractType(qualifiedName: "Something.InternalHelper", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Something.Other", config: config)) + #expect(!shouldExtractSwiftType(qualifiedName: "Something.InternalHelper", config: config)) } - @Test("File-path patterns are ignored by shouldJExtractType") + @Test("File-path patterns are ignored by shouldExtractSwiftType") func filePathPatternsIgnoredByTypeFilter() { var config = Configuration() config.swiftFilterInclude = ["Models/**"] // File-path-only includes should not restrict type-level filtering - #expect(shouldJExtractType(qualifiedName: "Anything", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Anything", config: config)) } @Test("Plain pattern matches both file and type") @@ -240,12 +241,12 @@ struct JExtractFileFilterTests { config.swiftFilterInclude = ["MyType"] // Plain pattern works at file level (matched against filename segment) - #expect(shouldJExtractFile(relativePath: "MyType.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "OtherType.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "MyType.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "OtherType.swift", config: config)) // Plain pattern works at type level - #expect(shouldJExtractType(qualifiedName: "MyType", config: config)) - #expect(!shouldJExtractType(qualifiedName: "OtherType", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "MyType", config: config)) + #expect(!shouldExtractSwiftType(qualifiedName: "OtherType", config: config)) } @Test("Mixed file and type patterns in same config") @@ -254,12 +255,12 @@ struct JExtractFileFilterTests { config.swiftFilterInclude = ["Models/**", "Something.Other"] // File filter applies the file-path pattern - #expect(shouldJExtractFile(relativePath: "Models/User.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Other/Thing.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/User.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Other/Thing.swift", config: config)) // Type filter applies the type-name pattern - #expect(shouldJExtractType(qualifiedName: "Something.Other", config: config)) - #expect(!shouldJExtractType(qualifiedName: "Something.Wrong", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Something.Other", config: config)) + #expect(!shouldExtractSwiftType(qualifiedName: "Something.Wrong", config: config)) } // ==== ------------------------------------------------------------------- @@ -329,12 +330,12 @@ struct JExtractFileFilterTests { private func makeTranslator( include: [String]? = nil, exclude: [String]? = nil, - ) throws -> Swift2JavaTranslator { + ) throws -> SwiftAnalyzer { var config = Configuration() config.swiftModule = "__FakeModule" config.swiftFilterInclude = include config.swiftFilterExclude = exclude - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) translator.log.logLevel = .error try translator.analyze(path: "Fake.swift", text: Self.nestedTypeSource) return translator @@ -343,37 +344,37 @@ struct JExtractFileFilterTests { @Test("swiftFilterExclude with exact nested type name") func excludeExactNested() throws { let st = try makeTranslator(exclude: ["Tank.Internal"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("swiftFilterExclude with `Type.*` excludes direct children only") func excludeDirectChildren() throws { let st = try makeTranslator(exclude: ["Tank.*"]) - #expect(st.importedTypes["Tank"] != nil, "Top-level Tank itself should not be excluded by `Tank.*`") - #expect(st.importedTypes["Tank.Fish"] == nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil, "Top-level Tank itself should not be excluded by `Tank.*`") + #expect(st.extractedTypes["Tank.Fish"] == nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("swiftFilterExclude with suffix wildcard inside nested name") func excludeSuffixWildcard() throws { let st = try makeTranslator(exclude: ["Tank.Inter*"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("swiftFilterExclude with `**.Name` matches at any depth") func excludeRecursiveLeaf() throws { let st = try makeTranslator(exclude: ["**.Internal"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("plain-name swiftFilterExclude excludes top-level type and its nested members") @@ -381,10 +382,10 @@ struct JExtractFileFilterTests { // Plain pattern matches the top-level component, so excluding `Tank` also // prevents the visitor from descending into its nested types let st = try makeTranslator(exclude: ["Tank"]) - #expect(st.importedTypes["Tank"] == nil) - #expect(st.importedTypes["Tank.Fish"] == nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] == nil) + #expect(st.extractedTypes["Tank.Fish"] == nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("swiftFilterInclude with `Type.**` keeps the parent and all nested members") @@ -392,10 +393,10 @@ struct JExtractFileFilterTests { // `Tank.**` is a type-name pattern; via the trailing-`**` rule it matches // both `Tank` itself and any nested type underneath let st = try makeTranslator(include: ["Tank.**"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] != nil) - #expect(st.importedTypes["FishTank"] == nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] != nil) + #expect(st.extractedTypes["FishTank"] == nil) } @Test("file-path-only filter does not interfere with nested-type extraction") @@ -403,9 +404,9 @@ struct JExtractFileFilterTests { // A file-path-only filter must not accidentally gate type-level filtering; // every nested type in the included file should still be extracted let st = try makeTranslator(include: ["**/Fake.swift", "Fake.swift"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] != nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] != nil) + #expect(st.extractedTypes["FishTank"] != nil) } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift index 5339e07d6..9743dbfaa 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -354,7 +355,7 @@ struct JNIEnumTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try! translator.analyze(path: "/fake/Fake.swiftinterface", text: input) var printer: CodePrinter = CodePrinter(mode: .accumulateAll) diff --git a/Tests/JExtractSwiftTests/JavaTypeAnnotationsTests.swift b/Tests/JExtractSwiftTests/JavaTypeAnnotationsTests.swift index 031487368..9291fcde1 100644 --- a/Tests/JExtractSwiftTests/JavaTypeAnnotationsTests.swift +++ b/Tests/JExtractSwiftTests/JavaTypeAnnotationsTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftSyntax import Testing @@ -27,10 +28,9 @@ struct JavaTypeAnnotationsTests { init() { let symbolTable = SwiftSymbolTable.setup( moduleName: "TestModule", - [SwiftJavaInputFile(syntax: "" as SourceFileSyntax, path: "Fake.swift")], + [SwiftInputFile(syntax: "" as SourceFileSyntax, path: "Fake.swift")], config: nil, - sourceDependencies: SourceDependencies(), - log: Logger(label: "test", logLevel: .critical) + sourceDependencies: SourceDependencies() ) self.knownTypes = SwiftKnownTypes(symbolTable: symbolTable) self.config = Configuration() diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 298ef0738..49b6e4eb3 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -71,7 +72,7 @@ final class MethodImportTests { func method_helloWorld() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -84,7 +85,7 @@ final class MethodImportTests { javaOutputDirectory: "/fake" ) - let funcDecl = try #require(st.importedGlobalFuncs.first { $0.name == "helloWorld" }) + let funcDecl = try #require(st.extractedGlobalFuncs.first { $0.name == "helloWorld" }) let output = CodePrinter.toString { printer in generator.printJavaBindingWrapperMethod(&printer, funcDecl) @@ -113,13 +114,13 @@ final class MethodImportTests { func func_globalTakeInt() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "globalTakeInt" } ) @@ -162,13 +163,13 @@ final class MethodImportTests { func func_globalTakeIntLongString() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "globalTakeIntLongString" } ) @@ -208,13 +209,13 @@ final class MethodImportTests { func func_globalReturnClass() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "globalReturnClass" } ) @@ -254,13 +255,13 @@ final class MethodImportTests { func func_globalSwapRawBufferPointer() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "swapRawBufferPointer" } ) @@ -303,13 +304,13 @@ final class MethodImportTests { func method_class_helloMemberFunction() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) - let funcDecl: ImportedFunc = try #require( - st.importedTypes["MySwiftClass"]!.methods.first { + let funcDecl: ExtractedFunc = try #require( + st.extractedTypes["MySwiftClass"]!.methods.first { $0.name == "helloMemberFunction" } ) @@ -348,13 +349,13 @@ final class MethodImportTests { func method_class_makeInt() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) - let funcDecl: ImportedFunc = try #require( - st.importedTypes["MySwiftClass"]!.methods.first { + let funcDecl: ExtractedFunc = try #require( + st.extractedTypes["MySwiftClass"]!.methods.first { $0.name == "makeInt" } ) @@ -399,13 +400,13 @@ final class MethodImportTests { func class_constructor() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) - let initDecl: ImportedFunc = try #require( - st.importedTypes["MySwiftClass"]!.initializers.first { + let initDecl: ExtractedFunc = try #require( + st.extractedTypes["MySwiftClass"]!.initializers.first { $0.name == "init" } ) @@ -453,14 +454,14 @@ final class MethodImportTests { func struct_constructor() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) - let initDecl: ImportedFunc = try #require( - st.importedTypes["MySwiftStruct"]!.initializers.first { + let initDecl: ExtractedFunc = try #require( + st.extractedTypes["MySwiftStruct"]!.initializers.first { $0.name == "init" } ) @@ -508,13 +509,13 @@ final class MethodImportTests { func func_globalReturnAny() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) #expect( - !st.importedGlobalFuncs.contains { + !st.extractedGlobalFuncs.contains { $0.name == "globalReturnAny" }, "'Any' return type is not supported yet" diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index f2303d453..ef4797713 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -69,29 +70,29 @@ struct SpecializationTests { """# // ==== ----------------------------------------------------------------------- - // MARK: importedTypes structure + // MARK: extractedTypes structure - @Test("Multiple specializations of same base type produce distinct importedTypes") + @Test("Multiple specializations of same base type produce distinct extractedTypes") func multipleSpecializationsProduceDistinctTypes() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) // Both specialized types should be registered - #expect(translator.importedTypes["FishBox"] != nil, "FishBox should be in importedTypes") - #expect(translator.importedTypes["ToolBox"] != nil, "ToolBox should be in importedTypes") + #expect(translator.extractedTypes["FishBox"] != nil, "FishBox should be in extractedTypes") + #expect(translator.extractedTypes["ToolBox"] != nil, "ToolBox should be in extractedTypes") - // The base generic type remains in importedTypes (not removed) - let baseBox = try #require(translator.importedTypes["Box"]) + // The base generic type remains in extractedTypes (not removed) + let baseBox = try #require(translator.extractedTypes["Box"]) #expect(!baseBox.isSpecialization, "Base 'Box' should not be a specialization") #expect(baseBox.genericParameterNames == ["Element"]) #expect(baseBox.genericArguments.isEmpty) #expect(!baseBox.isFullySpecialized) // Specialized types link back to their base - let fishBox = try #require(translator.importedTypes["FishBox"]) - let toolBox = try #require(translator.importedTypes["ToolBox"]) + let fishBox = try #require(translator.extractedTypes["FishBox"]) + let toolBox = try #require(translator.extractedTypes["ToolBox"]) #expect(fishBox.isSpecialization) #expect(toolBox.isSpecialization) @@ -117,12 +118,12 @@ struct SpecializationTests { // Both wrappers delegate to the same base type #expect(fishBox.specializationBaseType === toolBox.specializationBaseType, "Both should wrap the same base Box type") - #expect(fishBox.specializationBaseType === translator.importedTypes["Box"], "Base should be the original Box") + #expect(fishBox.specializationBaseType === translator.extractedTypes["Box"], "Base should be the original Box") // Both wrappers have owned method models - let baseCountFunc: ImportedFunc = try #require(baseBox.methods.first(where: { $0.name == "count" })) - let fishCountFunc: ImportedFunc = try #require(fishBox.methods.first(where: { $0.name == "count" })) - let toolCountFunc: ImportedFunc = try #require(toolBox.methods.first(where: { $0.name == "count" })) + let baseCountFunc: ExtractedFunc = try #require(baseBox.methods.first(where: { $0.name == "count" })) + let fishCountFunc: ExtractedFunc = try #require(fishBox.methods.first(where: { $0.name == "count" })) + let toolCountFunc: ExtractedFunc = try #require(toolBox.methods.first(where: { $0.name == "count" })) #expect(baseCountFunc.parentType?.description == "Box") #expect(fishCountFunc.parentType?.description == "FishBox") #expect(toolCountFunc.parentType?.description == "ToolBox") @@ -132,10 +133,10 @@ struct SpecializationTests { func specializationEntriesContainAll() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) - let baseBox = try #require(translator.importedTypes["Box"]) + let baseBox = try #require(translator.extractedTypes["Box"]) let specializations = try #require(translator.specializations[baseBox]) #expect(specializations.count == 2, "Should have exactly 2 specializations for Box") @@ -192,9 +193,9 @@ struct SpecializationTests { // Verify observeTheFish does NOT appear inside ToolBox's class body var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) - let toolBox = try #require(translator.importedTypes["ToolBox"]) + let toolBox = try #require(translator.extractedTypes["ToolBox"]) let methodNames = toolBox.methods.map(\.name) #expect(!methodNames.contains("observeTheFish"), "ToolBox should not have Fish-constrained method") } @@ -337,7 +338,7 @@ struct SpecializationTests { func specializeNonGenericTypeThrows() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze( path: "/fake/Fake.swiftinterface", text: """ @@ -347,7 +348,7 @@ struct SpecializationTests { """, ) - let fish = try #require(translator.importedTypes["Fish"]) + let fish = try #require(translator.extractedTypes["Fish"]) #expect(!fish.swiftNominal.isGeneric) #expect(throws: SpecializationError.self) { diff --git a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift index 864457aa2..2517b884f 100644 --- a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift +++ b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// @_spi(Testing) import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -21,38 +22,6 @@ import Testing @Suite("Swift symbol table") struct SwiftSymbolTableSuite { - @Test func lookupBindingTests() throws { - let sourceFile1: SourceFileSyntax = """ - extension X.Y { - struct Z { } - } - extension X { - struct Y {} - } - """ - let sourceFile2: SourceFileSyntax = """ - struct X {} - """ - let symbolTable = SwiftSymbolTable.setup( - moduleName: "MyModule", - [ - .init(syntax: sourceFile1, path: "Fake.swift"), - .init(syntax: sourceFile2, path: "Fake2.swift"), - ], - config: nil, - sourceDependencies: SourceDependencies(), - log: Logger(label: "swift-java", logLevel: .critical), - ) - - let x = try #require(symbolTable.lookupType("X", parent: nil)) - let xy = try #require(symbolTable.lookupType("Y", parent: x)) - let xyz = try #require(symbolTable.lookupType("Z", parent: xy)) - #expect(xyz.qualifiedName == "X.Y.Z") - - #expect(symbolTable.lookupType("Y", parent: nil) == nil) - #expect(symbolTable.lookupType("Z", parent: nil) == nil) - } - @Test(arguments: [JExtractGenerationMode.jni, .ffm]) func resolveSelfModuleName(mode: JExtractGenerationMode) throws { try assertOutput( @@ -94,35 +63,6 @@ struct SwiftSymbolTableSuite { ) } - @Test func moduleScopedLookup() throws { - let sourceFile: SourceFileSyntax = """ - public struct MyClass {} - """ - let symbolTable = SwiftSymbolTable.setup( - moduleName: "MyModule", - [ - .init(syntax: sourceFile, path: "Fake.swift") - ], - config: nil, - sourceDependencies: SourceDependencies(), - log: Logger(label: "swift-java", logLevel: .critical), - ) - - // Lookup in self-module by qualified name - let myClass = symbolTable.lookupTopLevelNominalType("MyClass", inModule: "MyModule") - #expect(myClass != nil) - #expect(myClass?.qualifiedName == "MyClass") - - // Lookup in imported module (Swift) - let swiftInt = symbolTable.lookupTopLevelNominalType("Int", inModule: "Swift") - #expect(swiftInt != nil) - #expect(swiftInt?.qualifiedName == "Int") - - // Lookup in unknown module returns nil - let unknown = symbolTable.lookupTopLevelNominalType("Foo", inModule: "NoSuchModule") - #expect(unknown == nil) - } - @Test(arguments: [JExtractGenerationMode.jni, .ffm]) func resolveQualifiedTypesInFunctionSignatures(mode: JExtractGenerationMode) throws { try assertOutput( diff --git a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift index 55cabb115..a849b2474 100644 --- a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift +++ b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -48,10 +49,10 @@ struct TypealiasResolutionTests { func primitiveAliasResolvesStructMembers() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) - let user = try #require(translator.importedTypes["TypealiasUser"]) + let user = try #require(translator.extractedTypes["TypealiasUser"]) #expect(user.variables.contains { $0.name == "amount" }, "Property `amount: Amount` should be extracted") #expect(user.methods.contains { $0.name == "doubled" }, "Method `doubled() -> Amount` should be extracted") @@ -62,11 +63,11 @@ struct TypealiasResolutionTests { func primitiveAliasResolvesFreeFunc() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) #expect( - translator.importedGlobalFuncs.contains { $0.name == "makeAmount" }, + translator.extractedGlobalFuncs.contains { $0.name == "makeAmount" }, "Global func `makeAmount(_:)` should be extracted" ) } @@ -88,10 +89,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let holder = try #require(translator.importedTypes["Holder"]) + let holder = try #require(translator.extractedTypes["Holder"]) #expect(holder.variables.contains { $0.name == "value" }) #expect(!holder.initializers.isEmpty) } @@ -112,10 +113,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrapOrZero" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "unwrapOrZero" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) // The parameter type should be Optional (substituted), preserving @@ -143,10 +144,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "describe" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "describe" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { @@ -175,10 +176,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let holder = try #require(translator.importedTypes["Holder"]) + let holder = try #require(translator.extractedTypes["Holder"]) #expect(holder.variables.isEmpty, "Property `bad: OneArg` should be dropped (arity mismatch)") } @@ -200,12 +201,12 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) // The struct itself is still imported, but its members are dropped // because the alias never resolves. - let usesAlias = try #require(translator.importedTypes["UsesAlias"]) + let usesAlias = try #require(translator.extractedTypes["UsesAlias"]) #expect(usesAlias.variables.isEmpty, "Property `x: A` should be silently dropped (cycle)") } @@ -259,10 +260,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "passA" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "passA" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) #expect( paramType.description == "Int64", @@ -284,10 +285,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrap" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "unwrap" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { @@ -309,10 +310,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "first" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "first" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { @@ -343,10 +344,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "add" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "add" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) #expect( paramType.description == "Int64", @@ -375,10 +376,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let player = try #require(translator.importedTypes["Player"]) + let player = try #require(translator.extractedTypes["Player"]) let bump = try #require(player.methods.first { $0.name == "bump" }) let paramType = try #require(bump.functionSignature.parameters.first?.type) #expect(paramType.description == "Int64") @@ -400,10 +401,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "openIntBag" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "openIntBag" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { Issue.record("Expected Optional nominal, got \(paramType)") diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift new file mode 100644 index 000000000..64ff1d3c1 --- /dev/null +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -0,0 +1,430 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftExtract +import Testing + +/// End-to-end tests that drive the analysis pipeline (Swift source → +/// `AnalysisResult`) without touching any code-generation layer. These verify +/// `SwiftExtract` produces a correct analysis snapshot independent of any +/// downstream language target. +@Suite("AnalysisResult") +struct AnalysisResultSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Top-level types + + @Test func topLevelTypesAreRecorded() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + } + public class FishTank { + public init() {} + } + public enum Status { + case open, closed + } + """ + ) + ], + moduleName: "Aquarium" + ) + + #expect(result.extractedTypes["Tank"] != nil) + #expect(result.extractedTypes["FishTank"] != nil) + #expect(result.extractedTypes["Status"] != nil) + + let tank = try #require(result.extractedTypes["Tank"]) + #expect(tank.swiftNominal.kind == .struct) + #expect(tank.swiftNominal.isGeneric) + + let fishTank = try #require(result.extractedTypes["FishTank"]) + #expect(fishTank.swiftNominal.kind == .class) + + let status = try #require(result.extractedTypes["Status"]) + #expect(status.swiftNominal.kind == .enum) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Methods on a type + + @Test func methodsAreRecordedOnEnclosingType() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class FishTank { + public init() {} + public func feed() {} + public func count() -> Int { 0 } + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let fishTank = try #require(result.extractedTypes["FishTank"]) + let methodNames = Set(fishTank.methods.map(\.name)) + #expect(methodNames == ["feed", "count"]) + #expect(fishTank.initializers.count == 1) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Properties (variables) — getter/setter pair + + @Test func storedPropertyProducesGetterAndSetter() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class FishTank { + public init() {} + public var capacity: Int = 0 + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let fishTank = try #require(result.extractedTypes["FishTank"]) + let capacityAccessors = fishTank.variables.filter { $0.name == "capacity" } + let kinds = Set(capacityAccessors.map(\.apiKind)) + #expect(kinds == [.getter, .setter]) + } + + @Test func readOnlyPropertyHasOnlyGetter() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class FishTank { + public init() {} + public var name: String { "Fish Tank" } + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let fishTank = try #require(result.extractedTypes["FishTank"]) + let nameAccessors = fishTank.variables.filter { $0.name == "name" } + let kinds = nameAccessors.map(\.apiKind) + #expect(kinds == [.getter]) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Global functions and variables + + @Test func globalFunctionLandsInImportedGlobalFuncs() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public func feedAll() {} + public func mood() -> String { "" } + """ + ) + ], + moduleName: "Aquarium" + ) + + let names = Set(result.extractedGlobalFuncs.map(\.name)) + #expect(names == ["feedAll", "mood"]) + #expect(result.extractedTypes.isEmpty) + } + + @Test func globalVariableProducesGetterSetterPair() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public var globalCounter: Int = 0 + """ + ) + ], + moduleName: "Aquarium" + ) + + let counterAccessors = result.extractedGlobalVariables.filter { $0.name == "globalCounter" } + let kinds = Set(counterAccessors.map(\.apiKind)) + #expect(kinds == [.getter, .setter]) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Effect specifiers (throws / async) + + @Test func effectSpecifiersAreCapturedOnFunctionSignatures() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public func plain() {} + public func throwing() throws {} + public func asynchronous() async {} + public func both() async throws {} + """ + ) + ], + moduleName: "Aquarium" + ) + + let byName = Dictionary(uniqueKeysWithValues: result.extractedGlobalFuncs.map { ($0.name, $0) }) + let plain = try #require(byName["plain"]) + #expect(plain.functionSignature.effectSpecifiers.isEmpty) + + let throwing = try #require(byName["throwing"]) + #expect(throwing.functionSignature.effectSpecifiers.contains(.throws)) + #expect(!throwing.functionSignature.effectSpecifiers.contains(.async)) + + let asynchronous = try #require(byName["asynchronous"]) + #expect(asynchronous.functionSignature.effectSpecifiers.contains(.async)) + #expect(!asynchronous.functionSignature.effectSpecifiers.contains(.throws)) + + let both = try #require(byName["both"]) + #expect(both.functionSignature.effectSpecifiers.contains(.async)) + #expect(both.functionSignature.effectSpecifiers.contains(.throws)) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Access-level filtering + + @Test func internalDeclarationsAreNotImportedByDefault() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class Public { + public init() {} + } + internal class Internal { + init() {} + } + private class Private { + init() {} + } + """ + ) + ], + moduleName: "Aquarium" + ) + + #expect(result.extractedTypes["Public"] != nil) + #expect(result.extractedTypes["Internal"] == nil) + #expect(result.extractedTypes["Private"] == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Filter include/exclude + + @Test func swiftFilterExcludeSkipsMatchingTypes() throws { + var config = DefaultSwiftExtractConfiguration() + config.swiftFilterExclude = ["Skip*"] + + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class Tank { + public init() {} + } + public class SkipMe { + public init() {} + } + public class SkipAlso { + public init() {} + } + """ + ) + ], + moduleName: "Aquarium", + config: config + ) + + #expect(result.extractedTypes["Tank"] != nil) + #expect(result.extractedTypes["SkipMe"] == nil) + #expect(result.extractedTypes["SkipAlso"] == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Generic typealias produces a specialization + + @Test func genericTypealiasProducesSpecializationEntry() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + } + public struct Fish {} + public typealias FishTank = Tank + """ + ) + ], + moduleName: "Aquarium" + ) + + // Both the generic base and its specialization land in extractedTypes. + #expect(result.extractedTypes["Tank"] != nil) + let fishTank = try #require(result.extractedTypes["FishTank"]) + #expect(fishTank.isSpecialization) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Empty input + + @Test func emptyModuleProducesEmptyResult() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ("/fake/Source.swift", "// nothing here") + ], + moduleName: "Empty" + ) + + #expect(result.extractedTypes.isEmpty) + #expect(result.extractedGlobalFuncs.isEmpty) + #expect(result.extractedGlobalVariables.isEmpty) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Configuration knobs + + /// By default, initializers on an unspecialized generic type are NOT + /// extracted: the open generic isn't directly constructible, so swift-java + /// (and any caller leaving the knob at its default `false`) drops them. + @Test func unspecializedGenericInitializersAreSkippedByDefault() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + public init(capacity: Int) {} + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let tank = try #require(result.extractedTypes["Tank"]) + #expect(tank.swiftNominal.isGeneric) + #expect(!tank.isSpecialization) + #expect(tank.initializers.isEmpty) + } + + /// Opting into `extractsGenericTypeInitializers` keeps the base type's + /// initializers in the analysis result so post-analysis specializers can + /// clone them onto a concrete `Tank`. + @Test func extractsGenericTypeInitializersKeepsBaseInitializers() throws { + var config = DefaultSwiftExtractConfiguration() + config.extractsGenericTypeInitializers = true + + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + public init(capacity: Int) {} + } + """ + ) + ], + moduleName: "Aquarium", + config: config + ) + + let tank = try #require(result.extractedTypes["Tank"]) + #expect(tank.swiftNominal.isGeneric) + #expect(!tank.isSpecialization) + #expect(tank.initializers.count == 2) + } + + /// `#if canImport()` blocks are inactive by default for modules the + /// build configuration doesn't know about — the type guarded behind them + /// must not appear in the analysis result. + @Test func canImportGuardedDeclsAreSkippedWhenModuleNotAvailable() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct AlwaysHere { + public init() {} + } + #if canImport(MadeUpModule) + public struct OnlyWhenImportable { + public init() {} + } + #endif + """ + ) + ], + moduleName: "Aquarium" + ) + + #expect(result.extractedTypes["AlwaysHere"] != nil) + #expect(result.extractedTypes["OnlyWhenImportable"] == nil) + } + + /// Adding the module to `availableImportModules` activates the + /// `#if canImport()` clause so its declarations are extracted. + @Test func availableImportModulesActivatesCanImportClause() throws { + var config = DefaultSwiftExtractConfiguration() + config.availableImportModules = ["MadeUpModule"] + + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct AlwaysHere { + public init() {} + } + #if canImport(MadeUpModule) + public struct OnlyWhenImportable { + public init() {} + } + #endif + """ + ) + ], + moduleName: "Aquarium", + config: config + ) + + #expect(result.extractedTypes["AlwaysHere"] != nil) + #expect(result.extractedTypes["OnlyWhenImportable"] != nil) + } +} diff --git a/Tests/SwiftExtractTests/SourceDependenciesTests.swift b/Tests/SwiftExtractTests/SourceDependenciesTests.swift new file mode 100644 index 000000000..278a1dde5 --- /dev/null +++ b/Tests/SwiftExtractTests/SourceDependenciesTests.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftExtract +import SwiftParser +import SwiftSyntax +import Testing + +@Suite("SourceDependencies") +struct SourceDependenciesSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Real dependency module + + @Test func realDependencyModuleResolvesItsTypes() throws { + var deps = SourceDependencies() + deps.swiftModuleInputs["DepModule"] = [ + makeInputFile("public class DepClass {}", path: "Dep.swift") + ] + + let symbolTable = makeSymbolTable( + moduleName: "MyModule", + sources: ["public func use(_ x: DepClass) {}"], + sourceDependencies: deps + ) + + let dep = try #require(symbolTable.lookupTopLevelNominalType("DepClass")) + #expect(dep.moduleName == "DepModule") + #expect(dep.kind == .class) + + // Module-scoped lookup also works. + let depViaModule = symbolTable.lookupTopLevelNominalType("DepClass", inModule: "DepModule") + #expect(depViaModule === dep) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Synthetic stubs + + /// Verifies the fix that keeps `` resolvable for type lookup + /// while excluding it from anything that would emit `import `. + @Test func syntheticStubsAreResolvableButNotPrintable() throws { + var deps = SourceDependencies() + deps.syntheticStubInputs[""] = [ + makeInputFile("@JavaClass public class JavaUtilFunction {}", path: ".swift") + ] + + let symbolTable = makeSymbolTable( + moduleName: "MyModule", + sources: ["public struct Anything {}"], + sourceDependencies: deps + ) + + // The stub type is resolvable. + let stub = try #require(symbolTable.lookupTopLevelNominalType("JavaUtilFunction")) + #expect(stub.moduleName == "") + + // The synthetic name is recorded as such. + #expect(symbolTable.syntheticImportedModuleNames.contains("")) + + // It must NOT be confused with a real module — `Swift` is not synthetic. + #expect(!symbolTable.syntheticImportedModuleNames.contains("Swift")) + } + + // ==== ----------------------------------------------------------------------- + // MARK: swiftModuleNames / syntheticModuleNames + + @Test func moduleNameSetsAreSeparateAndUnion() { + var deps = SourceDependencies() + deps.swiftModuleInputs["DepModule"] = [makeInputFile("public class A {}")] + deps.syntheticStubInputs[""] = [ + makeInputFile("@JavaClass public class B {}") + ] + + #expect(deps.swiftModuleNames == Set(["DepModule", ""])) + #expect(deps.syntheticModuleNames == Set([""])) + } + + @Test func mutatingOneFieldDoesNotAffectTheOther() { + var deps = SourceDependencies() + deps.swiftModuleInputs["DepModule"] = [makeInputFile("public class A {}")] + #expect(deps.syntheticModuleNames.isEmpty) + + deps.syntheticStubInputs[""] = [ + makeInputFile("@JavaClass public class B {}") + ] + #expect(deps.swiftModuleInputs.keys.contains("DepModule")) + #expect(!deps.swiftModuleInputs.keys.contains("")) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Empty dependencies + + @Test func emptyDependenciesProduceUsableSymbolTable() throws { + let symbolTable = makeSymbolTable(sources: ["public struct X {}"]) + + // Built-in Swift module is still available even with no dependencies. + #expect(symbolTable.lookupTopLevelNominalType("Int") != nil) + + // No synthetic modules. + #expect(symbolTable.syntheticImportedModuleNames.isEmpty) + } +} diff --git a/Tests/SwiftExtractTests/Support/SymbolTableFixture.swift b/Tests/SwiftExtractTests/Support/SymbolTableFixture.swift new file mode 100644 index 000000000..4d5e36ac0 --- /dev/null +++ b/Tests/SwiftExtractTests/Support/SymbolTableFixture.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftExtract +import SwiftParser +import SwiftSyntax + +/// Build a `SwiftSymbolTable` from inline Swift source strings. +/// +/// Each entry in `sources` becomes a synthetic `Test.swift` file feeding +/// the primary module being analysed. +func makeSymbolTable( + moduleName: String = "TestModule", + sources: [String], + sourceDependencies: SourceDependencies = SourceDependencies() +) -> SwiftSymbolTable { + let inputs: [SwiftInputFile] = sources.enumerated().map { (i, src) in + SwiftInputFile( + syntax: Parser.parse(source: src), + path: "Test\(i).swift" + ) + } + return SwiftSymbolTable.setup( + moduleName: moduleName, + inputs, + config: nil, + sourceDependencies: sourceDependencies, + ) +} + +/// Convenience: build a single `SwiftInputFile` from a source string. +func makeInputFile(_ source: String, path: String = "Dep.swift") -> SwiftInputFile { + SwiftInputFile(syntax: Parser.parse(source: source), path: path) +} diff --git a/Tests/SwiftExtractTests/SwiftKnownModuleTests.swift b/Tests/SwiftExtractTests/SwiftKnownModuleTests.swift new file mode 100644 index 000000000..a1912cc2c --- /dev/null +++ b/Tests/SwiftExtractTests/SwiftKnownModuleTests.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftExtract +import Testing + +@Suite("SwiftKnownModule and SwiftKnownTypes") +struct SwiftKnownModuleSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Built-in Swift module catalog + + @Test(arguments: [ + "Int", "Int8", "Int16", "Int32", "Int64", + "UInt", "UInt8", "UInt16", "UInt32", "UInt64", + "Float", "Double", + "Bool", "String", + "Array", "Dictionary", "Set", "Optional", + ]) + func swiftModuleContains(_ typeName: String) throws { + let table = SwiftKnownModule.swift.symbolTable + let decl = try #require(table.lookupTopLevelNominalType(typeName)) + #expect(decl.name == typeName) + #expect(decl.moduleName == "Swift") + } + + // ==== ----------------------------------------------------------------------- + // MARK: SwiftKnownTypes accessor + + @Test func knownTypesExposeNominalSwiftStdlibTypes() throws { + let symbolTable = makeSymbolTable(sources: ["public struct X {}"]) + let known = SwiftKnownTypes(symbolTable: symbolTable) + + // Each accessor must yield a nominal type whose decl is the corresponding + // Swift stdlib type. + let int8Decl = try #require(known.int8.asNominalTypeDeclaration) + #expect(int8Decl.knownTypeKind == .int8) + + let uint8Decl = try #require(known.uint8.asNominalTypeDeclaration) + #expect(uint8Decl.knownTypeKind == .uint8) + + let stringDecl = try #require(known.string.asNominalTypeDeclaration) + #expect(stringDecl.knownTypeKind == .string) + + let boolDecl = try #require(known.bool.asNominalTypeDeclaration) + #expect(boolDecl.knownTypeKind == .bool) + + let doubleDecl = try #require(known.double.asNominalTypeDeclaration) + #expect(doubleDecl.knownTypeKind == .double) + } +} diff --git a/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift b/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift new file mode 100644 index 000000000..4b8e44dbd --- /dev/null +++ b/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift @@ -0,0 +1,192 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 SwiftExtract +import SwiftParser +import SwiftSyntax +import Testing + +@Suite("SwiftSymbolTable") +struct SwiftSymbolTableSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Lookup binding (moved from JExtractSwiftTests) + + @Test func lookupBindingTests() throws { + let sourceFile1: SourceFileSyntax = """ + extension X.Y { + struct Z { } + } + extension X { + struct Y {} + } + """ + let sourceFile2: SourceFileSyntax = """ + struct X {} + """ + let symbolTable = SwiftSymbolTable.setup( + moduleName: "MyModule", + [ + .init(syntax: sourceFile1, path: "Fake.swift"), + .init(syntax: sourceFile2, path: "Fake2.swift"), + ], + config: nil, + sourceDependencies: SourceDependencies(), + ) + + let x = try #require(symbolTable.lookupType("X", parent: nil)) + let xy = try #require(symbolTable.lookupType("Y", parent: x)) + let xyz = try #require(symbolTable.lookupType("Z", parent: xy)) + #expect(xyz.qualifiedName == "X.Y.Z") + + #expect(symbolTable.lookupType("Y", parent: nil) == nil) + #expect(symbolTable.lookupType("Z", parent: nil) == nil) + } + + @Test func moduleScopedLookup() throws { + let symbolTable = makeSymbolTable( + moduleName: "MyModule", + sources: ["public struct MyClass {}"] + ) + + // Lookup in self-module by qualified name + let myClass = symbolTable.lookupTopLevelNominalType("MyClass", inModule: "MyModule") + #expect(myClass != nil) + #expect(myClass?.qualifiedName == "MyClass") + + // Lookup in imported module (Swift) + let swiftInt = symbolTable.lookupTopLevelNominalType("Int", inModule: "Swift") + #expect(swiftInt != nil) + #expect(swiftInt?.qualifiedName == "Int") + + // Lookup in unknown module returns nil + let unknown = symbolTable.lookupTopLevelNominalType("Foo", inModule: "NoSuchModule") + #expect(unknown == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Top-level lookup by nominal kind + + @Test func topLevelLookupResolvesEachNominalKind() throws { + let symbolTable = makeSymbolTable(sources: [ + """ + public struct S {} + public class C {} + public enum E { case a } + public actor A {} + public protocol P {} + """ + ]) + + let s = try #require(symbolTable.lookupTopLevelNominalType("S")) + #expect(s.kind == .struct) + + let c = try #require(symbolTable.lookupTopLevelNominalType("C")) + #expect(c.kind == .class) + + let e = try #require(symbolTable.lookupTopLevelNominalType("E")) + #expect(e.kind == .enum) + + let a = try #require(symbolTable.lookupTopLevelNominalType("A")) + #expect(a.kind == .actor) + + let p = try #require(symbolTable.lookupTopLevelNominalType("P")) + #expect(p.kind == .protocol) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Nested-type lookup, multiple levels + + @Test func nestedLookupTwoLevelsDeep() throws { + let symbolTable = makeSymbolTable(sources: [ + """ + public struct A { + public struct B { + public struct C {} + } + } + """ + ]) + + let a = try #require(symbolTable.lookupTopLevelNominalType("A")) + let b = try #require(symbolTable.lookupNestedType("B", parent: a)) + let c = try #require(symbolTable.lookupNestedType("C", parent: b)) + #expect(c.qualifiedName == "A.B.C") + + // C is not a top-level type + #expect(symbolTable.lookupTopLevelNominalType("C") == nil) + // B is not nested under C + #expect(symbolTable.lookupNestedType("B", parent: c) == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Negative lookups + + @Test func unknownNamesReturnNil() throws { + let symbolTable = makeSymbolTable(sources: [ + """ + public struct Known { + public struct Inner {} + } + """ + ]) + + #expect(symbolTable.lookupTopLevelNominalType("DoesNotExist") == nil) + #expect(symbolTable.lookupTopLevelTypealias("AlsoMissing") == nil) + + let known = try #require(symbolTable.lookupTopLevelNominalType("Known")) + #expect(symbolTable.lookupNestedType("Missing", parent: known) == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Typealias resolution + + @Test func topLevelTypealiasResolvesUnderlyingType() throws { + let symbolTable = makeSymbolTable(sources: [ + """ + public typealias Alias = Int + """ + ]) + + let alias = try #require(symbolTable.lookupTopLevelTypealias("Alias")) + #expect(alias.name == "Alias") + } + + // ==== ----------------------------------------------------------------------- + // MARK: Built-in module presence + + @Test func builtInSwiftModuleIsAlwaysRegistered() throws { + let symbolTable = makeSymbolTable(sources: ["public struct Anything {}"]) + + // Same lookup, two reachable paths: implicit cross-module, and module-scoped. + let int = try #require(symbolTable.lookupTopLevelNominalType("Int")) + #expect(int.moduleName == "Swift") + + let intInSwift = try #require(symbolTable.lookupTopLevelNominalType("Int", inModule: "Swift")) + #expect(intInSwift === int) + + #expect(symbolTable.lookupTopLevelNominalType("Int", inModule: "NoSuchModule") == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: isModuleName + + @Test func isModuleNameRecognisesOwnAndImportedModules() throws { + let symbolTable = makeSymbolTable(moduleName: "MyModule", sources: ["public struct X {}"]) + + #expect(symbolTable.isModuleName("MyModule")) + #expect(symbolTable.isModuleName("Swift")) + #expect(!symbolTable.isModuleName("NotAModule")) + } +}