diff --git a/Package.swift b/Package.swift index e009119d4..d2ad9662d 100644 --- a/Package.swift +++ b/Package.swift @@ -108,6 +108,11 @@ let package = Package( targets: ["SwiftRuntimeFunctions"] ), + .library( + name: "SwiftExtract", + targets: ["SwiftExtract"] + ), + .library( name: "JExtractSwiftLib", targets: ["JExtractSwiftLib"] @@ -336,6 +341,30 @@ 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"), + "SwiftJavaConfigurationShared", + ], + path: "Sources/SwiftExtract", + resources: [ + .process("Resources") + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ], + plugins: [ + .plugin(name: "_StaticBuildConfigPlugin") + ] + ), + .target( name: "JExtractSwiftLib", dependencies: [ @@ -347,19 +376,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 +459,7 @@ let package = Package( name: "JExtractSwiftTests", dependencies: [ "JExtractSwiftLib", + "SwiftExtract", "CodePrinting", ], swiftSettings: [ @@ -442,6 +467,18 @@ let package = Package( ] ), + .testTarget( + name: "SwiftExtractTests", + dependencies: [ + "SwiftExtract", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ] + ), + .testTarget( name: "SwiftRuntimeFunctionsTests", dependencies: [ 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/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/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..5a39f845b 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 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..5d84b983f 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 diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 1357a2492..3f84fca7c 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index bfcc02efb..2bd12a50c 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index ccecc17ee..49e71d9af 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 diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 118f4ad20..08890b3b9 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 @@ -69,7 +70,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { package init( config: Configuration, - translator: Swift2JavaTranslator, + translator: SwiftAnalyzer, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, diff --git a/Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift b/Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift new file mode 100644 index 000000000..6f71df330 --- /dev/null +++ b/Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// 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-facing name aliases for ImportedNominalType + +extension ImportedNominalType { + 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 ImportedFunc + +extension ImportedFunc { + /// 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/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index 4ce16af9a..6cec38886 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + enum JNICaching { static func cacheName(for type: ImportedNominalType) -> String { cacheName(for: type.effectiveJavaTypeName) diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index 84fabeb77..b676a8385 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 3d36ee34f..abdc1db2a 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 diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index e02a7f767..5b0e28e28 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 diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 2d7762bef..f69ea1918 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 diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index c0cdd7a06..66f89459f 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 diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 3f6797f1b..e71919e12 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 diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index adcef96fd..f1e56166d 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 @@ -65,7 +66,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { package init( config: Configuration, - translator: Swift2JavaTranslator, + translator: SwiftAnalyzer, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, 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..dbaf85cdd 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`, @@ -33,8 +35,8 @@ package struct JavaIdentifierFactory { 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) @@ -64,8 +66,8 @@ package struct JavaIdentifierFactory { package func makeJavaMethodName(_ decl: ImportedFunc) -> 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) diff --git a/Sources/JExtractSwiftLib/SourceDependencies.swift b/Sources/JExtractSwiftLib/JavaSourceDependencies.swift similarity index 58% rename from Sources/JExtractSwiftLib/SourceDependencies.swift rename to Sources/JExtractSwiftLib/JavaSourceDependencies.swift index a1128e51b..55fb17e6c 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,25 @@ 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 +62,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..4e0de0e68 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 @@ -56,3 +58,73 @@ extension SwiftKitPrinting.Names { } } + +// ==== ----------------------------------------------------------------------- +// MARK: SwiftSymbolTable printing helpers + +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/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/ThunkNameRegistry.swift b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift index ac783f5c8..9b99aa5e7 100644 --- a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift +++ b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +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 { diff --git a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift index bab21fdd9..664061973 100644 --- a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift +++ b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftSyntax diff --git a/Sources/SwiftExtract/AnalysisResult.swift b/Sources/SwiftExtract/AnalysisResult.swift new file mode 100644 index 000000000..dedfbfa4a --- /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 importedTypes: [String: ImportedNominalType] + public let importedGlobalVariables: [ImportedFunc] + public let importedGlobalFuncs: [ImportedFunc] + + public init( + importedTypes: [String: ImportedNominalType], + importedGlobalVariables: [ImportedFunc], + importedGlobalFuncs: [ImportedFunc] + ) { + self.importedTypes = importedTypes + self.importedGlobalVariables = importedGlobalVariables + self.importedGlobalFuncs = importedGlobalFuncs + } +} 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..34fcafe0c --- /dev/null +++ b/Sources/SwiftExtract/Convenience/String+Extensions.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// 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()))" + } + + /// Returns whether the string is of the format `isX` + 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 + } + + /// 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/ImportedDecls.swift similarity index 71% rename from Sources/JExtractSwiftLib/ImportedDecls.swift rename to Sources/SwiftExtract/ImportedDecls.swift index 1f6b80d28..81f622cdb 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/SwiftExtract/ImportedDecls.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 @@ -34,51 +34,50 @@ package enum SwiftAPIKind: Equatable { /// (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 ImportedNominalType: 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: ImportedNominalType? // 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: [ImportedFunc] = [] + public var methods: [ImportedFunc] = [] + public var variables: [ImportedFunc] = [] + public var cases: [ImportedEnumCase] = [] + 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 = @@ -119,41 +118,41 @@ 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 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,7 +163,7 @@ 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 { @@ -187,7 +186,7 @@ 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 importedTypes: [String: ImportedNominalType]) -> Bool { var visited: Set = [] var queue: [ImportedNominalType] = [self] while let current = queue.popLast() { @@ -203,25 +202,25 @@ 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 ImportedEnumCase: 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: ImportedFunc - init( + public init( name: String, parameters: [SwiftEnumCaseParameter], swiftDecl: any DeclSyntaxProtocol, @@ -247,7 +246,7 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { """ } - func clone(for parent: SwiftType) -> ImportedEnumCase { + public func clone(for parent: SwiftType) -> ImportedEnumCase { ImportedEnumCase( name: name, parameters: parameters, @@ -267,7 +266,7 @@ extension ImportedEnumCase: Hashable { } } -public final class ImportedFunc: ImportedDecl, CustomStringConvertible { +public final class ImportedFunc: ExtractedSwiftDecl, CustomStringConvertible { /// Swift module name (e.g. the target name where a type or function was declared) public let module: String @@ -277,26 +276,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 +304,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 +329,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, @@ -365,7 +362,7 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { """ } - func clone(for parent: SwiftType) -> ImportedFunc { + public func clone(for parent: SwiftType) -> ImportedFunc { var functionSignature = functionSignature assert(functionSignature.selfParameter?.selfType != nil) functionSignature.selfParameter?.selfType = parent @@ -388,34 +385,6 @@ extension ImportedFunc: Hashable { } } -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 { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) diff --git a/Sources/JExtractSwiftLib/Logger.swift b/Sources/SwiftExtract/Logger.swift similarity index 89% rename from Sources/JExtractSwiftLib/Logger.swift rename to Sources/SwiftExtract/Logger.swift index 5c4267830..aae0148af 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,7 +12,6 @@ // //===----------------------------------------------------------------------===// -import ArgumentParser import Foundation import SwiftJavaConfigurationShared import SwiftSyntax @@ -117,16 +116,6 @@ extension Logger { public typealias Level = SwiftJavaConfigurationShared.LogLevel } -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 -} - extension Logger.Level { var naturalIntegralValue: Int { switch self { 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 92% rename from Sources/JExtractSwiftLib/Swift2JavaVisitor.swift rename to Sources/SwiftExtract/SwiftAnalysisVisitor.swift index ed6b4ed8b..84458800f 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,19 @@ //===----------------------------------------------------------------------===// import Foundation +import Logging import SwiftIfConfig import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -final class Swift2JavaVisitor { - let translator: Swift2JavaTranslator +final class SwiftAnalysisVisitor { + let translator: SwiftAnalyzer var config: Configuration { self.translator.config } - init(translator: Swift2JavaTranslator) { + init(translator: SwiftAnalyzer) { self.translator = translator } @@ -38,7 +39,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) { @@ -179,7 +180,7 @@ final class Swift2JavaVisitor { in typeContext: ImportedNominalType?, 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 } @@ -281,7 +282,7 @@ final class Swift2JavaVisitor { in typeContext: ImportedNominalType?, 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 } @@ -328,7 +329,7 @@ final class Swift2JavaVisitor { 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 } @@ -369,7 +370,7 @@ final class Swift2JavaVisitor { subscriptDecl node: SubscriptDeclSyntax, in typeContext: ImportedNominalType?, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return } @@ -517,7 +518,7 @@ final class Swift2JavaVisitor { in typeContext: ImportedNominalType?, sourceFilePath: String, ) { - let javaName = node.name.text + let outputName = node.name.text let rhsType = node.initializer.value let genericArgs: [String] @@ -539,7 +540,7 @@ final class Swift2JavaVisitor { } registerSpecialization( - javaName: javaName, + outputName: outputName, baseType: baseType, genericArgs: genericArgs, rhsDescription: rhsType.trimmedDescription, @@ -548,7 +549,7 @@ final class Swift2JavaVisitor { /// Register a specialization from a typealias that specializes a generic type private func registerSpecialization( - javaName: String, + outputName: String, baseType: ImportedNominalType, genericArgs: [String], rhsDescription: String, @@ -566,13 +567,13 @@ final class Swift2JavaVisitor { let specialized: ImportedNominalType 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)") } // ==== ----------------------------------------------------------------------- @@ -585,8 +586,8 @@ final class Swift2JavaVisitor { } for specialized in specializations { - translator.importedTypes[specialized.effectiveJavaName] = specialized - log.info("Applied specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + translator.importedTypes[specialized.effectiveOutputName] = specialized + log.info("Applied specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } @@ -595,11 +596,11 @@ final class Swift2JavaVisitor { func applyPendingSpecializations() { for (_, specializations) in translator.specializations { for specialized in specializations { - if translator.importedTypes[specialized.effectiveJavaName] != nil { + if translator.importedTypes[specialized.effectiveOutputName] != nil { continue } - translator.importedTypes[specialized.effectiveJavaName] = specialized - log.info("Applied pending specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + translator.importedTypes[specialized.effectiveOutputName] = specialized + log.info("Applied pending specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } @@ -712,30 +713,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 = + /// 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: Configuration, + log: Logger, + in parent: ImportedNominalType?, + decider: (any ExtractDecider)? + ) -> Bool { + let accessLevelPasses: Bool = switch config.effectiveMinimumInputAccessLevelMode { 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)" ) - 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 79% rename from Sources/JExtractSwiftLib/Swift2JavaTranslator.swift rename to Sources/SwiftExtract/SwiftAnalyzer.swift index 6afa5b385..6a4818d34 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,39 @@ //===----------------------------------------------------------------------===// 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 (e.g. Java/JNI/FFM, others) can consume +/// +/// The analysis 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: Configuration /// 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. @@ -61,14 +65,19 @@ public final class Swift2JavaTranslator { /// Specializations of generic types that will get their concrete Java declarations, "as if" they were independent types package var specializations: [ImportedNominalType: 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: Configuration, + extractDecider: (any ExtractDecider)? = nil ) { guard let swiftModule = config.swiftModule else { fatalError("Missing 'swiftModule' name.") // FIXME: can we make it required in config? but we shared config for many cases @@ -76,6 +85,7 @@ public final class Swift2JavaTranslator { self.log = Logger(label: "translator", logLevel: config.logLevel ?? .info) self.config = config self.swiftModuleName = swiftModule + self.extractDecider = extractDecider if let staticBuildConfigPath = config.staticBuildConfigurationFile { do { @@ -87,7 +97,7 @@ public final class Swift2JavaTranslator { fatalError("Failed to load static build configuration from '\(staticBuildConfigPath)': \(error)") } } else { - self.buildConfig = .jextractDefault + self.buildConfig = .swiftExtractDefault } } } @@ -95,8 +105,9 @@ public final class Swift2JavaTranslator { // ===== -------------------------------------------------------------------------------------------------------------- // 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, @@ -107,7 +118,7 @@ extension Swift2JavaTranslator { 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 +128,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 +144,28 @@ 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`. Useful for tests and for callers that only + /// need analysis (no code generation). + public static func analyze( + sources: [(path: String, text: String)], + moduleName: String, + config: Configuration? = nil, + sourceDependencies: SourceDependencies = SourceDependencies(), + extractDecider: (any ExtractDecider)? = nil + ) throws -> AnalysisResult { + var effectiveConfig = config ?? Configuration() + effectiveConfig.swiftModule = moduleName + let translator = SwiftAnalyzer(config: effectiveConfig, 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 +238,6 @@ extension Swift2JavaTranslator { config: self.config, sourceDependencies: self.sourceDependencies, buildConfig: self.buildConfig, - log: self.log, ) self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) } @@ -269,13 +300,13 @@ extension Swift2JavaTranslator { // ==== ---------------------------------------------------------------------------------------------------------------- // MARK: Type translation -extension Swift2JavaTranslator { +extension SwiftAnalyzer { /// Try to resolve the given nominal declaration node into its imported representation. func importedNominalType( _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, parent: ImportedNominalType?, ) -> ImportedNominalType? { - if !nominalNode.shouldExtract(config: config, log: log, in: parent) { + if !nominalNode.shouldExtract(config: config, log: log, in: parent, decider: extractDecider) { return nil } @@ -303,7 +334,7 @@ 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 ImportedNominalType?, decider: extractDecider) else { return nil } @@ -313,7 +344,7 @@ extension Swift2JavaTranslator { func importedNominalType(_ nominal: SwiftNominalTypeDeclaration) -> ImportedNominalType? { 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 } @@ -332,7 +363,7 @@ extension Swift2JavaTranslator { // ==== ----------------------------------------------------------------------- // MARK: Errors -public struct Swift2JavaTranslatorError: Error { +public struct SwiftAnalyzerError: Error { let message: String public init(message: String) { diff --git a/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift similarity index 54% rename from Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift rename to Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift index 65adf8ff5..41f588974 100644 --- a/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift @@ -17,12 +17,12 @@ import SwiftIfConfig import SwiftSyntax /// A default, fixed build configuration during static analysis for interface extraction. -struct JExtractDefaultBuildConfiguration: BuildConfiguration { - static let shared = JExtractDefaultBuildConfiguration() +package struct SwiftExtractDefaultBuildConfiguration: BuildConfiguration { + package static let shared = SwiftExtractDefaultBuildConfiguration() private var base: StaticBuildConfiguration - init() { + 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") } @@ -35,69 +35,69 @@ struct JExtractDefaultBuildConfiguration: BuildConfiguration { } } - func isCustomConditionSet(name: String) throws -> Bool { + package func isCustomConditionSet(name: String) throws -> Bool { base.isCustomConditionSet(name: name) } - func hasFeature(name: String) throws -> Bool { + package func hasFeature(name: String) throws -> Bool { base.hasFeature(name: name) } - func hasAttribute(name: String) throws -> Bool { + package func hasAttribute(name: String) throws -> Bool { base.hasAttribute(name: name) } - func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { + package func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { try base.canImport(importPath: importPath, version: version) } - func isActiveTargetOS(name: String) throws -> Bool { + package func isActiveTargetOS(name: String) throws -> Bool { true } - func isActiveTargetArchitecture(name: String) throws -> Bool { + package func isActiveTargetArchitecture(name: String) throws -> Bool { true } - func isActiveTargetEnvironment(name: String) throws -> Bool { + package func isActiveTargetEnvironment(name: String) throws -> Bool { true } - func isActiveTargetRuntime(name: String) throws -> Bool { + package func isActiveTargetRuntime(name: String) throws -> Bool { true } - func isActiveTargetPointerAuthentication(name: String) throws -> Bool { + package func isActiveTargetPointerAuthentication(name: String) throws -> Bool { true } - func isActiveTargetObjectFormat(name: String) throws -> Bool { + package func isActiveTargetObjectFormat(name: String) throws -> Bool { true } - var targetPointerBitWidth: Int { + package var targetPointerBitWidth: Int { base.targetPointerBitWidth } - var targetAtomicBitWidths: [Int] { + package var targetAtomicBitWidths: [Int] { base.targetAtomicBitWidths } - var endianness: Endianness { + package var endianness: Endianness { base.endianness } - var languageVersion: VersionTuple { + package var languageVersion: VersionTuple { base.languageVersion } - var compilerVersion: VersionTuple { + package var compilerVersion: VersionTuple { base.compilerVersion } } -extension BuildConfiguration where Self == JExtractDefaultBuildConfiguration { - static var jextractDefault: JExtractDefaultBuildConfiguration { +extension BuildConfiguration where Self == SwiftExtractDefaultBuildConfiguration { + package static var swiftExtractDefault: SwiftExtractDefaultBuildConfiguration { .shared } } 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..40c6ef6b3 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 @@ -20,7 +20,7 @@ import SwiftJavaConfigurationShared /// 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 +31,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 +167,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 +181,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 +189,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: Configuration) -> 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 +244,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: Configuration) -> 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/ImportedSwiftModule.swift similarity index 73% rename from Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift rename to Sources/SwiftExtract/SwiftTypes/ImportedSwiftModule.swift index 8db800d63..5c22b2654 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift +++ b/Sources/SwiftExtract/SwiftTypes/ImportedSwiftModule.swift @@ -12,13 +12,13 @@ // //===----------------------------------------------------------------------===// -struct ImportedSwiftModule: Hashable { - let name: String - let availableWithModuleName: String? - var alternativeModuleNames: Set - var isMainSourceOfSymbols: Bool +public struct ImportedSwiftModule: 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 97% rename from Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift index a36475c7b..e24c2eea7 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift @@ -15,7 +15,7 @@ import SwiftSyntax /// Scan importing modules. -func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftModule] { +package func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftModule] { var importingModuleNames: [ImportedSwiftModule] = [] for item in sourceFile.statements { if let importDecl = item.item.as(ImportDeclSyntax.self) { 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..9a0261c14 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,22 @@ 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( + public init( selfParameter: SwiftSelfParameter? = nil, parameters: [SwiftParameter], result: SwiftResult, @@ -56,7 +56,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 +68,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 +89,7 @@ enum SwiftSelfParameter: Equatable { } extension SwiftFunctionSignature { - init( + public init( _ node: InitializerDeclSyntax, enclosingType: SwiftType?, lookupContext: SwiftTypeLookupContext @@ -126,7 +126,7 @@ extension SwiftFunctionSignature { ) } - init( + public init( _ node: EnumCaseElementSyntax, enclosingType: SwiftType, lookupContext: SwiftTypeLookupContext @@ -145,7 +145,7 @@ extension SwiftFunctionSignature { ) } - init( + public init( _ node: FunctionDeclSyntax, enclosingType: SwiftType?, lookupContext: SwiftTypeLookupContext @@ -212,7 +212,7 @@ extension SwiftFunctionSignature { ) } - static func translateGenericParameters( + public static func translateGenericParameters( parameterClause: GenericParameterClauseSyntax?, whereClause: GenericWhereClauseSyntax?, lookupContext: SwiftTypeLookupContext @@ -270,7 +270,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 +289,7 @@ extension SwiftFunctionSignature { return (parameters, effectSpecifiers) } - init( + public init( _ varNode: VariableDeclSyntax, isSet: Bool, enclosingType: SwiftType?, @@ -340,7 +340,7 @@ extension SwiftFunctionSignature { self.genericRequirements = [] } - init( + public init( _ subscriptNode: SubscriptDeclSyntax, isSet: Bool, enclosingType: SwiftType?, @@ -443,7 +443,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 +457,19 @@ extension VariableDeclSyntax { } extension AccessorBlockSyntax { - struct SupportedAccessorKinds: OptionSet { - var rawValue: UInt8 + public struct SupportedAccessorKinds: OptionSet { + public var rawValue: UInt8 - static var get: Self = .init(rawValue: 1 << 0) - static var set: Self = .init(rawValue: 1 << 1) + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + 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 +489,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 96% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift index 696b1f1a5..47e360146 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 diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift similarity index 95% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift index 5b1e28933..57a851b1d 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 @@ -56,7 +56,7 @@ enum SwiftKnownType: Equatable { // SwiftRuntimeFunctions case swiftJavaError - init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { + public init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { switch kind { case .bool: self = .bool case .int: self = .int @@ -113,7 +113,7 @@ enum SwiftKnownType: Equatable { } } - var kind: SwiftKnownTypeDeclKind { + public var kind: SwiftKnownTypeDeclKind { switch self { case .bool: .bool case .int: .int @@ -155,7 +155,7 @@ enum SwiftKnownType: Equatable { } } -enum SwiftKnownTypeDeclKind: String, Hashable { +public enum SwiftKnownTypeDeclKind: String, Hashable { // Swift case bool = "Swift.Bool" case int = "Swift.Int" @@ -198,7 +198,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { // 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 +207,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { ) } - var isPointer: Bool { + public var isPointer: Bool { switch self { case .unsafePointer, .unsafeMutablePointer, .unsafeRawPointer, .unsafeMutableRawPointer: return true @@ -221,7 +221,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { /// /// This means we do not have to perform any mapping when passing /// this type between jextract and wrap-java - var isDirectlyTranslatedToWrapJava: Bool { + public var isDirectlyTranslatedToWrapJava: Bool { switch self { case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string, .void: 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 79% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift index 56fe3cb9c..90b694a42 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift @@ -14,22 +14,34 @@ 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 + + public init( + convention: SwiftParameterConvention, + argumentLabel: String? = nil, + parameterName: String? = nil, + type: SwiftType + ) { + self.convention = convention + self.argumentLabel = argumentLabel + self.parameterName = parameterName + self.type = type + } } 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 +59,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,7 +69,7 @@ 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 @@ -67,7 +79,7 @@ extension SwiftParameter { } 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 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 66% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift index a4c4de73b..e8fb51020 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift @@ -12,13 +12,13 @@ // //===----------------------------------------------------------------------===// -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 +38,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 +46,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 +55,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 +77,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,11 +98,12 @@ package class SwiftSymbolTable { extension SwiftSymbolTable { package static func setup( moduleName: String, - _ inputFiles: some Collection, + _ inputFiles: some Collection, + additionalInputFiles: [SwiftInputFile] = [], config: Configuration?, sourceDependencies: SourceDependencies, - buildConfig: any BuildConfiguration = .jextractDefault, - log: Logger, + buildConfig: any BuildConfiguration = .swiftExtractDefault, + log: Logger? = nil, ) -> SwiftSymbolTable { // Prepare imported modules. @@ -122,7 +133,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 +152,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 +173,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 +194,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 +226,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 +234,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 +249,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 +264,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 +281,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 +296,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/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..748114c30 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 } 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..4c4050cfc 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) diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index fd9203750..8ff99fd82 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,7 +42,7 @@ 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) @@ -131,7 +132,7 @@ 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) @@ -246,7 +247,7 @@ 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) diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index c514a12fd..1733bab9e 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,7 +238,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) @@ -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) diff --git a/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift b/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift index b89e574f7..643a09042 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 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..96e1b8d02 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) @@ -113,7 +114,7 @@ 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) @@ -162,7 +163,7 @@ 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) @@ -208,7 +209,7 @@ 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) @@ -254,7 +255,7 @@ 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) @@ -303,7 +304,7 @@ 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) @@ -348,7 +349,7 @@ 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) @@ -399,7 +400,7 @@ 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) @@ -453,7 +454,7 @@ 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 @@ -508,7 +509,7 @@ 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) diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index f2303d453..876342941 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -75,7 +76,7 @@ struct SpecializationTests { 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 @@ -132,7 +133,7 @@ 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"]) @@ -192,7 +193,7 @@ 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 methodNames = toolBox.methods.map(\.name) @@ -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: """ 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..dff3ed40f 100644 --- a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift +++ b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -48,7 +49,7 @@ 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"]) @@ -62,7 +63,7 @@ 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( @@ -88,7 +89,7 @@ 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"]) @@ -112,7 +113,7 @@ 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" }) @@ -143,7 +144,7 @@ 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" }) @@ -175,7 +176,7 @@ 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"]) @@ -200,7 +201,7 @@ 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 @@ -259,7 +260,7 @@ 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" }) @@ -284,7 +285,7 @@ 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" }) @@ -309,7 +310,7 @@ 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" }) @@ -343,7 +344,7 @@ 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" }) @@ -375,7 +376,7 @@ 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"]) @@ -400,7 +401,7 @@ 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" }) diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift new file mode 100644 index 000000000..ac3123ba5 --- /dev/null +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -0,0 +1,317 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJavaConfigurationShared +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.importedTypes["Tank"] != nil) + #expect(result.importedTypes["FishTank"] != nil) + #expect(result.importedTypes["Status"] != nil) + + let tank = try #require(result.importedTypes["Tank"]) + #expect(tank.swiftNominal.kind == .struct) + #expect(tank.swiftNominal.isGeneric) + + let fishTank = try #require(result.importedTypes["FishTank"]) + #expect(fishTank.swiftNominal.kind == .class) + + let status = try #require(result.importedTypes["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.importedTypes["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.importedTypes["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.importedTypes["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.importedGlobalFuncs.map(\.name)) + #expect(names == ["feedAll", "mood"]) + #expect(result.importedTypes.isEmpty) + } + + @Test func globalVariableProducesGetterSetterPair() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public var globalCounter: Int = 0 + """ + ) + ], + moduleName: "Aquarium" + ) + + let counterAccessors = result.importedGlobalVariables.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.importedGlobalFuncs.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.importedTypes["Public"] != nil) + #expect(result.importedTypes["Internal"] == nil) + #expect(result.importedTypes["Private"] == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Filter include/exclude + + @Test func swiftFilterExcludeSkipsMatchingTypes() throws { + var config = Configuration() + 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.importedTypes["Tank"] != nil) + #expect(result.importedTypes["SkipMe"] == nil) + #expect(result.importedTypes["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 importedTypes. + #expect(result.importedTypes["Tank"] != nil) + let fishTank = try #require(result.importedTypes["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.importedTypes.isEmpty) + #expect(result.importedGlobalFuncs.isEmpty) + #expect(result.importedGlobalVariables.isEmpty) + } +} 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")) + } +}