diff --git a/Package.swift b/Package.swift index 800a9a0ca..18fda7eef 100644 --- a/Package.swift +++ b/Package.swift @@ -43,6 +43,11 @@ let package = Package( targets: ["SwiftJavaConfigurationShared"] ), + .library( + name: "SwiftExtractConfigurationShared", + targets: ["SwiftExtractConfigurationShared"] + ), + .library( name: "JavaUtil", targets: ["JavaUtil"] @@ -108,6 +113,16 @@ let package = Package( targets: ["SwiftRuntimeFunctions"] ), + .library( + name: "SwiftExtract", + targets: ["SwiftExtract"] + ), + + .library( + name: "CodePrinting", + targets: ["CodePrinting"] + ), + .library( name: "JExtractSwiftLib", targets: ["JExtractSwiftLib"] @@ -271,7 +286,14 @@ let package = Package( ), .target( - name: "SwiftJavaConfigurationShared" + name: "SwiftJavaConfigurationShared", + dependencies: [ + "SwiftExtractConfigurationShared" + ] + ), + + .target( + name: "SwiftExtractConfigurationShared" ), .target( @@ -336,6 +358,33 @@ let package = Package( ] ), + .target( + name: "SwiftExtract", + dependencies: [ + .product(name: "SwiftBasicFormat", package: "swift-syntax"), + .product(name: "SwiftIfConfig", package: "swift-syntax"), + .product(name: "SwiftLexicalLookup", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + .product(name: "Logging", package: "swift-log"), + "SwiftExtractConfigurationShared", + ], + path: "Sources/SwiftExtract", + resources: [ + // Holds the `dummy.json` placeholder so SwiftPM emits a `Bundle.module` + // for this target. The real `static-build-config.json` is generated at + // build time by the `_StaticBuildConfigPlugin` build tool below. + .process("Resources") + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ], + plugins: [ + .plugin(name: "_StaticBuildConfigPlugin") + ] + ), + .target( name: "JExtractSwiftLib", dependencies: [ @@ -347,19 +396,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 +479,7 @@ let package = Package( name: "JExtractSwiftTests", dependencies: [ "JExtractSwiftLib", + "SwiftExtract", "CodePrinting", ], swiftSettings: [ @@ -442,6 +487,22 @@ let package = Package( ] ), + .testTarget( + name: "SwiftExtractTests", + dependencies: [ + "SwiftExtract", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), + ] + ), + + .testTarget( + name: "CodePrintingTests", + dependencies: [ + "CodePrinting" + ] + ), + .testTarget( name: "SwiftRuntimeFunctionsTests", dependencies: [ diff --git a/Plugins/PluginsShared/SwiftExtractConfigurationShared b/Plugins/PluginsShared/SwiftExtractConfigurationShared new file mode 120000 index 000000000..263f08269 --- /dev/null +++ b/Plugins/PluginsShared/SwiftExtractConfigurationShared @@ -0,0 +1 @@ +../../Sources/SwiftExtractConfigurationShared \ No newline at end of file diff --git a/Sources/CodePrinting/CodePrinter.swift b/Sources/CodePrinting/CodePrinter.swift index d67679039..4d91c884b 100644 --- a/Sources/CodePrinting/CodePrinter.swift +++ b/Sources/CodePrinting/CodePrinter.swift @@ -21,11 +21,23 @@ import Foundation // ==== ----------------------------------------------------------------------- // MARK: CodePrinter -public struct CodePrinter { +public struct CodePrinter: Sendable { public var contents: String = "" public var verbose: Bool = false + /// When true, terminators of `.sloc` append a `// function @ file:line` + /// trailer to each line. Useful for debugging the generator. Downstream targets that compare + /// generated output against goldens can flip this off to get a clean + /// terminator equivalent to `.newLine`. + public var emitSourceLocations: Bool = true + + /// Token used to start an inline comment. Defaults to `//` for Swift- and + /// Java-style output. Hash-comment languages can flip this to `.hash` so + /// source-location trailers, `printSeparator` banners, and echo-mode + /// headers render with the right comment lead. + public var inlineCommentStyle: InlineCommentStyle = .slashSlash + public var indentationDepth: Int = 0 { didSet { indentationText = String(repeating: indentationPart, count: indentationDepth) @@ -49,12 +61,16 @@ public struct CodePrinter { } public var mode: PrintMode - public enum PrintMode { + public enum PrintMode: Sendable { case accumulateAll case flushToFileOnWrite } - public init(mode: PrintMode = .flushToFileOnWrite) { + public init( + mode: PrintMode = .flushToFileOnWrite, + inlineCommentStyle: InlineCommentStyle = .slashSlash + ) { self.mode = mode + self.inlineCommentStyle = inlineCommentStyle } public mutating func append(_ text: String) { @@ -167,7 +183,11 @@ public struct CodePrinter { } if terminator == .sloc { - append(" // \(function) @ \(file):\(line)\n") + if emitSourceLocations { + append(" \(inlineCommentStyle.rawValue) \(function) @ \(file):\(line)\n") + } else { + append("\n") + } atNewline = true } else { append(terminator.rawValue) @@ -181,11 +201,12 @@ public struct CodePrinter { public mutating func printSeparator(_ text: String) { assert(!text.contains(where: \.isNewline)) + let lead = inlineCommentStyle.rawValue print( """ - // ==== -------------------------------------------------- - // \(text) + \(lead) ==== -------------------------------------------------- + \(lead) \(text) """ ) @@ -215,10 +236,22 @@ public struct CodePrinter { } } +// ==== ----------------------------------------------------------------------- +// MARK: InlineCommentStyle + +/// Inline comment lead used by `CodePrinter` for source-location trailers, +/// `printSeparator` banners, and echo-mode headers. Swift, Java, and other +/// `//`-comment languages use `.slashSlash`; hash-comment languages use +/// `.hash`. +public enum InlineCommentStyle: String, Sendable { + case slashSlash = "//" + case hash = "#" +} + // ==== ----------------------------------------------------------------------- // MARK: PrinterTerminator -public enum PrinterTerminator: String { +public enum PrinterTerminator: String, Sendable { case newLine = "\n" case space = " " case commaSpace = ", " @@ -285,19 +318,20 @@ extension CodePrinter { // if we're accumulating everything, we don't want to finalize/flush // any contents; let's mark that this is where a write would have // happened though: - print("// ^^^^ Contents of: \(outputDirectory)\(PATH_SEPARATOR)\(filename)") + print("\(inlineCommentStyle.rawValue) ^^^^ Contents of: \(outputDirectory)\(PATH_SEPARATOR)\(filename)") return nil } let contents = finalize() if outputDirectory == "-" { + let lead = inlineCommentStyle.rawValue print( - "// ==== ---------------------------------------------------------------------------------------------------" + "\(lead) ==== ---------------------------------------------------------------------------------------------------" ) if let javaPackagePath { - print("// \(javaPackagePath)\(PATH_SEPARATOR)\(filename)") + print("\(lead) \(javaPackagePath)\(PATH_SEPARATOR)\(filename)") } else { - print("// \(filename)") + print("\(lead) \(filename)") } print(contents) return nil diff --git a/Sources/JExtractSwiftLib/AnalysisResult.swift b/Sources/JExtractSwiftLib/AnalysisResult.swift deleted file mode 100644 index 4d33bea19..000000000 --- a/Sources/JExtractSwiftLib/AnalysisResult.swift +++ /dev/null @@ -1,19 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -struct AnalysisResult { - let importedTypes: [String: ImportedNominalType] - let importedGlobalVariables: [ImportedFunc] - let importedGlobalFuncs: [ImportedFunc] -} diff --git a/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift b/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift index 7f540e527..2c733cb0e 100644 --- a/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift +++ b/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift new file mode 100644 index 000000000..1435a62ab --- /dev/null +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract +import SwiftJavaConfigurationShared + +/// `Configuration` already exposes every property the analysis layer needs +/// (`swiftModule`, `swiftFilterInclude`, `effectiveMinimumInputAccessLevelMode`, +/// `logLevel`, …) — `LogLevel` and `AccessLevelMode` are the same enums on +/// both sides, both pulled in from `SwiftExtractConfigurationShared`. The +/// protocol extension on `SwiftExtractConfiguration` defaults +/// `availableImportModules` and `allowUnresolvedTypeReferences`, so this +/// conformance is empty. +extension Configuration: SwiftExtractConfiguration {} diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index b2d5394a5..023910767 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaJNICore extension JavaType { diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+JNIExtensions.swift similarity index 65% rename from Sources/JExtractSwiftLib/Convenience/String+Extensions.swift rename to Sources/JExtractSwiftLib/Convenience/String+JNIExtensions.swift index 9fdb6a64c..d317d1b06 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+JNIExtensions.swift @@ -15,34 +15,7 @@ import SwiftJavaJNICore extension String { - - var firstCharacterUppercased: String { - guard let f = first else { - return self - } - - return "\(f.uppercased())\(String(dropFirst()))" - } - - var firstCharacterLowercased: String { - guard let f = first else { - return self - } - - return "\(f.lowercased())\(String(dropFirst()))" - } - - /// Returns whether the string is of the format `isX` - var hasJavaBooleanNamingConvention: Bool { - guard self.hasPrefix("is"), self.count > 2 else { - return false - } - - let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2) - return self[thirdCharacterIndex].isUppercase - } - - /// Returns a version of the string correctly escaped for a JNI + /// Returns a version of the string correctly escaped for a JNI identifier var escapedJNIIdentifier: String { self.map { if $0 == "_" { @@ -66,15 +39,6 @@ extension String { .joined() } - /// If the string ends with `.swift`, return it without that suffix; - /// otherwise return self unchanged - func dropSwiftFileSuffix() -> String { - if hasSuffix(".swift") { - return String(dropLast(".swift".count)) - } - return self - } - /// Looks up self as a SwiftJava wrapped class name and converts it /// into a `JavaType.class` if it exists in `lookupTable`. func parseJavaClassFromSwiftJavaName(in lookupTable: [String: String]) -> JavaType? { @@ -87,14 +51,6 @@ extension String { return .class(package: javaPackageName, name: javaClassName) } - - /// Unescapes the name if it is surrounded by backticks. - var unescapedSwiftName: String { - if count >= 2 && hasPrefix("`") && hasSuffix("`") { - return String(dropFirst().dropLast()) - } - return self - } } extension Array where Element == String { diff --git a/Sources/JExtractSwiftLib/Convenience/String+JavaNaming.swift b/Sources/JExtractSwiftLib/Convenience/String+JavaNaming.swift new file mode 100644 index 000000000..8d77383ef --- /dev/null +++ b/Sources/JExtractSwiftLib/Convenience/String+JavaNaming.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension String { + /// Returns whether the string is of the format `isX` (Java Beans boolean + /// property naming convention) + package var hasJavaBooleanNamingConvention: Bool { + guard self.hasPrefix("is"), self.count > 2 else { + return false + } + + let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2) + return self[thirdCharacterIndex].isUppercase + } +} diff --git a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift new file mode 100644 index 000000000..68965ce14 --- /dev/null +++ b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract + +// ==== ----------------------------------------------------------------------- +// MARK: Java name typealiases + +package typealias JavaClassName = String +package typealias JavaFullyQualifiedClassName = String +package typealias JavaPackageName = String + +extension ExtractedNominalType { + /// Structured Java-output-facing type name — "FishBox" for a specialization + /// registered via `typealias FishBox = Box`, the qualified base name + /// for a non-specialized type. For Java generation that's also the + /// generated class name. + package var effectiveJavaTypeName: SwiftQualifiedTypeName { effectiveOutputTypeName } + + /// Fully-qualified Java-output name (string form of `effectiveJavaTypeName`). + package var effectiveJavaName: String { + effectiveJavaTypeName.fullName + } + + /// Simple (unqualified) Java-output class name. Used for file naming + /// and Java-side declarations. + package var effectiveJavaSimpleName: String { + specializedTypeName ?? swiftNominal.name + } + + package var javaGenericClause: String { outputGenericClause } +} + +// ==== ----------------------------------------------------------------------- +// MARK: Java-facing name aliases for ExtractedFunc + +extension ExtractedFunc { + /// The Java getter name for a Swift property/subscript getter, following + /// Java Beans conventions: `get` for non-boolean, `is` for + /// boolean (unless the property already starts with `is`, in which case + /// the original name is preserved). + /// + /// Returns `nil` when the underlying declaration is not a getter — i.e. a + /// regular function, initializer, enum case, or setter — since those don't + /// have a Java getter name. + package var javaGetterName: String? { + switch apiKind { + case .getter, .subscriptGetter: break + case .setter, .subscriptSetter, .function, .initializer, .enumCase: return nil + } + + let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool + + if !returnsBoolean { + return "get\(self.name.firstCharacterUppercased)" + } else if !self.name.hasJavaBooleanNamingConvention { + return "is\(self.name.firstCharacterUppercased)" + } else { + return self.name + } + } + + /// The Java setter name for a Swift property/subscript setter. If the + /// property already starts with `is` (boolean naming), the `is` prefix is + /// stripped so the setter becomes `set` per Java Beans spec. + /// + /// Returns `nil` when the underlying declaration is not a setter — i.e. a + /// regular function, initializer, enum case, or getter — since those don't + /// have a Java setter name. + package var javaSetterName: String? { + switch apiKind { + case .setter, .subscriptSetter: break + case .getter, .subscriptGetter, .function, .initializer, .enumCase: return nil + } + + let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool + + if isBooleanSetter && self.name.hasJavaBooleanNamingConvention { + // Safe to force unwrap due to `hasJavaBooleanNamingConvention` check. + let propertyName = self.name.split(separator: "is", maxSplits: 1).last! + return "set\(propertyName)" + } else { + return "set\(self.name.firstCharacterUppercased)" + } + } +} diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index 58dc89263..727f226e3 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. /// @@ -71,7 +73,7 @@ extension CType { case .tuple([]): self = .void - case .genericParameter, .metatype, .tuple, .opaque, .existential, .composite: + case .genericParameter, .metatype, .tuple, .opaque, .existential, .composite, .inlineArray: throw CDeclToCLoweringError.invalidCDeclType(cdeclType) } } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 565c5c9de..5b577f52b 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaJNICore import SwiftSyntax @@ -364,10 +365,6 @@ struct CdeclLowering { case .foundationData, .essentialsData: break - case .swiftJavaError: - // SwiftJavaError is a class — treat as arbitrary nominal type below - break - default: // Unreachable? Should be handled by `CType(cdeclType:)` lowering above. throw LoweringError.unhandledType(type) @@ -449,6 +446,9 @@ struct CdeclLowering { case .composite: throw LoweringError.unhandledType(type) + + case .inlineArray: + throw LoweringError.unhandledType(type) } } @@ -541,7 +541,7 @@ struct CdeclLowering { } throw LoweringError.unhandledType(knownTypes.optionalSugar(wrappedType)) - case .function, .metatype, .composite: + case .function, .metatype, .composite, .inlineArray: throw LoweringError.unhandledType(knownTypes.optionalSugar(wrappedType)) } } @@ -643,7 +643,7 @@ struct CdeclLowering { // Custom types are not supported yet. throw LoweringError.unhandledType(type) - case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite: + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .inlineArray: // TODO: Implement throw LoweringError.unhandledType(type) } @@ -859,7 +859,7 @@ struct CdeclLowering { conversion: .tupleExplode(conversions, name: outParameterName), ) - case .genericParameter, .function, .existential, .opaque, .composite: + case .genericParameter, .function, .existential, .opaque, .composite, .inlineArray: throw LoweringError.unhandledType(type) } } diff --git a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift index 03541539c..a086828f2 100644 --- a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift +++ b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftSyntax import SwiftSyntaxBuilder diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift index 7dd054f22..a482041c5 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax @@ -23,7 +24,7 @@ import struct Foundation.URL extension FFMSwift2JavaGenerator { /// Print Java helper methods for Foundation.Data type - package func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + package func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { let typeName = decl.swiftNominal.name let thunkNameCopyBytes = "swiftjava_\(swiftModuleName)_\(typeName)_copyBytes__" diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 1357a2492..af5bb32e7 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -13,13 +13,14 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore extension FFMSwift2JavaGenerator { package func printFunctionDowncallMethods( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { guard let _ = translatedDecl(for: decl) else { // Failed to translate. Skip. @@ -39,7 +40,7 @@ extension FFMSwift2JavaGenerator { /// Print FFM Java binding descriptors for the imported Swift API. package func printJavaBindingDescriptorClass( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let thunkName = thunkNameRegistry.functionThunkName(decl: decl) let translated = self.translatedDecl(for: decl)! @@ -270,7 +271,7 @@ extension FFMSwift2JavaGenerator { /// * User-facing functional interfaces. func printJavaBindingWrapperHelperClass( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let translated = self.translatedDecl(for: decl)! let bindingDescriptorName = self.thunkNameRegistry.functionThunkName(decl: decl) @@ -359,7 +360,7 @@ extension FFMSwift2JavaGenerator { /// with adding `SwiftArena.ofAuto()` at the end. package func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let translated = self.translatedDecl(for: decl)! let methodName = translated.name @@ -420,7 +421,7 @@ extension FFMSwift2JavaGenerator { /// This assumes that all the parameters are passed-in with appropriate names. package func printDowncall( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { //=== Part 1: prepare temporary arena if needed. let translatedSignature = self.translatedDecl(for: decl)!.translatedSignature @@ -454,7 +455,7 @@ extension FFMSwift2JavaGenerator { let arena = if let className = type.className, - analysis.importedTypes[className] != nil + analysis.extractedTypes[className] != nil { // Use passed-in 'SwiftArena' for 'SwiftValue'. "swiftArena" diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index bfcc02efb..0b4f52695 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -12,12 +12,13 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore extension FFMSwift2JavaGenerator { func translatedDecl( - for decl: ImportedFunc + for decl: ExtractedFunc ) -> TranslatedFunctionDecl? { if let cached = translatedDecls[decl] { return cached @@ -175,7 +176,7 @@ extension FFMSwift2JavaGenerator { self.javaIdentifiers = javaIdentifiers } - func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { + func translate(_ decl: ExtractedFunc) throws -> TranslatedFunctionDecl { let lowering = CdeclLowering(knownTypes: knownTypes) let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature) @@ -450,10 +451,6 @@ extension FFMSwift2JavaGenerator { case .foundationData, .essentialsData: break - case .swiftJavaError: - // SwiftJavaError is a class — treat as arbitrary nominal type below - break - default: throw JavaTranslationError.unhandledType(swiftType) } @@ -521,7 +518,7 @@ extension FFMSwift2JavaGenerator { // Otherwise, not supported yet. throw JavaTranslationError.unhandledType(swiftType) - case .composite: + case .composite, .inlineArray: throw JavaTranslationError.unhandledType(swiftType) } } @@ -819,7 +816,7 @@ extension FFMSwift2JavaGenerator { resultAnnotations: resultAnnotations ) - case .genericParameter, .function, .existential, .opaque, .composite: + case .genericParameter, .function, .existential, .opaque, .composite, .inlineArray: throw JavaTranslationError.unhandledType(swiftType) } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index ccecc17ee..9e39cefdc 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftSyntax import SwiftSyntaxBuilder @@ -73,20 +74,20 @@ extension FFMSwift2JavaGenerator { // We have to write all types to their corresponding output file that matches the file they were declared in, // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. - let filteredTypes: [String: ImportedNominalType] + let filteredTypes: [String: ExtractedNominalType] if let singleType = config.singleType { - filteredTypes = self.analysis.importedTypes.filter { $0.key == singleType } + filteredTypes = self.analysis.extractedTypes.filter { $0.key == singleType } } else { - filteredTypes = self.analysis.importedTypes + filteredTypes = self.analysis.extractedTypes } - for group: (key: String, value: [Dictionary.Element]) in Dictionary( + for group: (key: String, value: [Dictionary.Element]) in Dictionary( grouping: filteredTypes, by: { $0.value.sourceFilePath }, ) { log.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") - let importedTypesForThisFile = group.value + let extractedTypesForThisFile = group.value .map(\.value) .sorted(by: { $0.qualifiedName < $1.qualifiedName }) @@ -104,7 +105,7 @@ extension FFMSwift2JavaGenerator { ) self.lookupContext.symbolTable.printImportedModules(&printer) - for ty in importedTypesForThisFile { + for ty in extractedTypesForThisFile { log.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)") printer.printSeparator("Thunks for \(ty.qualifiedName)") @@ -147,7 +148,7 @@ extension FFMSwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) self.currentJavaIdentifiers = JavaIdentifierFactory( - self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + self.analysis.extractedGlobalFuncs + self.analysis.extractedGlobalVariables ) for thunk in stt.renderGlobalThunks() { @@ -156,7 +157,7 @@ extension FFMSwift2JavaGenerator { } } - public func printSwiftThunkSources(_ printer: inout CodePrinter, decl: ImportedFunc) { + public func printSwiftThunkSources(_ printer: inout CodePrinter, decl: ExtractedFunc) { let stt = SwiftThunkTranslator(self) for thunk in stt.render(forFunc: decl) { @@ -165,7 +166,7 @@ extension FFMSwift2JavaGenerator { } } - package func printSwiftThunkSources(_ printer: inout CodePrinter, ty: ImportedNominalType) throws { + package func printSwiftThunkSources(_ printer: inout CodePrinter, ty: ExtractedNominalType) throws { let stt = SwiftThunkTranslator(self) self.currentJavaIdentifiers = JavaIdentifierFactory( @@ -190,14 +191,14 @@ struct SwiftThunkTranslator { func renderGlobalThunks() -> [DeclSyntax] { var decls: [DeclSyntax] = [] decls.reserveCapacity( - st.analysis.importedGlobalVariables.count + st.analysis.importedGlobalFuncs.count + st.analysis.extractedGlobalVariables.count + st.analysis.extractedGlobalFuncs.count ) - for decl in st.analysis.importedGlobalVariables { + for decl in st.analysis.extractedGlobalVariables { decls.append(contentsOf: render(forFunc: decl)) } - for decl in st.analysis.importedGlobalFuncs { + for decl in st.analysis.extractedGlobalFuncs { decls.append(contentsOf: render(forFunc: decl)) } @@ -205,7 +206,7 @@ struct SwiftThunkTranslator { } /// Render all the thunks that make Swift methods accessible to Java. - func renderThunks(forType nominal: ImportedNominalType) -> [DeclSyntax] { + func renderThunks(forType nominal: ExtractedNominalType) -> [DeclSyntax] { var decls: [DeclSyntax] = [] decls.reserveCapacity( 1 + nominal.initializers.count + nominal.variables.count + nominal.methods.count @@ -232,7 +233,7 @@ struct SwiftThunkTranslator { } /// Accessor to get the `T.self` of the Swift type, without having to rely on mangled name lookups. - func renderSwiftTypeAccessor(_ nominal: ImportedNominalType) -> DeclSyntax { + func renderSwiftTypeAccessor(_ nominal: ExtractedNominalType) -> DeclSyntax { let funcName = SwiftKitPrinting.Names.getType( module: st.swiftModuleName, nominal: nominal, @@ -247,7 +248,7 @@ struct SwiftThunkTranslator { """ } - func render(forFunc decl: ImportedFunc) -> [DeclSyntax] { + func render(forFunc decl: ExtractedFunc) -> [DeclSyntax] { st.log.trace("Rendering thunks for: \(decl.displayName)") let thunkName = st.thunkNameRegistry.functionThunkName(decl: decl) @@ -264,7 +265,7 @@ struct SwiftThunkTranslator { } /// Render special thunks for known types like Foundation.Data - func renderSpecificTypeThunks(_ nominal: ImportedNominalType) -> [DeclSyntax] { + func renderSpecificTypeThunks(_ nominal: ExtractedNominalType) -> [DeclSyntax] { guard let knownType = nominal.swiftNominal.knownTypeKind else { return [] } @@ -278,7 +279,7 @@ struct SwiftThunkTranslator { } /// Render Swift thunks for Foundation.Data helper methods - private func renderFoundationDataThunks(_ nominal: ImportedNominalType) -> [DeclSyntax] { + private func renderFoundationDataThunks(_ nominal: ExtractedNominalType) -> [DeclSyntax] { let thunkName = "swiftjava_\(st.swiftModuleName)_\(nominal.swiftNominal.name)_copyBytes__" let qualifiedName = nominal.swiftNominal.qualifiedName diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 118f4ad20..4a9f899ce 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax @@ -37,7 +38,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { var thunkNameRegistry: ThunkNameRegistry = ThunkNameRegistry() /// Cached Java translation result. 'nil' indicates failed translation. - var translatedDecls: [ImportedFunc: TranslatedFunctionDecl?] = [:] + var translatedDecls: [ExtractedFunc: TranslatedFunctionDecl?] = [:] /// Duplicate identifier tracking for the current batch of methods being generated. var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() @@ -69,7 +70,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { package init( config: Configuration, - translator: Swift2JavaTranslator, + translator: SwiftAnalyzer, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, @@ -85,7 +86,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { // If we are forced to write empty files, construct the expected outputs. // It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway. - if translator.config.effectiveWriteEmptyFiles { + if config.effectiveWriteEmptyFiles { self.expectedOutputSwiftFileNames = Set( translator.inputs.compactMap { (input) -> String? in guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { @@ -151,8 +152,8 @@ extension FFMSwift2JavaGenerator { ] /// Returns the Java class name for a nominal type, applying known-type overrides - func javaClassName(for decl: ImportedNominalType) -> String { - if decl.swiftNominal.knownTypeKind == .swiftJavaError { + func javaClassName(for decl: ExtractedNominalType) -> String { + if decl.swiftNominal.isSwiftJavaErrorType { return JavaType.swiftJavaErrorException.className! } return decl.swiftNominal.name @@ -170,13 +171,13 @@ extension FFMSwift2JavaGenerator { /// Every imported public type becomes a public class in its own file in Java. package func writeExportedJavaSources(printer: inout CodePrinter) throws { - let typesToExport: [(key: String, value: ImportedNominalType)] + let typesToExport: [(key: String, value: ExtractedNominalType)] if let singleType = config.singleType { - typesToExport = analysis.importedTypes + typesToExport = analysis.extractedTypes .filter { $0.key == singleType } .sorted(by: { $0.key < $1.key }) } else { - typesToExport = analysis.importedTypes + typesToExport = analysis.extractedTypes .sorted(by: { $0.key < $1.key }) } @@ -226,24 +227,24 @@ extension FFMSwift2JavaGenerator { printImports(&printer) self.currentJavaIdentifiers = JavaIdentifierFactory( - self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + self.analysis.extractedGlobalFuncs + self.analysis.extractedGlobalVariables ) printModuleClass(&printer) { printer in - for decl in analysis.importedGlobalVariables { + for decl in analysis.extractedGlobalVariables { self.log.trace("Print imported decl: \(decl)") printFunctionDowncallMethods(&printer, decl) } - for decl in analysis.importedGlobalFuncs { + for decl in analysis.extractedGlobalFuncs { self.log.trace("Print imported decl: \(decl)") printFunctionDowncallMethods(&printer, decl) } } } - func printImportedNominal(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + func printImportedNominal(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printHeader(&printer) printPackage(&printer) printImports(&printer) // TODO: we could have some imports be driven from types used in the generated decl @@ -252,7 +253,7 @@ extension FFMSwift2JavaGenerator { decl.initializers + decl.variables + decl.methods ) - let isErrorType = decl.swiftNominal.knownTypeKind == .swiftJavaError + let isErrorType = decl.swiftNominal.isSwiftJavaErrorType self.currentSymbolLookup = isErrorType ? .swiftRuntime : .module printNominal(&printer, decl) { printer in @@ -386,10 +387,10 @@ extension FFMSwift2JavaGenerator { func printNominal( _ printer: inout CodePrinter, - _ decl: ImportedNominalType, + _ decl: ExtractedNominalType, body: (inout CodePrinter) -> Void, ) { - let isErrorType = decl.swiftNominal.knownTypeKind == .swiftJavaError + let isErrorType = decl.swiftNominal.isSwiftJavaErrorType let baseClass: String let parentProtocol: String @@ -424,8 +425,8 @@ extension FFMSwift2JavaGenerator { /// Returns a closure that prints the constructor and related extras for special nominal types /// (e.g. error types), or `nil` for normal types that use the default layout + constructor - func getSpecialNominalConstructorPrinting(_ decl: ImportedNominalType) -> ((inout CodePrinter) -> Void)? { - if decl.swiftNominal.knownTypeKind == .swiftJavaError { + func getSpecialNominalConstructorPrinting(_ decl: ExtractedNominalType) -> ((inout CodePrinter) -> Void)? { + if decl.swiftNominal.isSwiftJavaErrorType { return { printer in // Error constructor: wrap the opaque pointer so it becomes a pointer-to-reference // (matching the convention used by normal class instance thunks) @@ -448,8 +449,8 @@ extension FFMSwift2JavaGenerator { /// Returns a closure that prints post-members extras for special nominal types /// (e.g. `fetchDescription` for error types), or `nil` for normal types that use `toString()` - func getSpecialNominalPostMembersPrinting(_ decl: ImportedNominalType) -> ((inout CodePrinter) -> Void)? { - if decl.swiftNominal.knownTypeKind == .swiftJavaError { + func getSpecialNominalPostMembersPrinting(_ decl: ExtractedNominalType) -> ((inout CodePrinter) -> Void)? { + if decl.swiftNominal.isSwiftJavaErrorType { return { printer in // Error types inherit toString() from Exception; print fetchDescription helper instead self.printSwiftJavaErrorFetchDescriptionMethod(&printer, decl) @@ -568,7 +569,7 @@ extension FFMSwift2JavaGenerator { ) } - private func printClassMemoryLayout(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printClassMemoryLayout(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printer.print( """ public static final GroupLayout $LAYOUT = (GroupLayout) SwiftValueWitnessTable.layoutOfSwiftType(TYPE_METADATA.$memorySegment()); @@ -581,7 +582,7 @@ extension FFMSwift2JavaGenerator { func printToStringMethod( _ printer: inout CodePrinter, - _ decl: ImportedNominalType, + _ decl: ExtractedNominalType, ) { printer.print( """ @@ -598,7 +599,7 @@ extension FFMSwift2JavaGenerator { } /// Print special helper methods for known types like Foundation.Data - func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { guard let knownType = decl.swiftNominal.knownTypeKind else { return } @@ -614,7 +615,7 @@ extension FFMSwift2JavaGenerator { /// Print the `fetchDescription` static helper for SwiftJavaError. /// This calls the `errorDescription()` downcall to get the error message /// for the super constructor - func printSwiftJavaErrorFetchDescriptionMethod(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + func printSwiftJavaErrorFetchDescriptionMethod(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { // Find the errorDescription method's thunk name let errorDescMethod = decl.methods.first { $0.name == "errorDescription" } guard let errorDescMethod, let _ = translatedDecl(for: errorDescMethod) else { diff --git a/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift b/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift deleted file mode 100644 index 65adf8ff5..000000000 --- a/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift +++ /dev/null @@ -1,103 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation -import SwiftIfConfig -import SwiftSyntax - -/// A default, fixed build configuration during static analysis for interface extraction. -struct JExtractDefaultBuildConfiguration: BuildConfiguration { - static let shared = JExtractDefaultBuildConfiguration() - - private var base: StaticBuildConfiguration - - init() { - guard let url = Bundle.module.url(forResource: "static-build-config", withExtension: "json") else { - fatalError("static-build-config.json is not found in module bundle") - } - do { - let data = try Data(contentsOf: url) - let decoder = JSONDecoder() - base = try decoder.decode(StaticBuildConfiguration.self, from: data) - } catch { - fatalError("\(error)") - } - } - - func isCustomConditionSet(name: String) throws -> Bool { - base.isCustomConditionSet(name: name) - } - - func hasFeature(name: String) throws -> Bool { - base.hasFeature(name: name) - } - - func hasAttribute(name: String) throws -> Bool { - base.hasAttribute(name: name) - } - - func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { - try base.canImport(importPath: importPath, version: version) - } - - func isActiveTargetOS(name: String) throws -> Bool { - true - } - - func isActiveTargetArchitecture(name: String) throws -> Bool { - true - } - - func isActiveTargetEnvironment(name: String) throws -> Bool { - true - } - - func isActiveTargetRuntime(name: String) throws -> Bool { - true - } - - func isActiveTargetPointerAuthentication(name: String) throws -> Bool { - true - } - - func isActiveTargetObjectFormat(name: String) throws -> Bool { - true - } - - var targetPointerBitWidth: Int { - base.targetPointerBitWidth - } - - var targetAtomicBitWidths: [Int] { - base.targetAtomicBitWidths - } - - var endianness: Endianness { - base.endianness - } - - var languageVersion: VersionTuple { - base.languageVersion - } - - var compilerVersion: VersionTuple { - base.compilerVersion - } -} - -extension BuildConfiguration where Self == JExtractDefaultBuildConfiguration { - static var jextractDefault: JExtractDefaultBuildConfiguration { - .shared - } -} diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index 4ce16af9a..4173d26f7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -12,8 +12,10 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + enum JNICaching { - static func cacheName(for type: ImportedNominalType) -> String { + static func cacheName(for type: ExtractedNominalType) -> String { cacheName(for: type.effectiveJavaTypeName) } @@ -21,7 +23,7 @@ enum JNICaching { cacheName(for: type.nominalTypeDecl.qualifiedTypeName) } - static func bridgeName(for type: ImportedNominalType) -> String { + static func bridgeName(for type: ExtractedNominalType) -> String { bridgeName(for: type.swiftNominal.qualifiedTypeName) } @@ -37,7 +39,7 @@ enum JNICaching { "_JNIBridge_\(typeName.fullFlatName)" } - static func cacheMemberName(for enumCase: ImportedEnumCase) -> String { + static func cacheMemberName(for enumCase: ExtractedEnumCase) -> String { "\(enumCase.enumType.nominalTypeDecl.name.firstCharacterLowercased)\(enumCase.name.firstCharacterUppercased)Cache" } diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index 84fabeb77..3e7821d48 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore @@ -52,8 +53,7 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID, - .swiftJavaError: + .foundationUUID, .essentialsUUID: return nil } } @@ -79,8 +79,7 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID, - .swiftJavaError: + .foundationUUID, .essentialsUUID: nil } } @@ -106,8 +105,7 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID, - .swiftJavaError: + .foundationUUID, .essentialsUUID: nil } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 3d36ee34f..41a9029a0 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax @@ -20,9 +21,9 @@ import SwiftSyntax extension JNISwift2JavaGenerator { func generateInterfaceWrappers( - _ types: [ImportedNominalType] - ) -> [ImportedNominalType: JavaInterfaceSwiftWrapper] { - var wrappers = [ImportedNominalType: JavaInterfaceSwiftWrapper]() + _ types: [ExtractedNominalType] + ) -> [ExtractedNominalType: JavaInterfaceSwiftWrapper] { + var wrappers = [ExtractedNominalType: JavaInterfaceSwiftWrapper]() for type in types where type.swiftNominal.kind == .protocol { // Skip protocols that have a known representative concrete type (e.g. DataProtocol). @@ -50,7 +51,7 @@ extension JNISwift2JavaGenerator { let protocolType: SwiftNominalType let functions: [Function] let variables: [Variable] - let importedType: ImportedNominalType + let importedType: ExtractedNominalType var wrapperName: String { protocolType.nominalTypeDecl.javaInterfaceSwiftProtocolWrapperName @@ -101,7 +102,7 @@ extension JNISwift2JavaGenerator { } struct JavaInterfaceProtocolWrapperGenerator { - func generate(for type: ImportedNominalType) throws -> JavaInterfaceSwiftWrapper { + func generate(for type: ExtractedNominalType) throws -> JavaInterfaceSwiftWrapper { if !type.initializers.isEmpty || type.methods.contains(where: \.isStatic) || type.variables.contains(where: \.isStatic) @@ -163,7 +164,7 @@ extension JNISwift2JavaGenerator { ) } - private func translate(function: ImportedFunc) throws -> JavaInterfaceSwiftWrapper.Function { + private func translate(function: ExtractedFunc) throws -> JavaInterfaceSwiftWrapper.Function { let parameters = try function.functionSignature.parameters.map { try self.translateParameter($0) } @@ -180,8 +181,8 @@ extension JNISwift2JavaGenerator { } private func translateVariable( - getter: ImportedFunc, - setter: ImportedFunc? + getter: ExtractedFunc, + setter: ExtractedFunc? ) throws -> JavaInterfaceSwiftWrapper.Variable { try JavaInterfaceSwiftWrapper.Variable( swiftDecl: getter.swiftDecl, // they should be the same @@ -231,7 +232,7 @@ extension JNISwift2JavaGenerator { case .tuple([]): // void return .placeholder - case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite: + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -258,7 +259,7 @@ extension JNISwift2JavaGenerator { ) ) - case .composite, .existential, .function, .genericParameter, .metatype, .opaque, .tuple: + case .composite, .existential, .function, .genericParameter, .metatype, .opaque, .tuple, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(known: .array(elementType)) } } @@ -314,7 +315,7 @@ extension JNISwift2JavaGenerator { case .tuple([]): // void return .placeholder - case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite: + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -341,7 +342,7 @@ extension JNISwift2JavaGenerator { ) ) - case .composite, .existential, .function, .genericParameter, .metatype, .opaque, .tuple: + case .composite, .existential, .function, .genericParameter, .metatype, .opaque, .tuple, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(known: .array(elementType)) } } @@ -476,7 +477,7 @@ extension SwiftType { return false } - case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite: + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .inlineArray: return false } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index d6191a475..6a3ad1e5f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -15,6 +15,7 @@ import CodePrinting import Foundation import OrderedCollections +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore @@ -42,13 +43,13 @@ extension JNISwift2JavaGenerator { } package func writeExportedJavaSources(_ printer: inout CodePrinter) throws { - let typesToExport: [(key: String, value: ImportedNominalType)] + let typesToExport: [(key: String, value: ExtractedNominalType)] if let singleType = config.singleType { - typesToExport = analysis.importedTypes + typesToExport = analysis.extractedTypes .filter { $0.key == singleType } .sorted(by: { $0.key < $1.key }) } else { - typesToExport = analysis.importedTypes + typesToExport = analysis.extractedTypes .sorted(by: { $0.key < $1.key }) } @@ -105,7 +106,7 @@ extension JNISwift2JavaGenerator { printImports(&printer) self.currentJavaIdentifiers = JavaIdentifierFactory( - self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + self.analysis.extractedGlobalFuncs + self.analysis.extractedGlobalVariables ) printModuleClass(&printer) { printer in @@ -139,13 +140,13 @@ extension JNISwift2JavaGenerator { ) } - for decl in analysis.importedGlobalFuncs { + for decl in analysis.extractedGlobalFuncs { self.logger.trace("Print global function: \(decl)") printFunctionDowncallMethods(&printer, decl) printer.println() } - for decl in analysis.importedGlobalVariables { + for decl in analysis.extractedGlobalVariables { self.logger.trace("Print global variable: \(decl)") printFunctionDowncallMethods(&printer, decl) printer.println() @@ -153,7 +154,7 @@ extension JNISwift2JavaGenerator { } } - private func printImportedNominal(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printImportedNominal(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printHeader(&printer) printPackage(&printer) printImports(&printer) @@ -170,7 +171,7 @@ extension JNISwift2JavaGenerator { } } - private func printProtocol(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printProtocol(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { var extends = self.inheritedProtocols(of: decl).map(\.effectiveJavaSimpleName) // If we cannot generate Swift wrappers @@ -206,7 +207,7 @@ extension JNISwift2JavaGenerator { } } - private func printConcreteType(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printConcreteType(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printNominal(&printer, decl) { printer in printer.print( """ @@ -244,7 +245,7 @@ extension JNISwift2JavaGenerator { ) } - let nestedTypes = self.analysis.importedTypes.filter { _, type in + let nestedTypes = self.analysis.extractedTypes.filter { _, type in type.parent == decl.swiftNominal } @@ -398,7 +399,7 @@ extension JNISwift2JavaGenerator { } /// Prints helpers for specific types like `Foundation.Date` - private func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { guard let knownType = decl.swiftNominal.knownTypeKind else { return } switch knownType { @@ -441,7 +442,7 @@ extension JNISwift2JavaGenerator { private func printNominal( _ printer: inout CodePrinter, - _ decl: ImportedNominalType, + _ decl: ExtractedNominalType, body: (inout CodePrinter) -> Void, ) { if decl.swiftNominal.isSendable { @@ -473,7 +474,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printEnumDiscriminator(&printer, decl) printer.println() printEnumCaseInterface(&printer, decl) @@ -483,7 +484,7 @@ extension JNISwift2JavaGenerator { printEnumCases(&printer, decl) } - private func printEnumDiscriminator(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumDiscriminator(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { if decl.cases.isEmpty { return } @@ -500,7 +501,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumCaseInterface(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumCaseInterface(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { if decl.cases.isEmpty { return } @@ -557,7 +558,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { let isEffectivelyGeneric = decl.swiftNominal.isGeneric && !decl.isSpecialization if !decl.cases.isEmpty && isEffectivelyGeneric { self.logger.debug("Skipping generic static initializers in '\(decl.effectiveJavaSimpleName)'") @@ -569,7 +570,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumCases(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printEnumCases(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { let caseTypeParameters: [JavaType] = decl.genericParameterNames.map { .class(package: nil, name: $0) } @@ -625,7 +626,7 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncallMethods( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, skipMethodBody: Bool = false, ) { guard translatedDecl(for: decl) != nil else { @@ -649,7 +650,7 @@ extension JNISwift2JavaGenerator { /// * User-facing functional interfaces. private func printJavaBindingWrapperHelperClass( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let translated = self.translatedDecl(for: decl)! if translated.functionTypes.isEmpty { @@ -686,7 +687,7 @@ extension JNISwift2JavaGenerator { private func printNecessarySupportTypes( _ printer: inout CodePrinter, - _ decl: ImportedFunc + _ decl: ExtractedFunc ) { let translatedDecl = translatedDecl(for: decl)! @@ -697,7 +698,7 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, skipMethodBody: Bool, ) { guard let translatedDecl = translatedDecl(for: decl) else { @@ -709,7 +710,7 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, - importedFunc: ImportedFunc? = nil, + importedFunc: ExtractedFunc? = nil, skipMethodBody: Bool, ) { var modifiers = ["public"] @@ -878,7 +879,7 @@ extension JNISwift2JavaGenerator { } } - private func printTypeMetadataAddressFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printTypeMetadataAddressFunction(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization if isEffectivelyGeneric { printer.print("@Override") @@ -896,7 +897,7 @@ extension JNISwift2JavaGenerator { } } - private func printFoundationDateHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printFoundationDateHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printer.print( """ /** @@ -949,7 +950,7 @@ extension JNISwift2JavaGenerator { ) } - private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printer.print( """ /** diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 2d7762bef..00c606d66 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax @@ -29,12 +30,12 @@ extension JNISwift2JavaGenerator { protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, javaIdentifiers: self.currentJavaIdentifiers, - importedTypes: self.analysis.importedTypes, + extractedTypes: self.analysis.extractedTypes, ) } func translatedDecl( - for decl: ImportedFunc + for decl: ExtractedFunc ) -> TranslatedFunctionDecl? { if let cached = translatedDecls[decl] { return cached @@ -53,7 +54,7 @@ extension JNISwift2JavaGenerator { } func translatedEnumCase( - for decl: ImportedEnumCase + for decl: ExtractedEnumCase ) -> TranslatedEnumCase? { if let cached = translatedEnumCases[decl] { return cached @@ -71,7 +72,7 @@ extension JNISwift2JavaGenerator { protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, javaIdentifiers: self.currentJavaIdentifiers, - importedTypes: self.analysis.importedTypes, + extractedTypes: self.analysis.extractedTypes, ) translated = try translation.translate(enumCase: decl) } catch { @@ -90,12 +91,12 @@ extension JNISwift2JavaGenerator { let javaClassLookupTable: JavaClassLookupTable let moduleJavaPackages: ModuleJavaPackages var knownTypes: SwiftKnownTypes - let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger var javaIdentifiers: JavaIdentifierFactory - let importedTypes: [String: ImportedNominalType] + let extractedTypes: [SwiftTypeName: ExtractedNominalType] - func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { + func translate(enumCase: ExtractedEnumCase) throws -> TranslatedEnumCase { let methodName = "" // TODO: Used for closures, replace with better name? let parameterResults = try enumCase.parameters.enumerated().map { idx, parameter in @@ -122,9 +123,9 @@ extension JNISwift2JavaGenerator { } ) ) - let getAsCaseFunction: ImportedFunc? = + let getAsCaseFunction: ExtractedFunc? = if !enumCase.parameters.isEmpty { - ImportedFunc( + ExtractedFunc( module: enumCase.enumType.nominalTypeDecl.moduleName, swiftDecl: DeclSyntax("func getAs\(raw: javaCaseClassName)() -> (\(raw: associatedValueTypes))?"), name: "getAs\(javaCaseClassName)", @@ -151,7 +152,7 @@ extension JNISwift2JavaGenerator { ) } - func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { + func translate(_ decl: ExtractedFunc) throws -> TranslatedFunctionDecl { let nativeTranslation = NativeJavaTranslation( config: self.config, javaPackage: self.javaPackage, @@ -575,7 +576,7 @@ extension JNISwift2JavaGenerator { case .tuple: throw JavaTranslationError.emptyTuple() - case .composite: + case .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -973,7 +974,7 @@ extension JNISwift2JavaGenerator { genericRequirements: genericRequirements, ) - case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite: + case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -1106,7 +1107,7 @@ extension JNISwift2JavaGenerator { } return .tuple(elementTypes: elementJavaTypes) - case .metatype, .function, .existential, .opaque, .composite: + case .metatype, .function, .existential, .opaque, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -1578,12 +1579,12 @@ extension JNISwift2JavaGenerator { let name: String /// The oringinal enum case. - let original: ImportedEnumCase + let original: ExtractedEnumCase /// A list of the translated associated values let parameters: [JavaParameter] - let getAsCaseFunction: ImportedFunc? + let getAsCaseFunction: ExtractedFunc? /// Returns whether the associated values require an arena let requiresSwiftArena: Bool diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index c0cdd7a06..fe67b5221 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore @@ -23,7 +24,7 @@ extension JNISwift2JavaGenerator { let javaPackage: String let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes - let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger /// Translates a Swift function into the native JNI method signature. @@ -352,7 +353,7 @@ extension JNISwift2JavaGenerator { genericRequirements: genericRequirements ) - case .tuple, .composite: + case .tuple, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -677,7 +678,7 @@ extension JNISwift2JavaGenerator { outParameters: [] ) - case .function, .metatype, .tuple, .existential, .opaque, .genericParameter, .composite: + case .function, .metatype, .tuple, .existential, .opaque, .genericParameter, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -709,7 +710,7 @@ extension JNISwift2JavaGenerator { // Custom types are not supported yet. throw JavaTranslationError.unsupportedSwiftType(type) - case .function, .metatype, .tuple, .existential, .opaque, .genericParameter, .composite: + case .function, .metatype, .tuple, .existential, .opaque, .genericParameter, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -814,7 +815,7 @@ extension JNISwift2JavaGenerator { case .tuple(let elements) where !elements.isEmpty: return try translateTupleResult(methodName: methodName, elements: elements, resultName: resultName) - case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite: + case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 823fe7485..21063d3dc 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaJNICore import SwiftSyntax @@ -78,27 +79,27 @@ extension JNISwift2JavaGenerator { // We have to write all types to their corresponding output file that matches the file they were declared in, // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. - let filteredTypes: [String: ImportedNominalType] + let filteredTypes: [String: ExtractedNominalType] if let singleType = config.singleType { - filteredTypes = self.analysis.importedTypes.filter { $0.key == singleType } + filteredTypes = self.analysis.extractedTypes.filter { $0.key == singleType } } else { - filteredTypes = self.analysis.importedTypes + filteredTypes = self.analysis.extractedTypes } - for group: (key: String, value: [Dictionary.Element]) in Dictionary( + for group: (key: String, value: [Dictionary.Element]) in Dictionary( grouping: filteredTypes, by: { $0.value.sourceFilePath }, ) { logger.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") - let importedTypesForThisFile = group.value + let extractedTypesForThisFile = group.value .map(\.value) .sorted(by: { $0.qualifiedName < $1.qualifiedName }) let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift") - for ty in importedTypesForThisFile { + for ty in extractedTypesForThisFile { logger.info("Printing Swift thunks for type: \(ty.effectiveJavaName.bold)") printer.printSeparator("Thunks for \(ty.effectiveJavaName)") @@ -291,21 +292,21 @@ extension JNISwift2JavaGenerator { printHeader(&printer) self.currentJavaIdentifiers = JavaIdentifierFactory( - self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + self.analysis.extractedGlobalFuncs + self.analysis.extractedGlobalVariables ) - for decl in analysis.importedGlobalFuncs { + for decl in analysis.extractedGlobalFuncs { printSwiftFunctionThunk(&printer, decl) printer.println() } - for decl in analysis.importedGlobalVariables { + for decl in analysis.extractedGlobalVariables { printSwiftFunctionThunk(&printer, decl) printer.println() } } - private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { + private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) throws { printHeader(&printer) printer.println() @@ -321,7 +322,7 @@ extension JNISwift2JavaGenerator { } } - private func printConcreteTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printConcreteTypeThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { // Specialized types are treated as concrete even if the underlying Swift type is generic let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization @@ -370,7 +371,7 @@ extension JNISwift2JavaGenerator { printer.println() } - private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { + private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) throws { guard let protocolWrapper = self.interfaceProtocolWrappers[type] else { return } @@ -378,7 +379,7 @@ extension JNISwift2JavaGenerator { try printSwiftInterfaceWrapper(&printer, protocolWrapper) } - private func printEnumRawDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printEnumRawDiscriminator(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { if type.cases.isEmpty { return } @@ -394,7 +395,7 @@ extension JNISwift2JavaGenerator { } } - private func printEnumCase(_ printer: inout CodePrinter, _ enumType: ImportedNominalType, _ enumCase: ImportedEnumCase) { + private func printEnumCase(_ printer: inout CodePrinter, _ enumType: ExtractedNominalType, _ enumCase: ExtractedEnumCase) { guard let translatedCase = self.translatedEnumCase(for: enumCase) else { return } @@ -417,7 +418,7 @@ extension JNISwift2JavaGenerator { private func printEnumGetAsCaseThunk( _ printer: inout CodePrinter, - _ enumType: ImportedNominalType, + _ enumType: ExtractedNominalType, _ enumCase: TranslatedEnumCase, ) { if let getAsCaseFunction = enumCase.getAsCaseFunction { @@ -442,7 +443,7 @@ extension JNISwift2JavaGenerator { private func printSwiftFunctionThunk( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { guard let translatedDecl = translatedDecl(for: decl) else { // Failed to translate. Skip. @@ -465,7 +466,7 @@ extension JNISwift2JavaGenerator { private func printSwiftFunctionHelperClasses( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { let protocolParameters = decl.functionSignature.parameters.compactMap { parameter in if let concreteType = parameter.type.typeIn( @@ -498,7 +499,7 @@ extension JNISwift2JavaGenerator { // we generate a Swift class that conforms to all of those. for (parameter, protocolTypes) in protocolParameters { let protocolWrappers: [JavaInterfaceSwiftWrapper] = protocolTypes.compactMap { protocolType in - guard let importedType = self.asImportedNominalTypeDecl(protocolType), + guard let importedType = self.asExtractedNominalTypeDecl(protocolType), let wrapper = self.interfaceProtocolWrappers[importedType] else { return nil @@ -544,8 +545,8 @@ extension JNISwift2JavaGenerator { } } - private func asImportedNominalTypeDecl(_ type: SwiftType) -> ImportedNominalType? { - self.analysis.importedTypes.first( + private func asExtractedNominalTypeDecl(_ type: SwiftType) -> ExtractedNominalType? { + self.analysis.extractedTypes.first( where: ({ name, nominalType in nominalType.swiftType == type }) @@ -556,7 +557,7 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncall( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { guard let translatedDecl = self.translatedDecl(for: decl) else { fatalError("Cannot print function downcall for a function that can't be translated: \(decl)") @@ -782,7 +783,7 @@ extension JNISwift2JavaGenerator { } } - private func printJNICache(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printJNICache(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let cacheName = JNICaching.cacheName(for: type) let jniClassName = "\(javaPackagePath)/\(type.effectiveJavaTypeName.jniEscapedName)" let isEffectivelyGeneric = type.swiftNominal.isGeneric && type.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName @@ -819,7 +820,7 @@ extension JNISwift2JavaGenerator { } } - private func printNominalJavaBridge(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printNominalJavaBridge(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let bridgeName = JNICaching.bridgeName(for: type) let cacheName = JNICaching.cacheName(for: type) let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization @@ -880,7 +881,7 @@ extension JNISwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) } - private func printTypeMetadataAddressThunk(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printTypeMetadataAddressThunk(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { // Specialized types are treated as concrete let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization if isEffectivelyGeneric { @@ -904,7 +905,7 @@ extension JNISwift2JavaGenerator { } /// Prints thunks for specific known types like Foundation.Date, Foundation.Data - private func printSpecificTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printSpecificTypeThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { guard let knownType = type.swiftNominal.knownTypeKind else { return } switch knownType { @@ -918,7 +919,7 @@ extension JNISwift2JavaGenerator { } /// Prints Swift thunks for Foundation.Data helper methods - private func printFoundationDataThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printFoundationDataThunks(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) let parentName = type.qualifiedName @@ -964,7 +965,7 @@ extension JNISwift2JavaGenerator { } } - private func printFunctionOpenerCall(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + private func printFunctionOpenerCall(_ printer: inout CodePrinter, _ decl: ExtractedFunc) { guard let translatedDecl = self.translatedDecl(for: decl) else { fatalError("Cannot print function opener for a function that can't be translated: \(decl)") } @@ -1004,10 +1005,10 @@ extension JNISwift2JavaGenerator { "_\(swiftModuleName)_\(type.flatName)_opener" } - private func printOpenerProtocol(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + private func printOpenerProtocol(_ printer: inout CodePrinter, _ type: ExtractedNominalType) { let protocolName = openerProtocolName(for: type.swiftNominal) - func printFunctionDecl(_ printer: inout CodePrinter, decl: ImportedFunc, skipMethodBody: Bool) { + func printFunctionDecl(_ printer: inout CodePrinter, decl: ExtractedFunc, skipMethodBody: Bool) { guard let translatedDecl = self.translatedDecl(for: decl) else { return } let nativeSignature = translatedDecl.nativeFunctionSignature @@ -1127,7 +1128,7 @@ extension SwiftNominalTypeDeclaration { } } -extension ImportedFunc { +extension ExtractedFunc { fileprivate var openerMethodName: String { let prefix = switch apiKind { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 208a3228f..b0379e2f4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore @@ -49,9 +50,9 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { var generatedCDeclSymbolNames: [String] = [] /// Cached Java translation result. 'nil' indicates failed translation. - var translatedDecls: [ImportedFunc: TranslatedFunctionDecl] = [:] - var translatedEnumCases: [ImportedEnumCase: TranslatedEnumCase] = [:] - var interfaceProtocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] = [:] + var translatedDecls: [ExtractedFunc: TranslatedFunctionDecl] = [:] + var translatedEnumCases: [ExtractedEnumCase: TranslatedEnumCase] = [:] + var interfaceProtocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] = [:] /// Duplicate identifier tracking for the current batch of methods being generated. var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() @@ -65,7 +66,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { package init( config: Configuration, - translator: Swift2JavaTranslator, + translator: SwiftAnalyzer, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, @@ -85,7 +86,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { // If we are forced to write empty files, construct the expected outputs. // It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway. - if translator.config.effectiveWriteEmptyFiles { + if config.effectiveWriteEmptyFiles { self.expectedOutputSwiftFileNames = Set( translator.inputs.compactMap { (input) -> String? in guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { @@ -116,11 +117,11 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { self.expectedOutputSwiftFileNames = [] } - if translator.config.enableJavaCallbacks ?? false { + if config.enableJavaCallbacks ?? false { // We translate all the protocol wrappers // as we need them to know what protocols we can allow the user to implement themselves // in Java. - self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.importedTypes.values)) + self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.extractedTypes.values)) } } @@ -142,12 +143,12 @@ extension JNISwift2JavaGenerator { "\(parameterName)$indirect" } - func inheritedProtocols(of type: ImportedNominalType) -> [ImportedNominalType] { + func inheritedProtocols(of type: ExtractedNominalType) -> [ExtractedNominalType] { type.inheritedTypes .compactMap(\.asNominalTypeDeclaration) .filter { $0.kind == .protocol } .compactMap { - self.analysis.importedTypes[$0.qualifiedName] + self.analysis.extractedTypes[$0.qualifiedName] } } } diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift new file mode 100644 index 000000000..34e2e164a --- /dev/null +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftSyntax + +public func makeSwiftJavaAnalyzer(config: Configuration) -> SwiftAnalyzer { + SwiftAnalyzer( + config: config, + extractDecider: JavaExtractDecider( + accessLevel: config.effectiveMinimumInputAccessLevelMode, + logLevel: config.logLevel ?? .info + ), + ) +} + +/// Java-specific per-decl extraction policy +/// +/// In addition to the configured access-level filter, the Java target: +/// +/// - Skips initializers of unspecialized generic types +/// - Force-extracts decls annotated `@JavaExport` even if they would +/// otherwise be filtered by access level +/// - Skips Swift wrappers of Java types (`@JavaClass`, `@JavaInterface`, +/// `@JavaField`, `@JavaStaticField`, `@JavaMethod`, `@JavaStaticMethod`, +/// `@JavaImplementation`) since those are bridged the other way +/// - Skips Swift operators (`+`, `-`, prefix/postfix forms) — Java has no +/// operator-overload syntax, so the generator can't render them +public struct JavaExtractDecider: ExtractDecider { + public let accessLevel: AccessLevelMode + let log: Logger + + public init(accessLevel: AccessLevelMode = .default, logLevel: LogLevel = .info) { + self.accessLevel = accessLevel + self.log = Logger(label: "JavaExtractDecider", logLevel: logLevel) + } + + public func shouldExtract( + decl: DeclSyntax, + in parent: ExtractedNominalType? + ) -> Bool { + // Initializers of an unspecialized generic type can't be constructed from + // Java — drop them regardless of attribute or access level. + if let parent, + decl.is(InitializerDeclSyntax.self), + parent.swiftNominal.isGeneric, + !parent.isSpecialization + { + log.trace("Skip '\(decl.qualifiedNameForDebug)': initializer of an unspecialized generic type") + return false + } + + let attrs = decl.asProtocol((any WithAttributesSyntax).self)?.attributes + if attrs?.contains(where: { $0.isJavaExport }) == true { + return true + } + if attrs?.contains(where: { $0.isJavaKitMacro }) == true { + log.trace("Skip '\(decl.qualifiedNameForDebug)': swift-java macro-wrapped Java type") + return false + } + + // Swift operators have no Java mapping + if let fn = decl.as(FunctionDeclSyntax.self) { + switch fn.name.tokenKind { + case .binaryOperator, .prefixOperator, .postfixOperator: + log.trace("Skip '\(decl.qualifiedNameForDebug)': operators are not supported on Java") + return false + default: + break + } + } + + guard let mod = decl.asProtocol((any WithModifiersSyntax).self) else { + return false + } + let ok = mod.isAtLeast(accessLevel, in: parent) + if !ok { + log.trace("Skip '\(decl.qualifiedNameForDebug)': not at least \(accessLevel)") + } + return ok + } +} diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift index 2706d8ff9..823246305 100644 --- a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + /// Detects Java method name conflicts caused by Swift overloads that differ /// only in parameter labels. When a conflict is detected, the affected methods /// get a camelCase suffix derived from their parameter labels (e.g. `takeValueA`, @@ -21,20 +23,20 @@ package struct JavaIdentifierFactory { package init() {} - package init(_ methods: [ImportedFunc]) { + package init(_ methods: [ExtractedFunc]) { self.init() record(methods) } /// Analyze the given methods and record any base names that have conflicts. - private mutating func record(_ methods: [ImportedFunc]) { + private mutating func record(_ methods: [ExtractedFunc]) { // Group methods by their Java base name. - var methodsByBaseName: [String: [ImportedFunc]] = [:] + var methodsByBaseName: [String: [ExtractedFunc]] = [:] for method in methods { let baseName: String = switch method.apiKind { - case .getter, .subscriptGetter: method.javaGetterName - case .setter, .subscriptSetter: method.javaSetterName + case .getter, .subscriptGetter: method.javaGetterName! + case .setter, .subscriptSetter: method.javaSetterName! case .function, .initializer, .enumCase: method.name } methodsByBaseName[baseName, default: []].append(method) @@ -61,11 +63,11 @@ package struct JavaIdentifierFactory { } /// Compute the disambiguated Java method name for a declaration. - package func makeJavaMethodName(_ decl: ImportedFunc) -> String { + package func makeJavaMethodName(_ decl: ExtractedFunc) -> String { let baseName: String = switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName + case .getter, .subscriptGetter: decl.javaGetterName! + case .setter, .subscriptSetter: decl.javaSetterName! case .function, .initializer, .enumCase: decl.name } var methodName = baseName + paramsSuffix(decl, baseName: baseName) @@ -75,7 +77,7 @@ package struct JavaIdentifierFactory { return methodName } - private func paramsSuffix(_ decl: ImportedFunc, baseName: String) -> String { + private func paramsSuffix(_ decl: ExtractedFunc, baseName: String) -> String { switch decl.apiKind { case .getter, .subscriptGetter, .setter, .subscriptSetter: return "" diff --git a/Sources/JExtractSwiftLib/SourceDependencies.swift b/Sources/JExtractSwiftLib/JavaSourceDependencies.swift similarity index 55% rename from Sources/JExtractSwiftLib/SourceDependencies.swift rename to Sources/JExtractSwiftLib/JavaSourceDependencies.swift index a1128e51b..a9c64567f 100644 --- a/Sources/JExtractSwiftLib/SourceDependencies.swift +++ b/Sources/JExtractSwiftLib/JavaSourceDependencies.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import OrderedCollections +import SwiftExtract import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -22,50 +24,21 @@ import FoundationEssentials import Foundation #endif - -package typealias SwiftModuleName = String -package typealias SwiftTypeName = String -package typealias SwiftSourceText = String -package typealias JavaClassName = String -package typealias JavaFullyQualifiedClassName = String -package typealias JavaPackageName = String - -/// Holds the inputs jextract needs for symbol resolution but does not generate -/// bindings for. -/// -/// Two flavours of "dependency" are tracked: -/// - Wrapped Java classes referenced from this module's API (e.g. `JavaInteger`). -/// - Real Swift sources from dependency Swift modules (passed via `--depends-on`), -/// parsed once and registered as imported `SwiftModuleSymbolTable`s so that -/// cross-module type references in this module's API can resolve them. -package struct SourceDependencies { - /// Swift wrapper type names for Java classes referenced from this module's - /// API (by convention `Java`, e.g. `JavaVector`). - package var javaClasses: [SwiftTypeName] = [] - - /// Parsed Swift inputs from dependency modules, keyed by Swift module name. - package var swiftModuleInputs: [SwiftModuleName: [SwiftJavaInputFile]] = [:] - - package init() {} - - /// Names of all dependency modules with associated Swift sources. - package var swiftModuleNames: Dictionary.Keys { - swiftModuleInputs.keys - } - - /// Synthetic Swift source registering `@JavaClass public class {}` stubs - package var syntheticJavaWrappersSwiftSource: SwiftJavaInputFile? { - guard !javaClasses.isEmpty else { return nil } +extension SourceDependencies { + /// Inject synthetic `@JavaClass public class {}` stubs so the symbol + /// table can resolve Java wrapper types referenced in the Swift API. + package mutating func addJavaWrapperStubs(_ javaClasses: [SwiftTypeName]) { + guard !javaClasses.isEmpty else { return } let text = javaClasses .map { "@JavaClass public class \($0) {}" } .joined(separator: "\n") - return SwiftJavaInputFile( - syntax: Parser.parse(source: text), - path: ".swift" - ) + let stub = SwiftInputFile(syntax: SwiftParser.Parser.parse(source: text), path: ".swift") + syntheticStubInputs[""] = [stub] } + /// Load Swift sources from a dependency module described by `dependency` and + /// register them in `swiftModuleInputs` for cross-module type resolution. package mutating func loadSwiftSources(from dependency: DependencyConfig, log: Logger) { guard let moduleName = dependency.swiftModuleName else { log.debug( @@ -85,15 +58,15 @@ package struct SourceDependencies { in: dependency.swiftSourcePaths, log: log, ) - var inputs: [SwiftJavaInputFile] = [] + var inputs: [SwiftInputFile] = [] let fm = FileManager.default for url in files where canExtract(from: url) { guard let data = fm.contents(atPath: url.path), let text = String(data: data, encoding: .utf8) else { continue } - let syntax = Parser.parse(source: text) - inputs.append(SwiftJavaInputFile(syntax: syntax, path: url.path)) + let syntax = SwiftParser.Parser.parse(source: text) + inputs.append(SwiftInputFile(syntax: syntax, path: url.path)) } if inputs.isEmpty { diff --git a/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift b/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift new file mode 100644 index 000000000..5b60fd324 --- /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 LogLevel: 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..feff73c5f 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 = makeSwiftJavaAnalyzer(config: config) 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/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift deleted file mode 100644 index 6afa5b385..000000000 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ /dev/null @@ -1,341 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import Foundation -import SwiftBasicFormat -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 { - static let SWIFT_INTERFACE_SUFFIX = ".swiftinterface" - - package var log: Logger - - let config: Configuration - - /// The build configuration used to resolve #if conditional compilation blocks. - let buildConfig: any BuildConfiguration - - /// The name of the Swift module being translated. - let swiftModuleName: String - - // ==== Input - - var inputs: [SwiftJavaInputFile] = [] - - /// 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] = [] - - /// Sources jextract needs for symbol resolution but does not generate bindings - /// for: wrapped Java classes plus real Swift sources from dependency modules. - /// Populated by `SwiftToJava.run` before `analyze()` runs. - package var sourceDependencies = SourceDependencies() - - // ==== Output state - - package var importedGlobalVariables: [ImportedFunc] = [] - - package var importedGlobalFuncs: [ImportedFunc] = [] - - /// A mapping from Swift type names (e.g., A.B) over to the imported nominal - /// type representation. - package var importedTypes: [String: ImportedNominalType] = [:] - - /// 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 - - var symbolTable: SwiftSymbolTable! { - lookupContext?.symbolTable - } - - public init( - config: Configuration - ) { - 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 - } - self.log = Logger(label: "translator", logLevel: config.logLevel ?? .info) - self.config = config - self.swiftModuleName = swiftModule - - if let staticBuildConfigPath = config.staticBuildConfigurationFile { - do { - let data = try Data(contentsOf: URL(fileURLWithPath: staticBuildConfigPath)) - let decoder = JSONDecoder() - self.buildConfig = try decoder.decode(StaticBuildConfiguration.self, from: data) - self.log.info("Using custom static build configuration from: \(staticBuildConfigPath)") - } catch { - fatalError("Failed to load static build configuration from '\(staticBuildConfigPath)': \(error)") - } - } else { - self.buildConfig = .jextractDefault - } - } -} - -// ===== -------------------------------------------------------------------------------------------------------------- -// MARK: Analysis - -extension Swift2JavaTranslator { - var result: AnalysisResult { - AnalysisResult( - importedTypes: self.importedTypes, - importedGlobalVariables: self.importedGlobalVariables, - importedGlobalFuncs: self.importedGlobalFuncs, - ) - } - - 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)) - } - - /// Convenient method for analyzing single file. - package func analyze(path: String, text: String) throws { - self.add(filePath: path, text: text) - try self.analyze() - } - - /// Analyze registered inputs. - func analyze() throws { - prepareForTranslation() - - let visitor = Swift2JavaVisitor(translator: self) - - for input in self.inputs { - log.trace("Analyzing \(input.path)") - visitor.visit(inputFile: input) - } - - // Apply any specializations registered after their target types were visited - visitor.applyPendingSpecializations() - - self.visitFoundationDeclsIfNeeded(with: visitor) - } - - private func visitFoundationDeclsIfNeeded(with visitor: Swift2JavaVisitor) { - // 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 - // Data also emits DataProtocol, etc. - struct FoundationTypeGroup { - let candidates: [SwiftKnownTypeDeclKind] - let fakeSourceFilePath: String - } - let groups: [[FoundationTypeGroup]] = [ - [ - .init( - candidates: [.foundationData, .essentialsData], - fakeSourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift", - ), - .init( - candidates: [.foundationDataProtocol, .essentialsDataProtocol], - fakeSourceFilePath: "Foundation/FAKE_FOUNDATION_DATAPROTOCOL.swift", - ), - ], - [ - .init( - candidates: [.foundationDate, .essentialsDate], - fakeSourceFilePath: "Foundation/FAKE_FOUNDATION_DATE.swift", - ) - ], - [ - .init( - candidates: [.foundationUUID, .essentialsUUID], - fakeSourceFilePath: "Foundation/FAKE_FOUNDATION_UUID.swift", - ) - ], - ] - - for group in groups { - let resolved: [(primary: SwiftNominalTypeDeclaration, source: String, candidates: [SwiftNominalTypeDeclaration])] = - group.compactMap { type in - let candidates = type.candidates.compactMap { self.symbolTable[$0] } - guard let primary = candidates.first else { - return nil - } - return (primary, type.fakeSourceFilePath, candidates) - } - guard !resolved.isEmpty else { - continue - } - - let allCandidates = resolved.flatMap(\.candidates) - let isReferenced = self.isUsing(where: { decl in - allCandidates.contains(where: { $0 === decl }) - }) - guard isReferenced else { - continue - } - - // Visit the fake source files, and register the types. - for entry in resolved { - visitor.visit( - nominalDecl: entry.primary.syntax.asNominal!, - in: nil, - sourceFilePath: entry.source, - ) - } - } - } - - package func prepareForTranslation() { - let symbolTable = SwiftSymbolTable.setup( - moduleName: self.swiftModuleName, - inputs, - config: self.config, - sourceDependencies: self.sourceDependencies, - buildConfig: self.buildConfig, - log: self.log, - ) - self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) - } - - /// Check if any of the imported decls uses a nominal declaration that satisfies - /// the given predicate. - func isUsing(where predicate: (SwiftNominalTypeDeclaration) -> Bool) -> Bool { - func check(_ type: SwiftType) -> Bool { - switch type { - case .nominal(let nominal): - if nominal.genericArguments.contains(where: check) { - return true - } - return predicate(nominal.nominalTypeDecl) - case .tuple(let tuple): - return tuple.contains(where: { check($0.type) }) - case .function(let fn): - return check(fn.resultType) || fn.parameters.contains(where: { check($0.type) }) - case .metatype(let ty): - return check(ty) - case .existential(let ty), .opaque(let ty): - return check(ty) - case .composite(let types): - return types.contains(where: check) - case .genericParameter: - return false - } - } - - func check(_ fn: ImportedFunc) -> Bool { - if check(fn.functionSignature.result.type) { - return true - } - if fn.functionSignature.parameters.contains(where: { check($0.type) }) { - return true - } - return false - } - - if self.importedGlobalFuncs.contains(where: check) { - return true - } - if self.importedGlobalVariables.contains(where: check) { - return true - } - for importedType in self.importedTypes.values { - if importedType.initializers.contains(where: check) { - return true - } - if importedType.methods.contains(where: check) { - return true - } - if importedType.variables.contains(where: check) { - return true - } - } - return false - } -} - -// ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Type translation -extension Swift2JavaTranslator { - /// 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) { - return nil - } - - guard let nominal = symbolTable.lookupType(nominalNode.name.text, parent: parent?.swiftNominal) else { - return nil - } - return self.importedNominalType(nominal) - } - - /// Try to resolve the given nominal type node into its imported representation. - func importedNominalType( - _ typeNode: TypeSyntax - ) -> ImportedNominalType? { - guard let swiftType = try? SwiftType(typeNode, lookupContext: lookupContext) else { - return nil - } - guard let swiftNominalDecl = swiftType.asNominalTypeDeclaration else { - return nil - } - - let isFromThisModule = swiftNominalDecl.moduleName == self.swiftModuleName - let isFromStubbedModule = config.hasImportedModuleStub(moduleOfNominal: swiftNominalDecl.moduleName) - let isFromDependencyModule = sourceDependencies.swiftModuleNames.contains(swiftNominalDecl.moduleName) - guard isFromThisModule || isFromStubbedModule || isFromDependencyModule else { - return nil - } - - guard swiftNominalDecl.syntax.shouldExtract(config: config, log: log, in: nil) else { - return nil - } - - return importedNominalType(swiftNominalDecl) - } - - func importedNominalType(_ nominal: SwiftNominalTypeDeclaration) -> ImportedNominalType? { - let fullName = nominal.qualifiedName - - guard shouldJExtractType(qualifiedName: fullName, config: config) else { - log.debug("Skip import '\(fullName)': filtered by swiftFilterInclude/swiftFilterExclude") - return nil - } - - if let alreadyImported = importedTypes[fullName] { - return alreadyImported - } - - let importedNominal = try? ImportedNominalType(swiftNominal: nominal, lookupContext: lookupContext) - - importedTypes[fullName] = importedNominal - return importedNominal - } -} - -// ==== ----------------------------------------------------------------------- -// MARK: Errors - -public struct Swift2JavaTranslatorError: Error { - let message: String - - public init(message: String) { - self.message = message - } -} diff --git a/Sources/JExtractSwiftLib/SwiftKit+Printing.swift b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift index f5ac6a4a4..881d33899 100644 --- a/Sources/JExtractSwiftLib/SwiftKit+Printing.swift +++ b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift @@ -12,8 +12,10 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import Foundation import SwiftBasicFormat +import SwiftExtract import SwiftParser import SwiftSyntax @@ -21,7 +23,7 @@ import SwiftSyntax package struct SwiftKitPrinting { /// Forms syntax for a Java call to a swiftkit exposed function. - static func renderCallGetSwiftType(module: String, nominal: ImportedNominalType) -> String { + static func renderCallGetSwiftType(module: String, nominal: ExtractedNominalType) -> String { """ SwiftRuntime.swiftjava.getType("\(module)", "\(nominal.swiftNominal.qualifiedName)") """ @@ -51,7 +53,7 @@ extension SwiftKitPrinting { } extension SwiftKitPrinting.Names { - static func getType(module: String, nominal: ImportedNominalType) -> String { + static func getType(module: String, nominal: ExtractedNominalType) -> String { "swiftjava_getType_\(module)_\(nominal.swiftNominal.qualifiedTypeName.fullFlatName)" } diff --git a/Sources/JExtractSwiftLib/SwiftKnownTypes+Java.swift b/Sources/JExtractSwiftLib/SwiftKnownTypes+Java.swift new file mode 100644 index 000000000..8f1ba13bf --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftKnownTypes+Java.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract + +extension SwiftNominalTypeDeclaration { + /// True if this is swift-java's runtime `SwiftJavaError` type, which jextract + /// maps to a thrown Java exception rather than an ordinary wrapped nominal + package var isSwiftJavaErrorType: Bool { + moduleName == "SwiftRuntimeFunctions" && name == "SwiftJavaError" + } +} + +extension SwiftKnownTypeDeclKind { + /// Indicates whether this known type is translated by `wrap-java` + /// into the same type as `jextract`. + /// + /// This means we do not have to perform any mapping when passing + /// this type between jextract and wrap-java + package var isDirectlyTranslatedToWrapJava: Bool { + switch self { + case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string, + .void: + return true + default: + return false + } + } +} diff --git a/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift new file mode 100644 index 000000000..c035bae01 --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftSyntax + +/// Names of the JavaKit macros that wrap a Java class/interface/member from +/// Swift (Java -> Swift direction). A nominal carrying any of these is +/// skipped during jextract because the binding goes the other way. +/// +/// `@JavaExport` is intentionally NOT here — it forces export of a Swift +/// type to Java (Swift -> Java direction) and uses a separate predicate. +let KnownJavaKitMacroNames: [String] = [ + "JavaClass", + "JavaInterface", + "JavaField", + "JavaStaticField", + "JavaMethod", + "JavaStaticMethod", + "JavaImplementation", +] + +extension AttributeListSyntax.Element { + /// Whether this node carries one of the JavaKit wrapping macros (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 isJavaKitMacro: 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 } + return KnownJavaKitMacroNames.contains(attrName) + } + + /// 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: \.isJavaKitMacro) + } +} diff --git a/Sources/JExtractSwiftLib/SymbolTable+Printing.swift b/Sources/JExtractSwiftLib/SymbolTable+Printing.swift new file mode 100644 index 000000000..0f28ebffb --- /dev/null +++ b/Sources/JExtractSwiftLib/SymbolTable+Printing.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CodePrinting +import SwiftExtract + +extension SwiftSymbolTable { + package func printImportedModules(_ printer: inout CodePrinter) { + let mainSymbolSourceModules = Set( + self.importedModules.values.filter { $0.alternativeModules?.isMainSourceOfSymbols ?? false }.map(\.moduleName) + ) + + for module in self.importedModules.keys.sorted() { + guard module != "Swift" else { + continue + } + + // Synthetic stub modules (e.g. ) exist purely for + // symbol-table resolution; they are not real Swift modules and must + // not be emitted as `import` statements. + guard !self.syntheticImportedModuleNames.contains(module) else { + continue + } + + guard let alternativeModules = self.importedModules[module]?.alternativeModules else { + printer.print("import \(module)") + continue + } + + // Only the main source of symbols emits the conditional import block. + // Secondary modules (e.g. FoundationEssentials when Foundation is the main source) + // are skipped when their main source is already present, because the main source's + // block already covers the import. If no main source is present, fall back to a + // plain import so the module is still imported. + guard alternativeModules.isMainSourceOfSymbols else { + if mainSymbolSourceModules.isDisjoint(with: alternativeModules.moduleNames) { + printer.print("import \(module)") + } + continue + } + + var importGroups: [String: [String]] = [:] + for name in alternativeModules.moduleNames { + guard let otherModule = self.importedModules[name] else { continue } + + let groupKey = otherModule.requiredAvailablityOfModuleWithName ?? otherModule.moduleName + importGroups[groupKey, default: []].append(otherModule.moduleName) + } + + for (index, group) in importGroups.keys.sorted().enumerated() { + if index > 0 && importGroups.keys.count > 1 { + printer.print("#elseif canImport(\(group))") + } else { + printer.print("#if canImport(\(group))") + } + + for groupModule in importGroups[group] ?? [] { + printer.print("import \(groupModule)") + } + } + + if importGroups.keys.isEmpty { + printer.print("import \(module)") + } else { + printer.print("#else") + printer.print("import \(module)") + printer.print("#endif") + } + } + printer.println() + } +} diff --git a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift index ac783f5c8..5beb710e3 100644 --- a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift +++ b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift @@ -12,18 +12,20 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + /// Registry of names we've already emitted as @_cdecl and must be kept unique. /// In order to avoid duplicate symbols, the registry can append some unique identifier to duplicated names package struct ThunkNameRegistry { /// Maps base names such as "swiftjava_Module_Type_method_a_b_c" to the number of times we've seen them. /// This is used to de-duplicate symbols as we emit them. - private var registry: [ImportedFunc: String] = [:] + private var registry: [ExtractedFunc: String] = [:] private var duplicateNames: [String: Int] = [:] package init() {} package mutating func functionThunkName( - decl: ImportedFunc, + decl: ExtractedFunc, file: String = #fileID, line: UInt = #line ) -> String { diff --git a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift index bab21fdd9..a6b01e08b 100644 --- a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift +++ b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift @@ -13,12 +13,13 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftSyntax enum TranslatedDocumentation { static func printDocumentation( - importedFunc: ImportedFunc, + importedFunc: ExtractedFunc, translatedDecl: FFMSwift2JavaGenerator.TranslatedFunctionDecl, config: Configuration, in printer: inout CodePrinter @@ -38,7 +39,7 @@ enum TranslatedDocumentation { } static func printDocumentation( - importedFunc: ImportedFunc, + importedFunc: ExtractedFunc, translatedDecl: JNISwift2JavaGenerator.TranslatedFunctionDecl, config: Configuration, in printer: inout CodePrinter diff --git a/Sources/SwiftExtract/AnalysisResult.swift b/Sources/SwiftExtract/AnalysisResult.swift new file mode 100644 index 000000000..4b39506de --- /dev/null +++ b/Sources/SwiftExtract/AnalysisResult.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// The complete analysis result of the analyzed Swift inputs. +/// This is used as the primary input to source generators, which then act on the analyzed decls. +public struct AnalysisResult { + public var extractedTypes: [SwiftTypeName: ExtractedNominalType] + public var extractedGlobalVariables: [ExtractedFunc] + public var extractedGlobalFuncs: [ExtractedFunc] + + public init( + extractedTypes: [SwiftTypeName: ExtractedNominalType], + extractedGlobalVariables: [ExtractedFunc], + extractedGlobalFuncs: [ExtractedFunc] + ) { + self.extractedTypes = extractedTypes + self.extractedGlobalVariables = extractedGlobalVariables + self.extractedGlobalFuncs = extractedGlobalFuncs + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/Collection+Extensions.swift b/Sources/SwiftExtract/Convenience/Collection+Extensions.swift similarity index 91% rename from Sources/JExtractSwiftLib/Convenience/Collection+Extensions.swift rename to Sources/SwiftExtract/Convenience/Collection+Extensions.swift index 4286d9e16..e04341368 100644 --- a/Sources/JExtractSwiftLib/Convenience/Collection+Extensions.swift +++ b/Sources/SwiftExtract/Convenience/Collection+Extensions.swift @@ -27,8 +27,8 @@ extension Dictionary { } extension Collection { - typealias IsLastElement = Bool - var withIsLast: any Collection<(Element, IsLastElement)> { + package typealias IsLastElement = Bool + package var withIsLast: any Collection<(Element, IsLastElement)> { var i = 1 let totalCount = self.count diff --git a/Sources/SwiftExtract/Convenience/String+Extensions.swift b/Sources/SwiftExtract/Convenience/String+Extensions.swift new file mode 100644 index 000000000..443984b4e --- /dev/null +++ b/Sources/SwiftExtract/Convenience/String+Extensions.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension String { + + package var firstCharacterUppercased: String { + guard let f = first else { + return self + } + + return "\(f.uppercased())\(String(dropFirst()))" + } + + package var firstCharacterLowercased: String { + guard let f = first else { + return self + } + + return "\(f.lowercased())\(String(dropFirst()))" + } + + /// If the string ends with `.swift`, return it without that suffix; + /// otherwise return self unchanged + package func dropSwiftFileSuffix() -> String { + if hasSuffix(".swift") { + return String(dropLast(".swift".count)) + } + return self + } + + /// Unescapes the name if it is surrounded by backticks. + package var unescapedSwiftName: String { + if count >= 2 && hasPrefix("`") && hasSuffix("`") { + return String(dropFirst().dropLast()) + } + return self + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift similarity index 85% rename from Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift rename to Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift index 76e5ba688..e54c1c3eb 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 @@ -121,39 +121,35 @@ extension WithModifiersSyntax { modifier.isAtLeastInternal } } -} -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 declaration's access level is at least `mode`. The `parent` + /// is consulted only by the `.public` mode (so e.g. members of a public + /// protocol are themselves treated as public). + public func isAtLeast( + _ mode: AccessLevelMode, + in parent: ExtractedNominalType? + ) -> Bool { + self.isAtLeast(mode, in: parent?.swiftNominal.syntax) } - /// 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" + /// Lower-level overload taking the parent's syntax node directly. Used by + /// the analyzer; downstream `ExtractDecider`s use the `ExtractedNominalType` + /// overload above. + package func isAtLeast( + _ mode: AccessLevelMode, + in parent: NominalTypeDeclSyntaxNode? + ) -> Bool { + switch mode { + case .public: return self.isPublic(in: parent) + case .package: return self.isAtLeastPackage + case .internal: return self.isAtLeastInternal + } } } extension DeclSyntaxProtocol { /// Find inner most "decl" node in ancestors. - var ancestorDecl: DeclSyntax? { + public var ancestorDecl: DeclSyntax? { var node: Syntax = Syntax(self) while let parent = node.parent { if let decl = parent.as(DeclSyntax.self) { @@ -165,7 +161,7 @@ extension DeclSyntaxProtocol { } /// Declaration name primarily for debugging. - var nameForDebug: String { + public var nameForDebug: String { switch DeclSyntax(self).as(DeclSyntaxEnum.self) { case .accessorDecl(let node): node.accessorSpecifier.text @@ -233,7 +229,7 @@ extension DeclSyntaxProtocol { } /// Qualified declaration name primarily for debugging. - var qualifiedNameForDebug: String { + public var qualifiedNameForDebug: String { if let parent = ancestorDecl { parent.qualifiedNameForDebug + "." + nameForDebug } else { @@ -242,7 +238,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..2e3a02302 --- /dev/null +++ b/Sources/SwiftExtract/ExtractDecider.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Decides if a declaration should be extracted. This logic is specific for every output language and should handle things like access control as well as supported features when deciding of to import or skip a decl +/// +/// `SwiftExtract` itself is language-neutral and applies no extraction +/// policy of its own beyond resolving the declaration. The decider is the +/// single place that decides, for a given decl, whether the analyzer should +/// import it. That covers the access-level filter, attribute rules +/// (e.g. Java's `@JavaExport` / `@JavaClass` family), and target-specific +/// quirks (e.g. skipping Swift operators when the language can't render +/// them). When no decider is provided, the analyzer falls back to +/// `DefaultAccessLevelExtractDecider`, which only enforces the configured +/// access level +public protocol ExtractDecider { + /// Decide whether `decl` should be extracted. + /// + /// Implementations should emit a `.trace` line on every skip path (using + /// their own logger) so users can see why a decl was dropped. + /// + /// - Parameters: + /// - decl: the declaration being considered + /// - parent: the nominal type containing `decl`, when applicable + func shouldExtract( + decl: DeclSyntax, + in parent: ExtractedNominalType? + ) -> Bool +} + +/// Minimal `ExtractDecider` that enforces only the configured access-level +/// filter. Used by `SwiftAnalyzer` when no decider is supplied +public struct DefaultAccessLevelExtractDecider: ExtractDecider { + public let accessLevel: AccessLevelMode + let log: Logger + + public init(accessLevel: AccessLevelMode, logLevel: LogLevel = .info) { + self.accessLevel = accessLevel + self.log = Logger(label: "DefaultAccessLevelExtractDecider", logLevel: logLevel) + } + + public func shouldExtract( + decl: DeclSyntax, + in parent: ExtractedNominalType? + ) -> Bool { + guard let mod = decl.asProtocol((any WithModifiersSyntax).self) else { + return false + } + let ok = mod.isAtLeast(accessLevel, in: parent) + if !ok { + log.trace("Skip '\(decl.qualifiedNameForDebug)': not at least \(accessLevel)") + } + return ok + } +} diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift similarity index 64% rename from Sources/JExtractSwiftLib/ImportedDecls.swift rename to Sources/SwiftExtract/ExtractedDecls.swift index 1f6b80d28..cdfe8185a 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -14,10 +14,10 @@ import SwiftSyntax -/// Any imported (Swift) declaration -protocol ImportedDecl: AnyObject {} +/// Any extracted Swift declaration +public protocol ExtractedSwiftDecl: AnyObject {} -package enum SwiftAPIKind: Equatable { +public enum SwiftAPIKind: Equatable { case function case initializer case getter @@ -28,57 +28,56 @@ package enum SwiftAPIKind: Equatable { } /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been -/// imported and is being translated into Java. +/// extracted by the analyzer for a downstream language target. /// /// When `base` is non-nil, this is a specialization of a generic type /// (e.g. `FishBox` specializing `Box` with `Element` = `Fish`). /// The specialization delegates its member collections to the base type /// so that extensions discovered later are visible through all specializations. -package final class ImportedNominalType: ImportedDecl { - let swiftNominal: SwiftNominalTypeDeclaration +public final class ExtractedNominalType: ExtractedSwiftDecl { + public let swiftNominal: SwiftNominalTypeDeclaration - /// If this type is a specialization (FishTank), then this points at the Tank base type of the specialization. - /// His allows simplified - package let specializationBaseType: ImportedNominalType? + /// If this type is a specialization (FishTank), it points at the Tank base type of the specialization + public let specializationBaseType: ExtractedNominalType? // The short path from module root to the file in which this nominal was originally declared. // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. - package var sourceFilePath: String { + public var sourceFilePath: String { self.swiftNominal.sourceFilePath } // Backing storage for member collections - package var initializers: [ImportedFunc] = [] - package var methods: [ImportedFunc] = [] - package var variables: [ImportedFunc] = [] - package var cases: [ImportedEnumCase] = [] - var inheritedTypes: [SwiftType] - package var parent: SwiftNominalTypeDeclaration? + public var initializers: [ExtractedFunc] = [] + public var methods: [ExtractedFunc] = [] + public var variables: [ExtractedFunc] = [] + public var cases: [ExtractedEnumCase] = [] + public var inheritedTypes: [SwiftType] + public var parent: SwiftNominalTypeDeclaration? /// The Swift base type name, e.g. "Box" — always the unparameterized name - package var baseTypeName: String { swiftNominal.qualifiedName } + public var baseTypeName: String { swiftNominal.qualifiedName } - /// The specialized/Java-facing name, e.g. "FishBox" — nil for base types - package private(set) var specializedTypeName: String? + /// The specialized output-facing name, e.g. "FishBox" — nil for base types + public private(set) var specializedTypeName: String? /// Whether this type is a specialization of a generic type - package var isSpecialization: Bool { specializationBaseType != nil } + public var isSpecialization: Bool { specializationBaseType != nil } /// Generic parameter names (e.g. ["Element"] for Box). Empty for non-generic types - package var genericParameterNames: [String] { + public var genericParameterNames: [String] { swiftNominal.genericParameters.map(\.name) } /// Maps generic parameter -> concrete type argument. Empty for unspecialized types /// e.g. {"Element": "Fish"} for FishBox - package var genericArguments: [String: String] = [:] + public var genericArguments: [String: String] = [:] /// True when all generic parameters have corresponding arguments - package var isFullySpecialized: Bool { + public var isFullySpecialized: Bool { !genericParameterNames.isEmpty && genericParameterNames.allSatisfy { genericArguments.keys.contains($0) } } - init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { + public init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { self.swiftNominal = swiftNominal self.specializationBaseType = nil self.inheritedTypes = @@ -90,7 +89,7 @@ package final class ImportedNominalType: ImportedDecl { } /// Init for creating a specialization - private init(base: ImportedNominalType, specializedTypeName: String, genericArguments: [String: String]) { + private init(base: ExtractedNominalType, specializedTypeName: String, genericArguments: [String: String]) { self.swiftNominal = base.swiftNominal self.specializationBaseType = base @@ -119,41 +118,54 @@ 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 simple Java class name (no qualification) for file naming purposes - var effectiveJavaSimpleName: String { - specializedTypeName ?? swiftNominal.name + /// The effective Swift-side type name used as a registration key in the + /// analyzer's type table - "FishBox" for a specialization registered via + /// `typealias FishBox = Box`, the qualified base name (e.g. "Box") + /// for a non-specialized type. Output-language-facing names live on the + /// downstream code generator (e.g. JExtractSwiftLib's `effectiveJavaName`). + public var effectiveTypeName: String { + effectiveOutputTypeName.fullName } /// The Swift type for thunk generation — "Box" for specialized, "Box" for base /// Computed from baseTypeName + genericArguments - var effectiveSwiftTypeName: String { + public var effectiveSwiftTypeName: String { guard !genericArguments.isEmpty else { return baseTypeName } let orderedArgs = genericParameterNames.compactMap { genericArguments[$0] } guard !orderedArgs.isEmpty else { return baseTypeName } return "\(baseTypeName)<\(orderedArgs.joined(separator: ", "))>" } - var qualifiedName: String { + public var qualifiedName: String { self.swiftNominal.qualifiedName } - /// The Java generic clause, e.g. "" for generic base types, "" for specialized or non-generic - var javaGenericClause: String { + /// The attribute list on this type's declaration (e.g. `@resultBuilder`), + /// for language targets that key behavior off attributes. + /// Mirrors `ExtractedFunc.swiftDecl` being public for the function case. + public var declAttributes: AttributeListSyntax { + swiftNominal.syntax.attributes + } + + /// The declaration-group syntax for this type (protocol/struct/class/enum/ + /// actor), for language targets that need to inspect members or clauses the + /// neutral model doesn't surface (e.g. a protocol's primary associated types). + public var declGroupSyntax: any DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax { + swiftNominal.syntax + } + + /// The output generic clause, e.g. "" for generic base types, "" for specialized or non-generic + public var outputGenericClause: String { if isSpecialization { "" } else if genericParameterNames.isEmpty { @@ -164,10 +176,10 @@ package final class ImportedNominalType: ImportedDecl { } /// Create a specialized version of this generic type - package func specialize( + public func specialize( as specializedName: String, with substitutions: [String: String], - ) throws -> ImportedNominalType { + ) throws -> ExtractedNominalType { guard !genericParameterNames.isEmpty else { throw SpecializationError( message: "Unable to specialize non-generic type '\(baseTypeName)' as '\(specializedName)'" @@ -179,7 +191,7 @@ package final class ImportedNominalType: ImportedDecl { message: "Missing type arguments for: \(missingParams) when specializing \(baseTypeName) as \(specializedName)" ) } - return ImportedNominalType( + return ExtractedNominalType( base: self, specializedTypeName: specializedName, genericArguments: substitutions, @@ -187,14 +199,14 @@ package final class ImportedNominalType: ImportedDecl { } /// Checks if this type, or any of types it inherits from, conforms to the passed in protocol. - package func conformsTo(_ protocolName: String, in importedTypes: [String: ImportedNominalType]) -> Bool { + public func conformsTo(_ protocolName: String, in extractedTypes: [SwiftTypeName: ExtractedNominalType]) -> Bool { var visited: Set = [] - var queue: [ImportedNominalType] = [self] + var queue: [ExtractedNominalType] = [self] while let current = queue.popLast() { for inherited in current.inheritedTypes { guard let name = inherited.asNominalTypeDeclaration?.name else { continue } if name == protocolName { return true } - if let next = importedTypes[name], visited.insert(ObjectIdentifier(next)).inserted { + if let next = extractedTypes[name], visited.insert(ObjectIdentifier(next)).inserted { queue.append(next) } } @@ -203,30 +215,30 @@ package final class ImportedNominalType: ImportedDecl { } } -struct SpecializationError: Error { - let message: String +public struct SpecializationError: Error { + public let message: String } -public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { +public final class ExtractedEnumCase: ExtractedSwiftDecl, CustomStringConvertible { /// The case name public let name: String /// The enum parameters - let parameters: [SwiftEnumCaseParameter] + public let parameters: [SwiftEnumCaseParameter] - let swiftDecl: any DeclSyntaxProtocol + public let swiftDecl: any DeclSyntaxProtocol - let enumType: SwiftNominalType + public let enumType: SwiftNominalType /// A function that represents the Swift static "initializer" for cases - let caseFunction: ImportedFunc + public let caseFunction: ExtractedFunc - init( + public init( name: String, parameters: [SwiftEnumCaseParameter], swiftDecl: any DeclSyntaxProtocol, enumType: SwiftNominalType, - caseFunction: ImportedFunc, + caseFunction: ExtractedFunc, ) { self.name = name self.parameters = parameters @@ -237,7 +249,7 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { public var description: String { """ - ImportedEnumCase { + ExtractedEnumCase { name: \(name), parameters: \(parameters), swiftDecl: \(swiftDecl), @@ -247,8 +259,8 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { """ } - func clone(for parent: SwiftType) -> ImportedEnumCase { - ImportedEnumCase( + public func clone(for parent: SwiftType) -> ExtractedEnumCase { + ExtractedEnumCase( name: name, parameters: parameters, swiftDecl: swiftDecl, @@ -258,16 +270,16 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { } } -extension ImportedEnumCase: Hashable { +extension ExtractedEnumCase: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } - public static func == (lhs: ImportedEnumCase, rhs: ImportedEnumCase) -> Bool { + public static func == (lhs: ExtractedEnumCase, rhs: ExtractedEnumCase) -> Bool { lhs === rhs } } -public final class ImportedFunc: ImportedDecl, CustomStringConvertible { +public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible { /// Swift module name (e.g. the target name where a type or function was declared) public let module: String @@ -277,26 +289,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 } @@ -304,9 +316,7 @@ 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. + /// this will contain that declaration's extracted name. public var hasParent: Bool { functionSignature.selfParameter != nil } /// A display name to use to refer to the Swift declaration with its @@ -332,15 +342,15 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { return prefix + context + self.name } - var isThrowing: Bool { + public var isThrowing: Bool { self.functionSignature.effectSpecifiers.contains(.throws) } - var isAsync: Bool { + public var isAsync: Bool { self.functionSignature.isAsync } - init( + public init( module: String, swiftDecl: any DeclSyntaxProtocol, name: String, @@ -356,7 +366,7 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { public var description: String { """ - ImportedFunc { + ExtractedFunc { apiKind: \(apiKind) module: \(module) name: \(name) @@ -365,11 +375,11 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { """ } - func clone(for parent: SwiftType) -> ImportedFunc { + public func clone(for parent: SwiftType) -> ExtractedFunc { var functionSignature = functionSignature assert(functionSignature.selfParameter?.selfType != nil) functionSignature.selfParameter?.selfType = parent - return ImportedFunc( + return ExtractedFunc( module: module, swiftDecl: swiftDecl, name: name, @@ -379,48 +389,20 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { } } -extension ImportedFunc: Hashable { +extension ExtractedFunc: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } - public static func == (lhs: ImportedFunc, rhs: ImportedFunc) -> Bool { + public static func == (lhs: ExtractedFunc, rhs: ExtractedFunc) -> Bool { lhs === rhs } } -extension ImportedFunc { - var javaGetterName: String { - let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool - - if !returnsBoolean { - return "get\(self.name.firstCharacterUppercased)" - } else if !self.name.hasJavaBooleanNamingConvention { - return "is\(self.name.firstCharacterUppercased)" - } else { - return self.name - } - } - - var javaSetterName: String { - let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool - - // If the variable is already named "isX", then we make - // the setter "setX" to match beans spec. - if isBooleanSetter && self.name.hasJavaBooleanNamingConvention { - // Safe to force unwrap due to `hasJavaBooleanNamingConvention` check. - let propertyName = self.name.split(separator: "is", maxSplits: 1).last! - return "set\(propertyName)" - } else { - return "set\(self.name.firstCharacterUppercased)" - } - } -} - -extension ImportedNominalType: Hashable { +extension ExtractedNominalType: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } - public static func == (lhs: ImportedNominalType, rhs: ImportedNominalType) -> Bool { + public static func == (lhs: ExtractedNominalType, rhs: ExtractedNominalType) -> Bool { lhs === rhs } } diff --git a/Sources/JExtractSwiftLib/Logger.swift b/Sources/SwiftExtract/Logger.swift similarity index 70% rename from Sources/JExtractSwiftLib/Logger.swift rename to Sources/SwiftExtract/Logger.swift index 5c4267830..c81974019 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,17 +12,16 @@ // //===----------------------------------------------------------------------===// -import ArgumentParser import Foundation -import SwiftJavaConfigurationShared +@_exported import SwiftExtractConfigurationShared import SwiftSyntax // Placeholder for some better logger, we could depend on swift-log public struct Logger { public var label: String - public var logLevel: Logger.Level + public var logLevel: LogLevel - public init(label: String, logLevel: Logger.Level) { + public init(label: String, logLevel: LogLevel) { self.label = label self.logLevel = logLevel } @@ -112,44 +111,3 @@ public struct Logger { print("[trace][\(file):\(line)](\(function)) \(message()) \(metadataString)") } } - -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 { - case .trace: - return 0 - case .debug: - return 1 - case .info: - return 2 - case .notice: - return 3 - case .warning: - return 4 - case .error: - return 5 - case .critical: - return 6 - } - } -} - -extension Logger.Level: Comparable { - public static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool { - lhs.naturalIntegralValue < rhs.naturalIntegralValue - } -} 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 73% rename from Sources/JExtractSwiftLib/Swift2JavaVisitor.swift rename to Sources/SwiftExtract/SwiftAnalysisVisitor.swift index ed6b4ed8b..3c9e79d1a 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,22 +13,22 @@ //===----------------------------------------------------------------------===// import Foundation +import Logging import SwiftIfConfig -import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -final class Swift2JavaVisitor { - let translator: Swift2JavaTranslator - var config: Configuration { - self.translator.config +final class SwiftAnalysisVisitor { + let analyzer: SwiftAnalyzer + var config: any SwiftExtractConfiguration { + self.analyzer.config } - init(translator: Swift2JavaTranslator) { - self.translator = translator + init(analyzer: SwiftAnalyzer) { + self.analyzer = analyzer } - var log: Logger { translator.log } + var log: Logger { analyzer.log } /// Constrained extensions deferred until specializations are applied private struct DeferredConstrainedExtension { @@ -38,7 +38,7 @@ final class Swift2JavaVisitor { } private var deferredConstrainedExtensions: [DeferredConstrainedExtension] = [] - func visit(inputFile: SwiftJavaInputFile) { + func visit(inputFile: SwiftInputFile) { let node = inputFile.syntax for codeItem in node.statements { if let declNode = codeItem.item.as(DeclSyntax.self) { @@ -47,7 +47,7 @@ final class Swift2JavaVisitor { } } - func visit(decl node: DeclSyntax, in parent: ImportedNominalType?, sourceFilePath: String) { + func visit(decl node: DeclSyntax, in parent: ExtractedNominalType?, sourceFilePath: String) { switch node.as(DeclSyntaxEnum.self) { case .actorDecl(let node): self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) @@ -86,24 +86,24 @@ final class Swift2JavaVisitor { func visit( nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String, ) { - guard let importedNominalType = translator.importedNominalType(node, parent: parent) else { + guard let extractedNominalType = analyzer.extractedNominalType(node, parent: parent) else { return } // Check if there's a specialization entry for this type - applySpecialization(to: importedNominalType) + applySpecialization(to: extractedNominalType) for memberItem in node.memberBlock.members { - self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) + self.visit(decl: memberItem.decl, in: extractedNominalType, sourceFilePath: sourceFilePath) } } func visit( enumDecl node: EnumDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String, ) { self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) @@ -113,14 +113,14 @@ final class Swift2JavaVisitor { func visit( extensionDecl node: ExtensionDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String, ) { guard parent == nil else { // 'extension' in a nominal type is invalid. Ignore return } - guard let importedNominalType = translator.importedNominalType(node.extendedType) else { + guard let extractedNominalType = analyzer.extractedNominalType(node.extendedType) else { return } @@ -133,12 +133,12 @@ final class Swift2JavaVisitor { guard !constraints.isEmpty else { // The extension is unconstrained: add to the base type (visible through all specializations) - importedNominalType.inheritedTypes += + extractedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap { - try? SwiftType($0.type, lookupContext: translator.lookupContext) + try? SwiftType($0.type, lookupContext: analyzer.lookupContext) } ?? [] for memberItem in node.memberBlock.members { - self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) + self.visit(decl: memberItem.decl, in: extractedNominalType, sourceFilePath: sourceFilePath) } return } @@ -155,7 +155,7 @@ final class Swift2JavaVisitor { } let matchingSpecializations = findMatchingSpecializations( - extendedType: importedNominalType, + extendedType: extractedNominalType, whereConstraints: constraints, ) if matchingSpecializations.isEmpty { @@ -176,21 +176,13 @@ final class Swift2JavaVisitor { func visit( functionDecl node: FunctionDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, in: typeContext, decider: analyzer.extractDecider) else { return } - switch node.name.tokenKind { - case .binaryOperator, .prefixOperator, .postfixOperator: - self.log.debug("Skip importing: '\(node.qualifiedNameForDebug)'; Operators are not supported.") - return - default: - break - } - self.log.debug("Import function: '\(node.qualifiedNameForDebug)'") let signature: SwiftFunctionSignature @@ -198,36 +190,36 @@ final class Swift2JavaVisitor { signature = try SwiftFunctionSignature( node, enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext, + lookupContext: analyzer.lookupContext, ) } catch { self.log.warning( Self.makeMissingTypeMessage( - "Failed to import: '\(node.qualifiedNameForDebug)' in module '\(translator.swiftModuleName)'; \(error)" + "Failed to import: '\(node.qualifiedNameForDebug)' in module '\(analyzer.swiftModuleName)'; \(error)" ) ) return } - let imported = ImportedFunc( - module: translator.swiftModuleName, + let extracted = ExtractedFunc( + module: analyzer.swiftModuleName, swiftDecl: node, name: node.name.text.unescapedSwiftName, apiKind: .function, functionSignature: signature, ) - log.debug("Record imported method \(node.qualifiedNameForDebug)") + log.debug("Record extracted method \(node.qualifiedNameForDebug)") if let typeContext { - typeContext.methods.append(imported) + typeContext.methods.append(extracted) } else { - translator.importedGlobalFuncs.append(imported) + analyzer.extractedGlobalFuncs.append(extracted) } } func visit( enumCaseDecl node: EnumCaseDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, ) { guard let typeContext else { self.log.info("Enum case must be within a current type; \(node)") @@ -239,25 +231,25 @@ final class Swift2JavaVisitor { self.log.debug("Import case \(caseElement.name) of enum \(node.qualifiedNameForDebug)") let parameters = try caseElement.parameterClause?.parameters.map { - try SwiftEnumCaseParameter($0, lookupContext: translator.lookupContext) + try SwiftEnumCaseParameter($0, lookupContext: analyzer.lookupContext) } let signature = try SwiftFunctionSignature( caseElement, enclosingType: typeContext.swiftType, - lookupContext: translator.lookupContext, + lookupContext: analyzer.lookupContext, ) let caseName = caseElement.name.text.unescapedSwiftName - let caseFunction = ImportedFunc( - module: translator.swiftModuleName, + let caseFunction = ExtractedFunc( + module: analyzer.swiftModuleName, swiftDecl: node, name: caseName, apiKind: .enumCase, functionSignature: signature, ) - let importedCase = ImportedEnumCase( + let extractedCase = ExtractedEnumCase( name: caseName, parameters: parameters ?? [], swiftDecl: node, @@ -265,12 +257,12 @@ final class Swift2JavaVisitor { caseFunction: caseFunction, ) - typeContext.cases.append(importedCase) + typeContext.cases.append(extractedCase) } } catch { self.log.warning( Self.makeMissingTypeMessage( - "Failed to import: \(node.qualifiedNameForDebug) in module '\(translator.swiftModuleName)'; \(error)" + "Failed to import: \(node.qualifiedNameForDebug) in module '\(analyzer.swiftModuleName)'; \(error)" ) ) } @@ -278,10 +270,10 @@ final class Swift2JavaVisitor { func visit( variableDecl node: VariableDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -314,7 +306,7 @@ final class Swift2JavaVisitor { } catch { self.log.warning( Self.makeMissingTypeMessage( - "Failed to import: \(node.qualifiedNameForDebug) in module '\(translator.swiftModuleName)'; \(error)" + "Failed to import: \(node.qualifiedNameForDebug) in module '\(analyzer.swiftModuleName)'; \(error)" ) ) } @@ -322,18 +314,13 @@ final class Swift2JavaVisitor { func visit( initializerDecl node: InitializerDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, ) { guard let typeContext else { self.log.info("Initializer must be within a current type; \(node)") return } - guard node.shouldExtract(config: config, log: log, in: typeContext) else { - return - } - - if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization { - log.debug("Skip Importing generic type initializer \(node.kind) '\(node.qualifiedNameForDebug)'") + guard node.shouldExtract(config: config, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -344,32 +331,32 @@ final class Swift2JavaVisitor { signature = try SwiftFunctionSignature( node, enclosingType: typeContext.swiftType, - lookupContext: translator.lookupContext, + lookupContext: analyzer.lookupContext, ) } catch { self.log.warning( Self.makeMissingTypeMessage( - "Failed to import: \(node.qualifiedNameForDebug) in module '\(translator.swiftModuleName)'; \(error)" + "Failed to import: \(node.qualifiedNameForDebug) in module '\(analyzer.swiftModuleName)'; \(error)" ) ) return } - let imported = ImportedFunc( - module: translator.swiftModuleName, + let extracted = ExtractedFunc( + module: analyzer.swiftModuleName, swiftDecl: node, name: "init", apiKind: .initializer, functionSignature: signature, ) - typeContext.initializers.append(imported) + typeContext.initializers.append(extracted) } private func visit( subscriptDecl node: SubscriptDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -400,7 +387,7 @@ final class Swift2JavaVisitor { } catch { self.log.warning( Self.makeMissingTypeMessage( - "Failed to import: \(node.qualifiedNameForDebug) in module '\(translator.swiftModuleName)'; \(error)" + "Failed to import: \(node.qualifiedNameForDebug) in module '\(analyzer.swiftModuleName)'; \(error)" ) ) } @@ -408,10 +395,10 @@ final class Swift2JavaVisitor { private func visit( ifConfigDecl node: IfConfigDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String ) { - let (clause, _) = node.activeClause(in: translator.buildConfig) + let (clause, _) = node.activeClause(in: analyzer.buildConfig) if let clause, let elements = clause.elements { switch elements { case .statements(let codeBlock): @@ -432,7 +419,7 @@ final class Swift2JavaVisitor { private func importAccessor( from node: DeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, kind: SwiftAPIKind, name: String, ) throws { @@ -444,22 +431,22 @@ final class Swift2JavaVisitor { varNode, isSet: kind == .setter, enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext, + lookupContext: analyzer.lookupContext, ) case .subscriptDecl(let subscriptNode): signature = try SwiftFunctionSignature( subscriptNode, isSet: kind == .subscriptSetter, enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext, + lookupContext: analyzer.lookupContext, ) default: log.warning("Not supported declaration type \(node.kind) while calling importAccessor!") return } - let imported = ImportedFunc( - module: translator.swiftModuleName, + let extracted = ExtractedFunc( + module: analyzer.swiftModuleName, swiftDecl: node, name: name, apiKind: kind, @@ -467,44 +454,44 @@ final class Swift2JavaVisitor { ) log.debug( - "Record imported variable accessor \(kind == .getter || kind == .subscriptGetter ? "getter" : "setter"):\(node.qualifiedNameForDebug)" + "Record extracted variable accessor \(kind == .getter || kind == .subscriptGetter ? "getter" : "setter"):\(node.qualifiedNameForDebug)" ) if let typeContext { - typeContext.variables.append(imported) + typeContext.variables.append(extracted) } else { - translator.importedGlobalVariables.append(imported) + analyzer.extractedGlobalVariables.append(extracted) } } private func synthesizeRawRepresentableConformance( enumDecl node: EnumDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, ) { - guard let imported = translator.importedNominalType(node, parent: parent) else { + guard let extracted = analyzer.extractedNominalType(node, parent: parent) else { return } - if let firstInheritanceType = imported.swiftNominal.firstInheritanceType, + if let firstInheritanceType = extracted.swiftNominal.firstInheritanceType, let inheritanceType = try? SwiftType( firstInheritanceType, - lookupContext: translator.lookupContext, + lookupContext: analyzer.lookupContext, ), inheritanceType.isRawTypeCompatible { - if !imported.variables.contains(where: { + if !extracted.variables.contains(where: { $0.name == "rawValue" && $0.functionSignature.result.type == inheritanceType }) { let decl: DeclSyntax = "public var rawValue: \(raw: inheritanceType.description) { get }" - self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) + self.visit(decl: decl, in: extracted, sourceFilePath: extracted.sourceFilePath) } - if !imported.initializers.contains(where: { + if !extracted.initializers.contains(where: { $0.functionSignature.parameters.count == 1 && $0.functionSignature.parameters.first?.parameterName == "rawValue" && $0.functionSignature.parameters.first?.type == inheritanceType }) { let decl: DeclSyntax = "public init?(rawValue: \(raw: inheritanceType))" - self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) + self.visit(decl: decl, in: extracted, sourceFilePath: extracted.sourceFilePath) } } } @@ -514,10 +501,10 @@ final class Swift2JavaVisitor { func visit( typeAliasDecl node: TypeAliasDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - let javaName = node.name.text + let outputName = node.name.text let rhsType = node.initializer.value let genericArgs: [String] @@ -533,13 +520,13 @@ final class Swift2JavaVisitor { guard !genericArgs.isEmpty else { return } // Resolve the base type through the symbol table - guard let baseType = translator.importedNominalType(rhsType) else { + guard let baseType = analyzer.extractedNominalType(rhsType) else { log.debug("Could not resolve base type for specialization: \(rhsType.trimmedDescription)") return } registerSpecialization( - javaName: javaName, + outputName: outputName, baseType: baseType, genericArgs: genericArgs, rhsDescription: rhsType.trimmedDescription, @@ -548,8 +535,8 @@ final class Swift2JavaVisitor { /// Register a specialization from a typealias that specializes a generic type private func registerSpecialization( - javaName: String, - baseType: ImportedNominalType, + outputName: String, + baseType: ExtractedNominalType, genericArgs: [String], rhsDescription: String, ) { @@ -564,48 +551,48 @@ final class Swift2JavaVisitor { } } - let specialized: ImportedNominalType + let specialized: ExtractedNominalType do { - specialized = try baseType.specialize(as: javaName, with: substitutions) + specialized = try baseType.specialize(as: outputName, with: substitutions) } catch { - log.warning("Failed to specialize \(baseType.baseTypeName) as \(javaName): \(error)") + log.warning("Failed to specialize \(baseType.baseTypeName) as \(outputName): \(error)") return } - translator.specializations[baseType, default: []].insert(specialized) - log.info("Registered specialization: \(javaName) = \(rhsDescription)") + analyzer.specializations[baseType, default: []].insert(specialized) + log.info("Registered specialization: \(outputName) = \(rhsDescription)") } // ==== ----------------------------------------------------------------------- // MARK: Specialization support /// Apply specializations to a type if matching entries exist - func applySpecialization(to importedType: ImportedNominalType) { - guard let specializations = translator.specializations[importedType] else { + func applySpecialization(to extractedType: ExtractedNominalType) { + guard let specializations = analyzer.specializations[extractedType] else { return } for specialized in specializations { - translator.importedTypes[specialized.effectiveJavaName] = specialized - log.info("Applied specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + analyzer.extractedTypes[specialized.effectiveTypeName] = specialized + log.info("Applied specialization: \(specialized.effectiveTypeName) -> \(specialized.effectiveSwiftTypeName)") } } /// Apply specializations that were registered after their target types were visited, /// then process any deferred constrained extensions func applyPendingSpecializations() { - for (_, specializations) in translator.specializations { + for (_, specializations) in analyzer.specializations { for specialized in specializations { - if translator.importedTypes[specialized.effectiveJavaName] != nil { + if analyzer.extractedTypes[specialized.effectiveTypeName] != nil { continue } - translator.importedTypes[specialized.effectiveJavaName] = specialized - log.info("Applied pending specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + analyzer.extractedTypes[specialized.effectiveTypeName] = specialized + log.info("Applied pending specialization: \(specialized.effectiveTypeName) -> \(specialized.effectiveSwiftTypeName)") } } // Process constrained extensions that were deferred for deferred in deferredConstrainedExtensions { - guard let baseType = translator.importedNominalType(deferred.node.extendedType) else { + guard let baseType = analyzer.extractedNominalType(deferred.node.extendedType) else { continue } let matchingSpecializations = findMatchingSpecializations( @@ -667,10 +654,10 @@ final class Swift2JavaVisitor { /// Find specializations whose type args match the given where-clause constraints private func findMatchingSpecializations( - extendedType: ImportedNominalType, + extendedType: ExtractedNominalType, whereConstraints: [ParsedWhereConstraint], - ) -> [ImportedNominalType] { - guard let specializations = translator.specializations[extendedType] else { + ) -> [ExtractedNominalType] { + guard let specializations = analyzer.specializations[extendedType] else { return [] } return specializations.filter { specialized in @@ -682,7 +669,7 @@ final class Swift2JavaVisitor { /// Where-clauses are conjunctive: every constraint must hold. private func constraintsMatchSpecialization( _ constraints: [ParsedWhereConstraint], - specialized: ImportedNominalType, + specialized: ExtractedNominalType, ) -> Bool { for constraint in constraints { switch constraint { @@ -695,10 +682,10 @@ final class Swift2JavaVisitor { guard let concreteName = specialized.genericArguments[typeParam] else { return false } - guard let concreteType = translator.importedTypes[concreteName] else { + guard let concreteType = analyzer.extractedTypes[concreteName] else { return false } - guard concreteType.conformsTo(proto, in: translator.importedTypes) else { + guard concreteType.conformsTo(proto, in: analyzer.extractedTypes) else { return false } } @@ -712,30 +699,15 @@ final class Swift2JavaVisitor { } extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { - func shouldExtract(config: Configuration, log: Logger, in parent: ImportedNominalType?) -> Bool { - // @JavaExport overrides all filters — always extract - if attributes.contains(where: { $0.isJavaExport }) { - return true - } - - let meetsRequiredAccessLevel: Bool = - switch config.effectiveMinimumInputAccessLevelMode { - case .public: self.isPublic(in: parent?.swiftNominal.syntax) - case .package: self.isAtLeastPackage - case .internal: self.isAtLeastInternal - } - - guard meetsRequiredAccessLevel else { - 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 + /// Decide whether this declaration should be extracted. + /// + /// Delegates entirely to the supplied `decider`; all per-decl policy + /// lives there — see `ExtractDecider`. + func shouldExtract( + config: any SwiftExtractConfiguration, + in parent: ExtractedNominalType?, + decider: any ExtractDecider + ) -> Bool { + decider.shouldExtract(decl: DeclSyntax(self), in: parent) } } diff --git a/Sources/SwiftExtract/SwiftAnalyzer+Foundation.swift b/Sources/SwiftExtract/SwiftAnalyzer+Foundation.swift new file mode 100644 index 000000000..720ed4de1 --- /dev/null +++ b/Sources/SwiftExtract/SwiftAnalyzer+Foundation.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +extension SwiftAnalyzer { + 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 + // Data also emits DataProtocol, etc. + struct FoundationTypeGroup { + let candidates: [SwiftKnownTypeDeclKind] + let fakeSourceFilePath: String + } + let groups: [[FoundationTypeGroup]] = [ + [ + .init( + candidates: [.foundationData, .essentialsData], + fakeSourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift", + ), + .init( + candidates: [.foundationDataProtocol, .essentialsDataProtocol], + fakeSourceFilePath: "Foundation/FAKE_FOUNDATION_DATAPROTOCOL.swift", + ), + ], + [ + .init( + candidates: [.foundationDate, .essentialsDate], + fakeSourceFilePath: "Foundation/FAKE_FOUNDATION_DATE.swift", + ) + ], + [ + .init( + candidates: [.foundationUUID, .essentialsUUID], + fakeSourceFilePath: "Foundation/FAKE_FOUNDATION_UUID.swift", + ) + ], + ] + + for group in groups { + let resolved: [(primary: SwiftNominalTypeDeclaration, source: String, candidates: [SwiftNominalTypeDeclaration])] = + group.compactMap { type in + let candidates = type.candidates.compactMap { self.symbolTable[$0] } + guard let primary = candidates.first else { + return nil + } + return (primary, type.fakeSourceFilePath, candidates) + } + guard !resolved.isEmpty else { + continue + } + + let allCandidates = resolved.flatMap(\.candidates) + let isReferenced = self.isUsing(where: { decl in + allCandidates.contains(where: { $0 === decl }) + }) + guard isReferenced else { + continue + } + + // Visit the fake source files, and register the types. + for entry in resolved { + visitor.visit( + nominalDecl: entry.primary.syntax.asNominal!, + in: nil, + sourceFilePath: entry.source, + ) + } + } + } +} diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift new file mode 100644 index 000000000..c50e9401d --- /dev/null +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -0,0 +1,428 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import Logging +import SwiftIfConfig +import SwiftParser +import SwiftSyntax + +/// Drives the analysis of Swift source code into an `AnalysisResult` that +/// downstream language generators can consume. +public final class SwiftAnalyzer { + static let SWIFT_INTERFACE_SUFFIX = ".swiftinterface" + + package var log: Logger + + package let config: any SwiftExtractConfiguration + + /// The build configuration used to resolve #if conditional compilation blocks. + package let buildConfig: any BuildConfiguration + + /// The name of the Swift module being translated. + package let swiftModuleName: String + + // ==== Input + + 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 + 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. + /// Populated by `SwiftToJava.run` before `analyze()` runs. + package var sourceDependencies = SourceDependencies() + + // ==== Output state + + package var extractedGlobalVariables: [ExtractedFunc] = [] + + package var extractedGlobalFuncs: [ExtractedFunc] = [] + + /// A mapping from Swift type names (e.g., A.B) over to the extracted nominal + /// type representation. + package var extractedTypes: [SwiftTypeName: ExtractedNominalType] = [:] + + /// Specializations of generic types that will get their concrete Java declarations, "as if" they were independent types + package var specializations: [ExtractedNominalType: Set] = [:] + + package var lookupContext: SwiftTypeLookupContext! = nil + + package var symbolTable: SwiftSymbolTable! { + lookupContext?.symbolTable + } + + /// Language-specific per-decl extraction policy. Every language target + /// must supply one — pass `DefaultAccessLevelExtractDecider` for the + /// access-level-only baseline. + package let extractDecider: any ExtractDecider + + public init( + config: any SwiftExtractConfiguration, + moduleName: String? = nil, + extractDecider: any ExtractDecider + ) { + guard let swiftModule = moduleName ?? config.swiftModule else { + fatalError("Missing 'swiftModule' name.") // FIXME: can we make it required in config? but we shared config for many cases + } + self.log = Logger(label: "analyzer", logLevel: config.logLevel ?? .info) + self.config = config + self.swiftModuleName = swiftModule + self.extractDecider = extractDecider + + if let staticBuildConfigPath = config.staticBuildConfigurationFile { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: staticBuildConfigPath)) + let decoder = JSONDecoder() + let staticConfig = try decoder.decode(StaticBuildConfiguration.self, from: data) + self.buildConfig = SwiftAnalyzer.overlayingAvailableModules(staticConfig, config.availableImportModules) + self.log.info("Using custom static build configuration from: \(staticBuildConfigPath)") + } catch { + fatalError("Failed to load static build configuration from '\(staticBuildConfigPath)': \(error)") + } + } else { + self.buildConfig = SwiftAnalyzer.overlayingAvailableModules(.swiftExtractDefault, config.availableImportModules) + } + } + + /// Overlay the configured extra importable modules onto a base build config + /// (returns the base unchanged when none are configured). + private static func overlayingAvailableModules( + _ base: Base, + _ availableImportModules: Set + ) -> any BuildConfiguration { + availableImportModules.isEmpty + ? base + : ImportOverlayBuildConfiguration(base: base, availableImportModules: availableImportModules) + } +} + +// ===== -------------------------------------------------------------------------------------------------------------- +// MARK: Analysis + +extension SwiftAnalyzer { + /// Snapshot of the analysis state as a value-typed `AnalysisResult`. + public var result: AnalysisResult { + AnalysisResult( + extractedTypes: self.extractedTypes, + extractedGlobalVariables: self.extractedGlobalVariables, + extractedGlobalFuncs: self.extractedGlobalFuncs, + ) + } + + package func add(filePath: String, text: String) { + log.debug("Adding: \(filePath)") + let sourceFileSyntax = Parser.parse(source: text) + self.inputs.append(SwiftInputFile(syntax: sourceFileSyntax, path: filePath)) + } + + /// Convenient method for analyzing single file. + package func analyze(path: String, text: String) throws { + self.add(filePath: path, text: text) + try self.analyze() + } + + /// Analyze registered inputs, optionally with a hook that fires after the + /// per-source walk has populated the specialization registry from any + /// in-source `typealias Alias = Base` declarations, but before the + /// deferred constrained-extension queue is processed against those + /// specializations. + /// + /// Downstream language code generators that drive specialization from a + /// configuration source other than Swift typealiases (e.g. a `specialize:` + /// config entry that names a base type by qualified name) can use the hook + /// to call `registerSpecialization(_:outputName:typeArgs:)` so their + /// specializations participate in `findMatchingSpecializations` alongside + /// the analyzer's natively-registered ones. Without the hook, those + /// specializations are registered too late and any constrained extension + /// (`extension Box where T == ConcreteT { … }`) is silently dropped. + package func analyze( + beforeProcessingDeferredExtensions hook: (SwiftAnalyzer) throws -> Void = { _ in } + ) throws { + prepareForTranslation() + + let visitor = SwiftAnalysisVisitor(analyzer: self) + + for input in self.inputs { + log.trace("Analyzing \(input.path)") + visitor.visit(inputFile: input) + } + + try hook(self) + + // Apply any specializations registered after their target types were visited + visitor.applyPendingSpecializations() + + self.visitFoundationDeclsIfNeeded(with: visitor) + } + + /// Register a specialization of a generic type, producing a concrete + /// extracted type as if an in-source `typealias \(outputName) = \(baseQualifiedName)<…>` + /// had been declared. Intended for use from the + /// `analyze(beforeProcessingDeferredExtensions:)` hook so deferred + /// constrained extensions can match against the registered specialization. + /// + /// The base type is resolved via the parsed module's symbol table by its + /// qualified name (e.g. `"Box"`, `"ATHM.Server"`); returns `nil` if the + /// base can't be found, isn't generic, or specialization fails. + /// + /// Note: this lower-cases over `ExtractedNominalType.specialize`, but unlike + /// the visitor's typealias path it accepts already-substituted argument + /// names (so callers that consume external configuration can pass the + /// concrete type names directly without going through TypeSyntax parsing). + @discardableResult + public func registerSpecialization( + baseQualifiedName: String, + outputName: String, + typeArgs: [String: String] + ) -> ExtractedNominalType? { + guard let base = resolveBaseForSpecialization(baseQualifiedName: baseQualifiedName) else { + return nil + } + let specialized: ExtractedNominalType + do { + specialized = try base.specialize(as: outputName, with: typeArgs) + } catch { + log.warning("Failed to specialize \(base.baseTypeName) as \(outputName): \(error)") + return nil + } + self.specializations[base, default: []].insert(specialized) + log.info("Registered specialization (external): \(outputName) = \(base.baseTypeName)<\(typeArgs.values.joined(separator: ", "))>") + return specialized + } + + /// Like `registerSpecialization(baseQualifiedName:outputName:typeArgs:)`, + /// but accepts positional generic arguments (matched in order to the + /// base's generic parameters). Convenient for callers that scrape + /// `typealias Foo = Bar` from source syntax and don't already + /// know the parameter names. + @discardableResult + public func registerSpecializationByPosition( + baseQualifiedName: String, + outputName: String, + positionalArgs: [String] + ) -> ExtractedNominalType? { + guard let base = resolveBaseForSpecialization(baseQualifiedName: baseQualifiedName) else { + return nil + } + var typeArgs: [String: String] = [:] + for (i, name) in base.genericParameterNames.enumerated() where i < positionalArgs.count { + typeArgs[name] = positionalArgs[i] + } + return registerSpecialization( + baseQualifiedName: baseQualifiedName, + outputName: outputName, + typeArgs: typeArgs + ) + } + + private func resolveBaseForSpecialization(baseQualifiedName: String) -> ExtractedNominalType? { + let parts = baseQualifiedName.split(separator: ".").map(String.init) + var resolvedBase: SwiftNominalTypeDeclaration? = nil + var parent: SwiftNominalTypeDeclaration? = nil + for part in parts { + let next = symbolTable.lookupType(part, parent: parent) + guard let next else { return nil } + parent = next + resolvedBase = next + } + guard let baseDecl = resolvedBase else { return nil } + let base = self.extractedNominalType(baseDecl) + guard let base, !base.genericParameterNames.isEmpty else { return nil } + return base + } + + /// The set of effective Swift-side type names of every specialization + /// currently registered with the analyzer (across all base types). Useful + /// for callers driving `registerSpecialization` from a hook to skip names + /// already registered by the analyzer's own typealias-decl visitor and + /// avoid double-registering distinct `ExtractedNominalType` instances + /// with the same effective name. + public var registeredSpecializationNames: Set { + Set(self.specializations.values.flatMap { $0 }.map(\.effectiveTypeName)) + } + + /// Top-level convenience: run analysis on the given Swift sources and return + /// the resulting `AnalysisResult`. Optionally accepts a hook fired after + /// the per-source walk and before deferred-constrained-extension + /// processing — see + /// `SwiftAnalyzer.analyze(beforeProcessingDeferredExtensions:)` for the + /// hook semantics. Use to drive specialization registration from + /// downstream configuration sources (e.g. `specialize:` config entries) + /// so deferred constrained extensions match against those specializations + /// before they're dropped. + public static func analyze( + sources: [(path: String, text: String)], + moduleName: String, + extractDecider: any ExtractDecider, + config: (any SwiftExtractConfiguration)? = nil, + sourceDependencies: SourceDependencies = SourceDependencies(), + beforeProcessingDeferredExtensions hook: (SwiftAnalyzer) throws -> Void = { _ in } + ) throws -> AnalysisResult { + let effectiveConfig = config ?? DefaultSwiftExtractConfiguration(swiftModule: moduleName) + let analyzer = SwiftAnalyzer(config: effectiveConfig, moduleName: moduleName, extractDecider: extractDecider) + analyzer.sourceDependencies = sourceDependencies + for source in sources { + analyzer.add(filePath: source.path, text: source.text) + } + try analyzer.analyze(beforeProcessingDeferredExtensions: hook) + return analyzer.result + } + + package func prepareForTranslation() { + let symbolTable = SwiftSymbolTable.setup( + moduleName: self.swiftModuleName, + inputs, + config: self.config, + sourceDependencies: self.sourceDependencies, + buildConfig: self.buildConfig, + ) + self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) + self.lookupContext.allowUnresolvedTypeReferences = + self.config.allowUnresolvedTypeReferences + } + + /// Check if any of the extracted decls uses a nominal declaration that satisfies + /// the given predicate. + func isUsing(where predicate: (SwiftNominalTypeDeclaration) -> Bool) -> Bool { + func check(_ type: SwiftType) -> Bool { + switch type { + case .nominal(let nominal): + if nominal.genericArguments.contains(where: check) { + return true + } + return predicate(nominal.nominalTypeDecl) + case .tuple(let tuple): + return tuple.contains(where: { check($0.type) }) + case .function(let fn): + return check(fn.resultType) || fn.parameters.contains(where: { check($0.type) }) + case .metatype(let ty): + return check(ty) + case .existential(let ty), .opaque(let ty): + return check(ty) + case .composite(let types): + return types.contains(where: check) + case .inlineArray(_, let element): + return check(element) + case .genericParameter: + return false + } + } + + func check(_ fn: ExtractedFunc) -> Bool { + if check(fn.functionSignature.result.type) { + return true + } + if fn.functionSignature.parameters.contains(where: { check($0.type) }) { + return true + } + return false + } + + if self.extractedGlobalFuncs.contains(where: check) { + return true + } + if self.extractedGlobalVariables.contains(where: check) { + return true + } + for extractedType in self.extractedTypes.values { + if extractedType.initializers.contains(where: check) { + return true + } + if extractedType.methods.contains(where: check) { + return true + } + if extractedType.variables.contains(where: check) { + return true + } + } + return false + } +} + +// ==== ---------------------------------------------------------------------------------------------------------------- +// MARK: Type translation +extension SwiftAnalyzer { + /// Try to resolve the given nominal declaration node into its extracted representation. + func extractedNominalType( + _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, + parent: ExtractedNominalType?, + ) -> ExtractedNominalType? { + if !nominalNode.shouldExtract(config: config, in: parent, decider: extractDecider) { + return nil + } + + guard let nominal = symbolTable.lookupType(nominalNode.name.text, parent: parent?.swiftNominal) else { + return nil + } + return self.extractedNominalType(nominal) + } + + /// Try to resolve the given nominal type node into its extracted representation. + func extractedNominalType( + _ typeNode: TypeSyntax + ) -> ExtractedNominalType? { + guard let swiftType = try? SwiftType(typeNode, lookupContext: lookupContext) else { + return nil + } + guard let swiftNominalDecl = swiftType.asNominalTypeDeclaration else { + return nil + } + + let isFromThisModule = swiftNominalDecl.moduleName == self.swiftModuleName + let isFromStubbedModule = config.hasImportedModuleStub(moduleOfNominal: swiftNominalDecl.moduleName) + let isFromDependencyModule = sourceDependencies.swiftModuleNames.contains(swiftNominalDecl.moduleName) + guard isFromThisModule || isFromStubbedModule || isFromDependencyModule else { + return nil + } + + guard swiftNominalDecl.syntax.shouldExtract(config: config, in: nil as ExtractedNominalType?, decider: extractDecider) else { + return nil + } + + return extractedNominalType(swiftNominalDecl) + } + + func extractedNominalType(_ nominal: SwiftNominalTypeDeclaration) -> ExtractedNominalType? { + let fullName = nominal.qualifiedName + + guard shouldExtractSwiftType(qualifiedName: fullName, config: config) else { + log.debug("Skip import '\(fullName)': filtered by swiftFilterInclude/swiftFilterExclude") + return nil + } + + if let alreadyExtracted = extractedTypes[fullName] { + return alreadyExtracted + } + + let extractedNominal = try? ExtractedNominalType(swiftNominal: nominal, lookupContext: lookupContext) + + extractedTypes[fullName] = extractedNominal + return extractedNominal + } +} + +// ==== ----------------------------------------------------------------------- +// MARK: Errors + +public struct SwiftAnalyzerError: Error { + let message: String + + public init(message: String) { + self.message = message + } +} diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift new file mode 100644 index 000000000..6ff68c9d6 --- /dev/null +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_exported import SwiftExtractConfigurationShared + +/// The configuration surface required by the language-neutral `SwiftExtract` +/// analysis layer. +/// +/// `SwiftExtract` deliberately does NOT depend on any language-specific +/// configuration module. Instead, each language layer makes its own +/// `Configuration` type conform to this protocol, mapping its settings onto the +/// neutral surface below. This keeps the analysis layer reusable across targets +/// (e.g. Java/JNI/FFM, or other language code generators) without pulling +/// target-specific config types into `SwiftExtract`. +/// +/// `AccessLevelMode` and `LogLevel` live in the small +/// `SwiftExtractConfigurationShared` target so language-specific configuration +/// shared modules (e.g. `SwiftJavaConfigurationShared`) can use the same +/// enums directly without taking a dependency on SwiftSyntax. +public protocol SwiftExtractConfiguration { + /// Name of the Swift module being analyzed. + var swiftModule: String? { get } + + /// Optional path to a JSON `StaticBuildConfiguration` used to resolve `#if`. + var staticBuildConfigurationFile: String? { get } + + /// Glob patterns selecting which Swift files/types to extract. + var swiftFilterInclude: [String]? { get } + + /// Glob patterns excluding Swift files/types from extraction. + var swiftFilterExclude: [String]? { get } + + /// Stub declarations for imported modules whose source is unavailable to the + /// analyzer. Keyed by module name; values are Swift declaration strings parsed + /// as if they belonged to that module. + var importedModuleStubs: [String: [String]]? { get } + + /// Minimum access level required for a declaration to be extracted. + var effectiveMinimumInputAccessLevelMode: AccessLevelMode { get } + + /// Verbosity for the analyzer's logger; `nil` falls back to `.info`. + var logLevel: LogLevel? { get } + + /// Module names that should be treated as importable when resolving + /// `#if canImport()` conditions, in addition to whatever the build + /// configuration already knows. Lets a target opt-in to extracting code + /// guarded behind `#if canImport(MyModule)` (e.g. another language code + /// generator can declare its runtime module importable here). Default: empty. + var availableImportModules: Set { get } + + /// Whether type lookups that can't resolve a name should fall back to a + /// synthetic, unresolved nominal reference instead of throwing + /// `TypeTranslationError.unknown`. + /// + /// `SwiftExtract` defaults to a strict policy: when a parameter, return + /// type, property type, etc. references a name the symbol table can't + /// resolve, the enclosing declaration is silently dropped (the analyzer + /// emits a `[warning] Failed to import: …` log line). That's correct for + /// Java/JNI, where the generator can't render code referencing an + /// unresolved Swift type. + /// + /// Code generators that resolve names later than analysis time + /// (lazy specializations) can opt-in by setting this `true`. Unresolved + /// names then become synthetic nominal types stamped with + /// `isUnresolvedTypePlaceholder == true` so downstream passes can substitute + /// or recognize them. See + /// `SwiftNominalTypeDeclaration.isUnresolvedTypePlaceholder` for the long + /// form. Default: false. + var allowUnresolvedTypeReferences: Bool { get } + + /// Whether the given module name has stub declarations configured. + func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool +} + +extension SwiftExtractConfiguration { + public var availableImportModules: Set { [] } + + public var allowUnresolvedTypeReferences: Bool { false } + + public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool { + importedModuleStubs?.keys.contains(moduleName) ?? false + } +} + +/// A minimal, self-contained `SwiftExtractConfiguration` for callers that only +/// need analysis (tests, tools) and don't have a richer language-specific +/// configuration to supply. +public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { + public var swiftModule: String? + public var staticBuildConfigurationFile: String? + public var swiftFilterInclude: [String]? + public var swiftFilterExclude: [String]? + public var importedModuleStubs: [String: [String]]? + public var effectiveMinimumInputAccessLevelMode: AccessLevelMode + public var logLevel: LogLevel? + public var availableImportModules: Set + public var allowUnresolvedTypeReferences: Bool + + public init( + swiftModule: String? = nil, + accessLevel: AccessLevelMode = .public, + logLevel: LogLevel? = nil, + staticBuildConfigurationFile: String? = nil, + swiftFilterInclude: [String]? = nil, + swiftFilterExclude: [String]? = nil, + importedModuleStubs: [String: [String]]? = nil, + availableImportModules: Set = [], + allowUnresolvedTypeReferences: Bool = false + ) { + self.swiftModule = swiftModule + self.effectiveMinimumInputAccessLevelMode = accessLevel + self.logLevel = logLevel + self.staticBuildConfigurationFile = staticBuildConfigurationFile + self.swiftFilterInclude = swiftFilterInclude + self.swiftFilterExclude = swiftFilterExclude + self.importedModuleStubs = importedModuleStubs + self.availableImportModules = availableImportModules + self.allowUnresolvedTypeReferences = allowUnresolvedTypeReferences + } +} diff --git a/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift new file mode 100644 index 000000000..a11d76d73 --- /dev/null +++ b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftIfConfig +import SwiftSyntax + +/// A default, fixed build configuration during static analysis for interface extraction. +package struct SwiftExtractDefaultBuildConfiguration: BuildConfiguration { + package static let shared = SwiftExtractDefaultBuildConfiguration() + + private var base: StaticBuildConfiguration + + package init() { + guard let url = Bundle.module.url(forResource: "static-build-config", withExtension: "json") else { + fatalError("static-build-config.json is not found in module bundle") + } + do { + let data = try Data(contentsOf: url) + let decoder = JSONDecoder() + base = try decoder.decode(StaticBuildConfiguration.self, from: data) + } catch { + fatalError("\(error)") + } + } + + package func isCustomConditionSet(name: String) throws -> Bool { + base.isCustomConditionSet(name: name) + } + + package func hasFeature(name: String) throws -> Bool { + base.hasFeature(name: name) + } + + package func hasAttribute(name: String) throws -> Bool { + base.hasAttribute(name: name) + } + + package func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { + try base.canImport(importPath: importPath, version: version) + } + + package func isActiveTargetOS(name: String) throws -> Bool { + true + } + + package func isActiveTargetArchitecture(name: String) throws -> Bool { + true + } + + package func isActiveTargetEnvironment(name: String) throws -> Bool { + true + } + + package func isActiveTargetRuntime(name: String) throws -> Bool { + true + } + + package func isActiveTargetPointerAuthentication(name: String) throws -> Bool { + true + } + + package func isActiveTargetObjectFormat(name: String) throws -> Bool { + true + } + + package var targetPointerBitWidth: Int { + base.targetPointerBitWidth + } + + package var targetAtomicBitWidths: [Int] { + base.targetAtomicBitWidths + } + + package var endianness: Endianness { + base.endianness + } + + package var languageVersion: VersionTuple { + base.languageVersion + } + + package var compilerVersion: VersionTuple { + base.compilerVersion + } +} + +extension BuildConfiguration where Self == SwiftExtractDefaultBuildConfiguration { + package static var swiftExtractDefault: SwiftExtractDefaultBuildConfiguration { + .shared + } +} + +/// Wraps any `BuildConfiguration` and additionally reports a configured set of +/// module names as importable. Used so a target can extract declarations guarded +/// behind `#if canImport()` for modules the static build configuration +/// does not otherwise know about (e.g. another language code generator can +/// declare its own runtime module importable for extraction). +package struct ImportOverlayBuildConfiguration: BuildConfiguration { + package var base: Base + package var availableImportModules: Set + + package init(base: Base, availableImportModules: Set) { + self.base = base + self.availableImportModules = availableImportModules + } + + package func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { + // `importPath` is the dotted module path; the leading component is the module. + if let moduleName = importPath.first?.1, availableImportModules.contains(moduleName) { + return true + } + return try base.canImport(importPath: importPath, version: version) + } + + package func isCustomConditionSet(name: String) throws -> Bool { try base.isCustomConditionSet(name: name) } + package func hasFeature(name: String) throws -> Bool { try base.hasFeature(name: name) } + package func hasAttribute(name: String) throws -> Bool { try base.hasAttribute(name: name) } + package func isActiveTargetOS(name: String) throws -> Bool { try base.isActiveTargetOS(name: name) } + package func isActiveTargetArchitecture(name: String) throws -> Bool { try base.isActiveTargetArchitecture(name: name) } + package func isActiveTargetEnvironment(name: String) throws -> Bool { try base.isActiveTargetEnvironment(name: name) } + package func isActiveTargetRuntime(name: String) throws -> Bool { try base.isActiveTargetRuntime(name: name) } + package func isActiveTargetPointerAuthentication(name: String) throws -> Bool { try base.isActiveTargetPointerAuthentication(name: name) } + package func isActiveTargetObjectFormat(name: String) throws -> Bool { try base.isActiveTargetObjectFormat(name: name) } + package var targetPointerBitWidth: Int { base.targetPointerBitWidth } + package var targetAtomicBitWidths: [Int] { base.targetAtomicBitWidths } + package var endianness: Endianness { base.endianness } + package var languageVersion: VersionTuple { base.languageVersion } + package var compilerVersion: VersionTuple { base.compilerVersion } +} diff --git a/Sources/JExtractSwiftLib/JExtractFileFilter.swift b/Sources/SwiftExtract/SwiftFileFilter.swift similarity index 92% rename from Sources/JExtractSwiftLib/JExtractFileFilter.swift rename to Sources/SwiftExtract/SwiftFileFilter.swift index aef41240f..e0e0e7741 100644 --- a/Sources/JExtractSwiftLib/JExtractFileFilter.swift +++ b/Sources/SwiftExtract/SwiftFileFilter.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,15 +12,13 @@ // //===----------------------------------------------------------------------===// -import SwiftJavaConfigurationShared - // ==== ----------------------------------------------------------------------- // MARK: Swift filter pattern classification /// A filter pattern is either a file-path pattern (uses `/` or a Swift file /// extension) or a type-name pattern (uses `.`). Plain names with neither /// match both -enum SwiftFilterPatternKind { +package enum SwiftFilterPatternKind { /// Pattern contains `/`, ends in a Swift source extension, or is a bare `**` /// glob — matches against relative file paths case filePath @@ -31,7 +29,7 @@ enum SwiftFilterPatternKind { case plain } -func classifyPattern(_ pattern: String) -> SwiftFilterPatternKind { +package func classifyPattern(_ pattern: String) -> SwiftFilterPatternKind { // Directory separator is the strongest signal for a file path if pattern.contains("/") { return .filePath @@ -167,7 +165,7 @@ private func matchSegment(_ segment: String, against pattern: String) -> Bool { /// - `**` matches zero or more path segments /// - `*` at the end of a segment matches any suffix (e.g. `Us*` matches `User.swift`) /// - exact segment match otherwise -func matchesFilePathFilter(relativePath: String, pattern: String) -> Bool { +package func matchesFilePathFilter(relativePath: String, pattern: String) -> Bool { matchesGlob(value: relativePath, pattern: pattern, separator: "/") } @@ -181,7 +179,7 @@ func matchesFilePathFilter(relativePath: String, pattern: String) -> Bool { /// - `**` matches zero or more name components /// - `*` at the end of a component matches any suffix /// - exact component match otherwise -func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bool { +package func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bool { matchesGlob(value: qualifiedName, pattern: pattern, separator: ".") } @@ -189,13 +187,13 @@ func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bool { // MARK: Combined filter application /// Determine whether a file at the given `relativePath` (including `.swift` -/// extension) should be included in jextract processing, based on the -/// include/exclude filters in `config`. +/// extension) should be included in extraction, based on the include/exclude +/// filters in `config`. /// /// Only file-path patterns (containing `/`) and plain patterns (no `/` or `.`) -/// are checked here. Type-name patterns are skipped — use `shouldJExtractType` -/// for those -func shouldJExtractFile(relativePath: String, config: Configuration) -> Bool { +/// are checked here. Type-name patterns are skipped — use +/// `shouldExtractSwiftType` for those +package func shouldExtractSwiftFile(relativePath: String, config: any SwiftExtractConfiguration) -> Bool { if let includeFilters = config.swiftFilterInclude, !includeFilters.isEmpty { // Must match at least one file-level include pattern. // If all include patterns are type-name patterns, don't filter at file level @@ -244,9 +242,9 @@ private func matchesFilePattern(relativePath: String, pattern: String) -> Bool { /// `config`. /// /// Only type-name patterns (containing `.`) and plain patterns (no `/` or `.`) -/// are checked here. File-path patterns are skipped — use `shouldJExtractFile` +/// are checked here. File-path patterns are skipped — use `shouldExtractSwiftFile` /// for those -func shouldJExtractType(qualifiedName: String, config: Configuration) -> Bool { +package func shouldExtractSwiftType(qualifiedName: String, config: any SwiftExtractConfiguration) -> Bool { if let includeFilters = config.swiftFilterInclude, !includeFilters.isEmpty { let typePatterns = includeFilters.filter { classifyPattern($0) != .filePath } if !typePatterns.isEmpty { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift b/Sources/SwiftExtract/SwiftTypes/ExtractedSwiftModule.swift similarity index 73% rename from Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift rename to Sources/SwiftExtract/SwiftTypes/ExtractedSwiftModule.swift index 8db800d63..f8c1a6d6f 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift +++ b/Sources/SwiftExtract/SwiftTypes/ExtractedSwiftModule.swift @@ -12,13 +12,13 @@ // //===----------------------------------------------------------------------===// -struct ImportedSwiftModule: Hashable { - let name: String - let availableWithModuleName: String? - var alternativeModuleNames: Set - var isMainSourceOfSymbols: Bool +public struct ExtractedSwiftModule: Hashable { + public let name: String + public let availableWithModuleName: String? + public var alternativeModuleNames: Set + public var isMainSourceOfSymbols: Bool - init( + public init( name: String, availableWithModuleName: String? = nil, alternativeModuleNames: Set = [], @@ -30,11 +30,11 @@ struct ImportedSwiftModule: Hashable { self.isMainSourceOfSymbols = isMainSourceOfSymbols } - static func == (lhs: Self, rhs: Self) -> Bool { + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.name == rhs.name } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(name) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift b/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift similarity index 87% rename from Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift index a36475c7b..ed20722e6 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift @@ -15,15 +15,15 @@ import SwiftSyntax /// Scan importing modules. -func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftModule] { - var importingModuleNames: [ImportedSwiftModule] = [] +package func importingModules(sourceFile: SourceFileSyntax) -> [ExtractedSwiftModule] { + var importingModuleNames: [ExtractedSwiftModule] = [] for item in sourceFile.statements { if let importDecl = item.item.as(ImportDeclSyntax.self) { guard let moduleName = importDecl.path.first?.name.text else { continue } importingModuleNames.append( - ImportedSwiftModule(name: moduleName, availableWithModuleName: nil, alternativeModuleNames: []) + ExtractedSwiftModule(name: moduleName, availableWithModuleName: nil, alternativeModuleNames: []) ) } else if let ifConfigDecl = item.item.as(IfConfigDeclSyntax.self) { importingModuleNames.append(contentsOf: modules(from: ifConfigDecl)) @@ -32,7 +32,7 @@ func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftModule] { return importingModuleNames } -private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ImportedSwiftModule] { +private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ExtractedSwiftModule] { guard let firstClause = ifConfigDecl.clauses.first, let calledExpression = firstClause.condition?.as(FunctionCallExprSyntax.self)?.calledExpression.as( @@ -43,7 +43,7 @@ private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ImportedSwiftMod return [] } - var modules: [ImportedSwiftModule] = [] + var modules: [ExtractedSwiftModule] = [] modules.reserveCapacity(ifConfigDecl.clauses.count) for (index, clause) in ifConfigDecl.clauses.enumerated() { @@ -65,7 +65,7 @@ private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ImportedSwiftMod } let clauseModules = importedModuleNames.map { - ImportedSwiftModule( + ExtractedSwiftModule( name: $0, availableWithModuleName: importModuleName, alternativeModuleNames: [] @@ -76,7 +76,7 @@ private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ImportedSwiftMod if clauseModules.count == 1 && index == (ifConfigDecl.clauses.count - 1) && clause.poundKeyword.tokenKind == .poundElse { - var fallbackModule: ImportedSwiftModule = clauseModules[0] + var fallbackModule: ExtractedSwiftModule = clauseModules[0] var moduleNames: [String] = [] moduleNames.reserveCapacity(modules.count) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift b/Sources/SwiftExtract/SwiftTypes/SwiftEffectSpecifier.swift similarity index 92% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftEffectSpecifier.swift index fb28586b1..c59d316d0 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftEffectSpecifier.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -enum SwiftEffectSpecifier: Equatable { +public enum SwiftEffectSpecifier: Equatable { case `throws` case `async` } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift b/Sources/SwiftExtract/SwiftTypes/SwiftEnumCaseParameter.swift similarity index 86% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftEnumCaseParameter.swift index 55682152d..335739397 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftEnumCaseParameter.swift @@ -14,13 +14,13 @@ import SwiftSyntax -struct SwiftEnumCaseParameter: Equatable { - var name: String? - var type: SwiftType +public struct SwiftEnumCaseParameter: Equatable { + public var name: String? + public var type: SwiftType } extension SwiftEnumCaseParameter { - init( + public init( _ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext ) throws { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift similarity index 92% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index a957c0ea1..477fb5f2e 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -15,7 +15,7 @@ import SwiftSyntax import SwiftSyntaxBuilder -enum SwiftGenericRequirement: Equatable { +public enum SwiftGenericRequirement: Equatable { case inherits(SwiftType, SwiftType) case equals(SwiftType, SwiftType) } @@ -23,22 +23,27 @@ enum SwiftGenericRequirement: Equatable { /// Provides a complete signature for a Swift function, which includes its /// parameters and return type. public struct SwiftFunctionSignature: Equatable { - var selfParameter: SwiftSelfParameter? - var parameters: [SwiftParameter] - var result: SwiftResult - var effectSpecifiers: [SwiftEffectSpecifier] - var genericParameters: [SwiftGenericParameterDeclaration] - var genericRequirements: [SwiftGenericRequirement] - - var isAsync: Bool { + public var selfParameter: SwiftSelfParameter? + public var parameters: [SwiftParameter] + public var result: SwiftResult + public var effectSpecifiers: [SwiftEffectSpecifier] + public var genericParameters: [SwiftGenericParameterDeclaration] + public var genericRequirements: [SwiftGenericRequirement] + + public var isAsync: Bool { effectSpecifiers.contains(.async) } - var isThrowing: Bool { + public var isThrowing: Bool { effectSpecifiers.contains(.throws) } - init( + /// Whether any parameter is variadic (`T...`). + public var hasVariadicParams: Bool { + parameters.contains(where: \.isVariadic) + } + + public init( selfParameter: SwiftSelfParameter? = nil, parameters: [SwiftParameter], result: SwiftResult, @@ -56,7 +61,7 @@ public struct SwiftFunctionSignature: Equatable { } /// Describes the "self" parameter of a Swift function signature. -enum SwiftSelfParameter: Equatable { +public enum SwiftSelfParameter: Equatable { /// 'self' is an instance parameter. case instance(convention: SwiftParameterConvention, swiftType: SwiftType) @@ -68,7 +73,7 @@ enum SwiftSelfParameter: Equatable { /// to form the call. case initializer(SwiftType) - var selfType: SwiftType { + public var selfType: SwiftType { get { switch self { case .instance(_, let swiftType), .staticMethod(let swiftType), .initializer(let swiftType): @@ -89,7 +94,7 @@ enum SwiftSelfParameter: Equatable { } extension SwiftFunctionSignature { - init( + public init( _ node: InitializerDeclSyntax, enclosingType: SwiftType?, lookupContext: SwiftTypeLookupContext @@ -126,7 +131,7 @@ extension SwiftFunctionSignature { ) } - init( + public init( _ node: EnumCaseElementSyntax, enclosingType: SwiftType, lookupContext: SwiftTypeLookupContext @@ -145,7 +150,7 @@ extension SwiftFunctionSignature { ) } - init( + public init( _ node: FunctionDeclSyntax, enclosingType: SwiftType?, lookupContext: SwiftTypeLookupContext @@ -212,7 +217,7 @@ extension SwiftFunctionSignature { ) } - static func translateGenericParameters( + public static func translateGenericParameters( parameterClause: GenericParameterClauseSyntax?, whereClause: GenericWhereClauseSyntax?, lookupContext: SwiftTypeLookupContext @@ -270,7 +275,7 @@ extension SwiftFunctionSignature { /// Translate the function signature, returning the list of translated /// parameters and effect specifiers. - static func translateFunctionSignature( + public static func translateFunctionSignature( _ signature: FunctionSignatureSyntax, lookupContext: SwiftTypeLookupContext ) throws -> ([SwiftParameter], [SwiftEffectSpecifier]) { @@ -289,7 +294,7 @@ extension SwiftFunctionSignature { return (parameters, effectSpecifiers) } - init( + public init( _ varNode: VariableDeclSyntax, isSet: Bool, enclosingType: SwiftType?, @@ -340,7 +345,7 @@ extension SwiftFunctionSignature { self.genericRequirements = [] } - init( + public init( _ subscriptNode: SubscriptDeclSyntax, isSet: Bool, enclosingType: SwiftType?, @@ -443,7 +448,7 @@ extension VariableDeclSyntax { /// /// - Parameters: /// - binding the pattern binding in this declaration. - func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { + public func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { if self.bindingSpecifier.tokenKind == .keyword(.let) { return [.get] } @@ -457,15 +462,19 @@ extension VariableDeclSyntax { } extension AccessorBlockSyntax { - struct SupportedAccessorKinds: OptionSet { - var rawValue: UInt8 + public struct SupportedAccessorKinds: OptionSet { + public var rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } - static var get: Self = .init(rawValue: 1 << 0) - static var set: Self = .init(rawValue: 1 << 1) + public static var get: Self = .init(rawValue: 1 << 0) + public static var set: Self = .init(rawValue: 1 << 1) } /// Determine what operations (i.e. get and/or set) supported in this `AccessorBlockSyntax` - func supportedAccessorKinds() -> SupportedAccessorKinds { + public func supportedAccessorKinds() -> SupportedAccessorKinds { switch self.accessors { case .getter: return [.get] @@ -485,7 +494,7 @@ extension AccessorBlockSyntax { } } -enum SwiftFunctionTranslationError: Error { +public enum SwiftFunctionTranslationError: Error { case `throws`(ThrowsClauseSyntax) case async(TokenSyntax) case classMethod(TokenSyntax) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift similarity index 77% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift index 628cbc7a1..b127e6fb7 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift @@ -14,20 +14,32 @@ import SwiftSyntax -struct SwiftFunctionType: Equatable { - enum Convention: Equatable { +public struct SwiftFunctionType: Equatable { + public enum Convention: Equatable { case swift case c } - var convention: Convention - var parameters: [SwiftParameter] - var resultType: SwiftType - var isEscaping: Bool = false + public var convention: Convention + public var parameters: [SwiftParameter] + public var resultType: SwiftType + public var isEscaping: Bool = false + + public init( + convention: Convention, + parameters: [SwiftParameter], + resultType: SwiftType, + isEscaping: Bool = false + ) { + self.convention = convention + self.parameters = parameters + self.resultType = resultType + self.isEscaping = isEscaping + } } extension SwiftFunctionType: CustomStringConvertible { - var description: String { + public var description: String { let parameterString = parameters.map { $0.descriptionInType }.joined(separator: ", ") let conventionPrefix = switch convention { @@ -40,7 +52,7 @@ extension SwiftFunctionType: CustomStringConvertible { } extension SwiftFunctionType { - init( + public init( _ node: FunctionTypeSyntax, convention: Convention, isEscaping: Bool = false, diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift similarity index 94% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift index 696b1f1a5..689c24f61 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift @@ -15,16 +15,16 @@ import SwiftSyntax import SwiftSyntaxBuilder -enum SwiftKnownModule: String { +public enum SwiftKnownModule: String { case swift = "Swift" case foundation = "Foundation" case foundationEssentials = "FoundationEssentials" - var name: String { + public var name: String { self.rawValue } - var symbolTable: SwiftModuleSymbolTable { + public var symbolTable: SwiftModuleSymbolTable { switch self { case .swift: swiftSymbolTable case .foundation: foundationSymbolTable @@ -32,7 +32,7 @@ enum SwiftKnownModule: String { } } - var sourceFile: SourceFileSyntax { + public var sourceFile: SourceFileSyntax { switch self { case .swift: swiftSourceFile case .foundation: foundationEssentialsSourceFile @@ -130,6 +130,11 @@ private let foundationEssentialsSourceFile: SourceFileSyntax = """ } public struct UUID {} + + public struct URL { + public init?(string: String) + public var absoluteString: String { get } + } """ private var foundationSourceFile: SourceFileSyntax { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift similarity index 87% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift index 5b1e28933..7b4ea12ea 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift @@ -14,7 +14,7 @@ import SwiftSyntax -enum SwiftKnownType: Equatable { +public enum SwiftKnownType: Equatable { case bool case int case uint @@ -53,10 +53,7 @@ enum SwiftKnownType: Equatable { case foundationUUID case essentialsUUID - // SwiftRuntimeFunctions - case swiftJavaError - - init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { + public init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { switch kind { case .bool: self = .bool case .int: self = .int @@ -109,11 +106,10 @@ enum SwiftKnownType: Equatable { case .essentialsDate: self = .essentialsDate case .foundationUUID: self = .foundationUUID case .essentialsUUID: self = .essentialsUUID - case .swiftJavaError: self = .swiftJavaError } } - var kind: SwiftKnownTypeDeclKind { + public var kind: SwiftKnownTypeDeclKind { switch self { case .bool: .bool case .int: .int @@ -150,12 +146,11 @@ enum SwiftKnownType: Equatable { case .essentialsDate: .essentialsDate case .foundationUUID: .foundationUUID case .essentialsUUID: .essentialsUUID - case .swiftJavaError: .swiftJavaError } } } -enum SwiftKnownTypeDeclKind: String, Hashable { +public enum SwiftKnownTypeDeclKind: String, Hashable { // Swift case bool = "Swift.Bool" case int = "Swift.Int" @@ -195,10 +190,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { case foundationUUID = "Foundation.UUID" case essentialsUUID = "FoundationEssentials.UUID" - // SwiftRuntimeFunctions - case swiftJavaError = "SwiftRuntimeFunctions.SwiftJavaError" - - var moduleAndName: (module: String, name: String) { + public var moduleAndName: (module: String, name: String) { let qualified = self.rawValue let period = qualified.firstIndex(of: ".")! return ( @@ -207,7 +199,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { ) } - var isPointer: Bool { + public var isPointer: Bool { switch self { case .unsafePointer, .unsafeMutablePointer, .unsafeRawPointer, .unsafeMutableRawPointer: return true @@ -215,19 +207,4 @@ enum SwiftKnownTypeDeclKind: String, Hashable { return false } } - - /// Indicates whether this known type is translated by `wrap-java` - /// into the same type as `jextract`. - /// - /// This means we do not have to perform any mapping when passing - /// this type between jextract and wrap-java - var isDirectlyTranslatedToWrapJava: Bool { - switch self { - case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string, - .void: - return true - default: - return false - } - } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypes.swift similarity index 51% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownTypes.swift index 198a1c14d..6c9c93a5d 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypes.swift @@ -12,50 +12,50 @@ // //===----------------------------------------------------------------------===// -struct SwiftKnownTypes { +public struct SwiftKnownTypes { private let symbolTable: SwiftSymbolTable - init(symbolTable: SwiftSymbolTable) { + public init(symbolTable: SwiftSymbolTable) { self.symbolTable = symbolTable } - var bool: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.bool])) } - var int: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int])) } - var uint: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint])) } - var int8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int8])) } - var uint8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint8])) } - var int16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int16])) } - var uint16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint16])) } - var int32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int32])) } - var uint32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint32])) } - var int64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int64])) } - var uint64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint64])) } - var float: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.float])) } - var double: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.double])) } - var unsafeRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawPointer])) } - var unsafeRawBufferPointer: SwiftType { + public var bool: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.bool])) } + public var int: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int])) } + public var uint: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint])) } + public var int8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int8])) } + public var uint8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint8])) } + public var int16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int16])) } + public var uint16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint16])) } + public var int32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int32])) } + public var uint32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint32])) } + public var int64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int64])) } + public var uint64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint64])) } + public var float: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.float])) } + public var double: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.double])) } + public var unsafeRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawPointer])) } + public var unsafeRawBufferPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawBufferPointer])) } - var unsafeMutableRawPointer: SwiftType { + public var unsafeMutableRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeMutableRawPointer])) } - var string: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.string])) } + public var string: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.string])) } - var foundationDataProtocol: SwiftType { + public var foundationDataProtocol: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationDataProtocol])) } - var foundationData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationData])) } - var essentialsDataProtocol: SwiftType { + public var foundationData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationData])) } + public var essentialsDataProtocol: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsDataProtocol])) } - var essentialsData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsData])) } - var foundationUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationUUID])) } - var essentialsUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsUUID])) } + public var essentialsData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsData])) } + public var foundationUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationUUID])) } + public var essentialsUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsUUID])) } /// `(UnsafeRawPointer, Long) -> ()` function type. /// /// Commonly used to initialize a buffer using the passed bytes and length. - var functionInitializeByteBuffer: SwiftType { + public var functionInitializeByteBuffer: SwiftType { .function( SwiftFunctionType( convention: .c, @@ -68,7 +68,7 @@ struct SwiftKnownTypes { ) } - func unsafePointer(_ pointeeType: SwiftType) -> SwiftType { + public func unsafePointer(_ pointeeType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafePointer], @@ -77,7 +77,7 @@ struct SwiftKnownTypes { ) } - func unsafeMutablePointer(_ pointeeType: SwiftType) -> SwiftType { + public func unsafeMutablePointer(_ pointeeType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafeMutablePointer], @@ -86,7 +86,7 @@ struct SwiftKnownTypes { ) } - func unsafeBufferPointer(_ elementType: SwiftType) -> SwiftType { + public func unsafeBufferPointer(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafeBufferPointer], @@ -95,7 +95,7 @@ struct SwiftKnownTypes { ) } - func unsafeMutableBufferPointer(_ elementType: SwiftType) -> SwiftType { + public func unsafeMutableBufferPointer(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafeMutableBufferPointer], @@ -104,7 +104,7 @@ struct SwiftKnownTypes { ) } - func optionalSugar(_ wrappedType: SwiftType) -> SwiftType { + public func optionalSugar(_ wrappedType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( sugarName: .optional, @@ -114,7 +114,7 @@ struct SwiftKnownTypes { ) } - func arraySugar(_ elementType: SwiftType) -> SwiftType { + public func arraySugar(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( sugarName: .array, @@ -124,7 +124,7 @@ struct SwiftKnownTypes { ) } - func dictionarySugar(_ keyType: SwiftType, _ valueType: SwiftType) -> SwiftType { + public func dictionarySugar(_ keyType: SwiftType, _ valueType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( sugarName: .dictionary, @@ -134,7 +134,7 @@ struct SwiftKnownTypes { ) } - func set(_ elementType: SwiftType) -> SwiftType { + public func set(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.set], @@ -145,13 +145,13 @@ struct SwiftKnownTypes { /// Returns the known representative concrete type if there is one for the /// given protocol kind. E.g. `Data` for `DataProtocol` - func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftType? { + public func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftType? { guard let kind = Self.representativeType(of: knownProtocol) else { return nil } return .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[kind])) } /// Returns the representative concrete type kind for a protocol, if one exists - static func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftKnownTypeDeclKind? { + public static func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftKnownTypeDeclKind? { switch knownProtocol { case .foundationDataProtocol: return .foundationData case .essentialsDataProtocol: return .essentialsData diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift b/Sources/SwiftExtract/SwiftTypes/SwiftModuleSymbolTable.swift similarity index 63% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftModuleSymbolTable.swift index c77038596..5e4f5ba6a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftModuleSymbolTable.swift @@ -15,63 +15,63 @@ import SwiftSyntax import SwiftSyntaxBuilder -struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { +public struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { /// The name of this module. - let moduleName: String + public let moduleName: String /// The name of module required to be imported and checked via canImport statement. - let requiredAvailablityOfModuleWithName: String? + public let requiredAvailablityOfModuleWithName: String? /// Data about alternative modules which provides desired symbos e.g. FoundationEssentials is non-Darwin platform alternative for Foundation - let alternativeModules: AlternativeModuleNamesData? + public let alternativeModules: AlternativeModuleNamesData? /// The top-level nominal types, found by name. - var topLevelTypes: [String: SwiftNominalTypeDeclaration] = [:] + public var topLevelTypes: [String: SwiftNominalTypeDeclaration] = [:] /// The top-level typealias declarations, found by name. - var topLevelTypeAliases: [String: SwiftTypeAliasDeclaration] = [:] + public var topLevelTypeAliases: [String: SwiftTypeAliasDeclaration] = [:] /// The nested types defined within this module. The map itself is indexed by the /// identifier of the nominal type declaration, and each entry is a map from the nested /// type name to the nominal type declaration. - var nestedTypes: [SwiftNominalTypeDeclaration: [String: SwiftNominalTypeDeclaration]] = [:] + public var nestedTypes: [SwiftNominalTypeDeclaration: [String: SwiftNominalTypeDeclaration]] = [:] /// The nested typealias declarations defined within this module. The map itself is indexed /// by the nominal type declaration, and each entry is a map from the nested typealias /// name to the typealias declaration. - var nestedTypeAliases: [SwiftNominalTypeDeclaration: [String: SwiftTypeAliasDeclaration]] = [:] + public var nestedTypeAliases: [SwiftNominalTypeDeclaration: [String: SwiftTypeAliasDeclaration]] = [:] /// Look for a top-level type with the given name. - func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { + public func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { topLevelTypes[name] } /// Look for a top-level typealias with the given name. - func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { + public func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { topLevelTypeAliases[name] } // Look for a nested type with the given name. - func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { + public func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { nestedTypes[parent]?[name] } // Look for a nested typealias with the given name. - func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { + public func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { nestedTypeAliases[parent]?[name] } - func isAlternative(for moduleName: String) -> Bool { + public func isAlternative(for moduleName: String) -> Bool { alternativeModules.flatMap { $0.moduleNames.contains(moduleName) } ?? false } } extension SwiftModuleSymbolTable { - struct AlternativeModuleNamesData { + public struct AlternativeModuleNamesData { /// Flag indicating module should be used as source of symbols to avoid duplication of symbols. - let isMainSourceOfSymbols: Bool + public let isMainSourceOfSymbols: Bool /// Names of modules which are alternative for currently checked module. - let moduleNames: Set + public let moduleNames: Set } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift similarity index 65% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift index 07543bf23..31a076648 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -14,26 +14,25 @@ import SwiftSyntax -///// A syntax node for a nominal type declaration. -@_spi(Testing) +/// A syntax node for a nominal type declaration. 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 +40,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 +52,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,35 +62,65 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } /// The syntax node this declaration is derived from. - let syntax: NominalTypeDeclSyntaxNode + 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] + + /// True when this declaration is a placeholder synthesized by + /// `SwiftSyntheticTypes.unresolvedNominal(_:)` because the symbol table + /// couldn't resolve the name. + /// + /// Exists to support **lazy specializations** — code generators that + /// resolve names later than analysis time. Without lenient mode the + /// analyzer would drop entire declarations the moment any unresolved name + /// appeared in their signature; lenient mode keeps the decl and stamps the + /// unknown name with this flag, letting a downstream substitution pass + /// recognize and replace it. + /// + /// Canonical case: an associated type in a protocol requirement. For + /// + /// protocol Container { + /// associatedtype Element + /// func first() -> Element + /// } + /// + /// the symbol table has no top-level `Element` when walking `first()` — it + /// resolves later when a conforming type fixes a carrier + /// (`MyCollection: Container where Element == Int`). Strict mode would + /// drop `first()` from the analysis result; lenient mode keeps it with + /// `Element` as a placeholder a later substitution pass replaces with the + /// carrier's real type. Generic-parameter references in the body of a + /// generic type and externally-bridged simple-name types follow the same + /// shape. + public let isUnresolvedTypePlaceholder: Bool /// 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( + public init( name: String, sourceFilePath: String, moduleName: String, parent: SwiftNominalTypeDeclaration?, node: NominalTypeDeclSyntaxNode, + isUnresolvedTypePlaceholder: Bool = false, ) { self.parent = parent self.syntax = node + self.isUnresolvedTypePlaceholder = isUnresolvedTypePlaceholder self.genericParameters = node.asProtocol(WithGenericParametersSyntax.self)?.genericParameterClause?.parameters.map { SwiftGenericParameterDeclaration(sourceFilePath: sourceFilePath, moduleName: moduleName, node: $0) @@ -109,7 +138,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 +146,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 +181,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 +189,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 +208,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 +223,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 +235,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 +247,7 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { } } - var packExpansionName: String { + public var packExpansionName: String { if hasEach { "repeat each \(name)" } else { @@ -232,10 +261,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 +275,13 @@ package final class SwiftTypeAliasDeclaration: SwiftTypeDeclaration { } extension SwiftTypeDeclaration: Equatable { - package static func == (lhs: SwiftTypeDeclaration, rhs: SwiftTypeDeclaration) -> Bool { + public static func == (lhs: SwiftTypeDeclaration, rhs: SwiftTypeDeclaration) -> Bool { lhs === rhs } } extension SwiftTypeDeclaration: Hashable { - package func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift similarity index 59% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift index 56fe3cb9c..71db093df 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift @@ -14,22 +14,62 @@ import SwiftSyntax -struct SwiftParameter: Equatable { - var convention: SwiftParameterConvention - var argumentLabel: String? - var parameterName: String? - var type: SwiftType +public struct SwiftParameter: Equatable { + public var convention: SwiftParameterConvention + public var argumentLabel: String? + public var parameterName: String? + public var type: SwiftType + + /// Whether this is a variadic parameter (`T...`). + public var isVariadic: Bool + /// Whether the parameter declares a default value. + public var hasDefaultValue: Bool + /// The default-value expression source, if any (e.g. `42`, `[]`). + public var defaultValueExpression: String? + + public init( + convention: SwiftParameterConvention, + argumentLabel: String? = nil, + parameterName: String? = nil, + type: SwiftType, + isVariadic: Bool = false, + hasDefaultValue: Bool = false, + defaultValueExpression: String? = nil + ) { + self.convention = convention + self.argumentLabel = argumentLabel + self.parameterName = parameterName + self.type = type + self.isVariadic = isVariadic + self.hasDefaultValue = hasDefaultValue + self.defaultValueExpression = defaultValueExpression + } + + /// The simple parameter name, falling back to the argument label. + public var name: String { + parameterName ?? argumentLabel ?? "_" + } + + /// The external argument label, falling back to the parameter name. + public var effectiveLabel: String { + argumentLabel ?? name + } + + /// Whether this parameter is passed `inout`. + public var isInout: Bool { + convention == .inout + } } extension SwiftParameter: CustomStringConvertible { - var description: String { + public var description: String { let argumentLabel = self.argumentLabel ?? "_" let parameterName = self.parameterName ?? "_" return "\(argumentLabel) \(parameterName): \(descriptionInType)" } - var descriptionInType: String { + public var descriptionInType: String { let conventionString: String switch convention { case .byValue: @@ -47,7 +87,7 @@ extension SwiftParameter: CustomStringConvertible { } /// Describes how a parameter is passed. -enum SwiftParameterConvention: Equatable { +public enum SwiftParameterConvention: Equatable { /// The parameter is passed by-value or borrowed. case byValue /// The parameter is passed by-value but consumed. @@ -57,17 +97,20 @@ enum SwiftParameterConvention: Equatable { } extension SwiftParameter { - init(_ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { + public init(_ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { self.convention = .byValue self.type = try SwiftType(node.type, lookupContext: lookupContext) self.argumentLabel = nil self.parameterName = node.firstName?.identifier?.name self.argumentLabel = node.firstName?.identifier?.name + self.isVariadic = false + self.hasDefaultValue = node.defaultValue != nil + self.defaultValueExpression = node.defaultValue?.value.trimmedDescription } } extension SwiftParameter { - init(_ node: FunctionParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { + public init(_ node: FunctionParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { // Determine the convention. The default is by-value, but there are // specifiers on the type for other conventions (like `inout`). var type = node.type @@ -101,6 +144,11 @@ extension SwiftParameter { // Determine the type. self.type = try SwiftType(type, lookupContext: lookupContext) + // Variadic / default-value information. + self.isVariadic = node.ellipsis != nil + self.hasDefaultValue = node.defaultValue != nil + self.defaultValueExpression = node.defaultValue?.value.trimmedDescription + // FIXME: swift-syntax itself should have these utilities based on identifiers. if let secondName = node.secondName { self.argumentLabel = node.firstName.identifier?.name diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift b/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift similarity index 90% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift index 8691cdfa1..560ed9c0c 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift @@ -12,30 +12,31 @@ // //===----------------------------------------------------------------------===// +import Logging import SwiftIfConfig import SwiftSyntax -struct SwiftParsedModuleSymbolTableBuilder { - let log: Logger? +package struct SwiftParsedModuleSymbolTableBuilder { + package let log: Logger? /// The symbol table being built. - var symbolTable: SwiftModuleSymbolTable + package var symbolTable: SwiftModuleSymbolTable /// Imported modules to resolve type syntax. - let importedModules: [String: SwiftModuleSymbolTable] + package let importedModules: [String: SwiftModuleSymbolTable] /// The build configuration used to resolve #if conditional compilation blocks. - let buildConfig: any BuildConfiguration + package let buildConfig: any BuildConfiguration /// Extension decls their extended type hasn't been resolved. - var unresolvedExtensions: [ExtensionDeclSyntax] + package var unresolvedExtensions: [ExtensionDeclSyntax] - init( + package init( moduleName: String, requiredAvailablityOfModuleWithName: String? = nil, alternativeModules: SwiftModuleSymbolTable.AlternativeModuleNamesData? = nil, importedModules: [String: SwiftModuleSymbolTable], - buildConfig: any BuildConfiguration = .jextractDefault, + buildConfig: any BuildConfiguration = .swiftExtractDefault, log: Logger? = nil ) { self.log = log @@ -49,14 +50,14 @@ struct SwiftParsedModuleSymbolTableBuilder { self.unresolvedExtensions = [] } - var moduleName: String { + package var moduleName: String { symbolTable.moduleName } } extension SwiftParsedModuleSymbolTableBuilder { - mutating func handle( + package mutating func handle( sourceFile: SourceFileSyntax, sourceFilePath: String ) { @@ -66,7 +67,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( codeBlockItem node: CodeBlockItemSyntax.Item, sourceFilePath: String ) { @@ -88,7 +89,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( typeAliasDecl node: TypeAliasDeclSyntax, sourceFilePath: String ) { @@ -108,7 +109,7 @@ extension SwiftParsedModuleSymbolTableBuilder { /// Add a nominal type declaration and all of the nested types within it to the symbol /// table. - mutating func handle( + package mutating func handle( sourceFilePath: String, nominalTypeDecl node: NominalTypeDeclSyntaxNode, parent: SwiftNominalTypeDeclaration? @@ -118,7 +119,7 @@ extension SwiftParsedModuleSymbolTableBuilder { if symbolTable.lookupType(node.name.text, parent: parent) != nil || symbolTable.lookupTypealias(node.name.text, parent: parent) != nil { - log?.debug("Failed to add a decl into symbol table: redeclaration; " + node.nameForDebug) + log?.debug("Failed to add a decl into symbol table: redeclaration; \(node.nameForDebug)") return } @@ -142,7 +143,7 @@ extension SwiftParsedModuleSymbolTableBuilder { self.handle(sourceFilePath: sourceFilePath, memberBlock: node.memberBlock, parent: nominalTypeDecl) } - mutating func handle( + package mutating func handle( sourceFilePath: String, typeAliasDecl node: TypeAliasDeclSyntax, parent: SwiftNominalTypeDeclaration? @@ -150,7 +151,7 @@ extension SwiftParsedModuleSymbolTableBuilder { if symbolTable.lookupType(node.name.text, parent: parent) != nil || symbolTable.lookupTypealias(node.name.text, parent: parent) != nil { - log?.debug("Failed to add a decl into symbol table: redeclaration; " + node.nameForDebug) + log?.debug("Failed to add a decl into symbol table: redeclaration; \(node.nameForDebug)") return } @@ -167,7 +168,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( sourceFilePath: String, memberBlock node: MemberBlockSyntax, parent: SwiftNominalTypeDeclaration @@ -182,7 +183,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( extensionDecl node: ExtensionDeclSyntax, sourceFilePath: String ) { @@ -193,7 +194,7 @@ extension SwiftParsedModuleSymbolTableBuilder { /// Add any nested types within the given extension to the symbol table. /// If the extended nominal type can't be resolved, returns false. - mutating func tryHandle( + package mutating func tryHandle( extension node: ExtensionDeclSyntax, sourceFilePath: String ) -> Bool { @@ -217,7 +218,7 @@ extension SwiftParsedModuleSymbolTableBuilder { return true } - mutating func handle( + package mutating func handle( ifConfig node: IfConfigDeclSyntax, sourceFilePath: String ) { @@ -235,7 +236,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } /// Finalize the symbol table and return it. - mutating func finalize() -> SwiftModuleSymbolTable { + package mutating func finalize() -> SwiftModuleSymbolTable { // Handle the unresolved extensions. // The work queue is required because, the extending type might be declared // in another extension that hasn't been processed. E.g.: @@ -264,7 +265,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } extension DeclSyntaxProtocol { - var asNominal: NominalTypeDeclSyntaxNode? { + package var asNominal: NominalTypeDeclSyntaxNode? { switch DeclSyntax(self).as(DeclSyntaxEnum.self) { case .actorDecl(let actorDecl): actorDecl case .classDecl(let classDecl): classDecl diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftQualifiedTypeName.swift b/Sources/SwiftExtract/SwiftTypes/SwiftQualifiedTypeName.swift similarity index 71% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftQualifiedTypeName.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftQualifiedTypeName.swift index 2c170ddb7..71cf956ca 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftQualifiedTypeName.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftQualifiedTypeName.swift @@ -18,33 +18,33 @@ /// - **qualifiedName** (`Logger.Message`) - for Swift source /// - **flatName** (`Logger_Message`) - for C symbols / `@_cdecl` and Java identifiers /// - **leafName** (`Message`) - innermost component only -package struct SwiftQualifiedTypeName: Hashable, Sendable, CustomStringConvertible { +public struct SwiftQualifiedTypeName: Hashable, Sendable, CustomStringConvertible { /// Name components from outermost to innermost, e.g. ["Logger", "Message"] - let components: [String] + public let components: [String] - init(_ components: [String]) { + public init(_ components: [String]) { precondition(!components.isEmpty) self.components = components } - init(_ leafName: String) { + public init(_ leafName: String) { self.components = [leafName] } /// Leaf name (innermost), e.g. "Message" - var leafName: String { components.last! } + public var leafName: String { components.last! } /// Dot-separated for Swift source, e.g. "Logger.Message" - var fullName: String { components.joined(separator: ".") } + public var fullName: String { components.joined(separator: ".") } /// Underscore-separated for C symbols and Java identifiers, e.g. "Logger_Message" - var fullFlatName: String { components.joined(separator: "_") } + public var fullFlatName: String { components.joined(separator: "_") } /// Dollar-separated for JNI C symbol parent names, e.g. "Logger$Message" - var jniEscapedName: String { components.joined(separator: "$") } + public var jniEscapedName: String { components.joined(separator: "$") } /// CustomStringConvertible - uses fullName - package var description: String { fullName } + public var description: String { fullName } - var isNested: Bool { components.count > 1 } + public var isNested: Bool { components.count > 1 } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftResult.swift b/Sources/SwiftExtract/SwiftTypes/SwiftResult.swift similarity index 64% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftResult.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftResult.swift index b7aa7b568..87fb57ea6 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftResult.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftResult.swift @@ -14,18 +14,23 @@ import SwiftSyntax -struct SwiftResult: Equatable { - var convention: SwiftResultConvention // currently not used. - var type: SwiftType +public struct SwiftResult: Equatable { + public var convention: SwiftResultConvention // currently not used. + public var type: SwiftType + + public init(convention: SwiftResultConvention, type: SwiftType) { + self.convention = convention + self.type = type + } } -enum SwiftResultConvention: Equatable { +public enum SwiftResultConvention: Equatable { case direct case indirect } extension SwiftResult { - static var void: Self { + public static var void: Self { Self(convention: .direct, type: .void) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift similarity index 65% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift index a4c4de73b..45557b7ac 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift @@ -12,13 +12,12 @@ // //===----------------------------------------------------------------------===// -import CodePrinting +import Logging import SwiftIfConfig -import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -package protocol SwiftSymbolTableProtocol { +public protocol SwiftSymbolTableProtocol { /// The module name that this symbol table describes. var moduleName: String { get } @@ -38,7 +37,7 @@ package protocol SwiftSymbolTableProtocol { extension SwiftSymbolTableProtocol { /// Look for a type - package func lookupType(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftNominalTypeDeclaration? { + public func lookupType(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftNominalTypeDeclaration? { if let parent { return lookupNestedType(name, parent: parent) } @@ -46,7 +45,7 @@ extension SwiftSymbolTableProtocol { return lookupTopLevelNominalType(name) } - package func lookupTypealias(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftTypeAliasDeclaration? { + public func lookupTypealias(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftTypeAliasDeclaration? { if let parent { return lookupNestedTypealias(name, parent: parent) } @@ -55,9 +54,14 @@ extension SwiftSymbolTableProtocol { } } -package class SwiftSymbolTable { - let importedModules: [String: SwiftModuleSymbolTable] - let parsedModule: SwiftModuleSymbolTable +public class SwiftSymbolTable { + public let importedModules: [String: SwiftModuleSymbolTable] + public let parsedModule: SwiftModuleSymbolTable + + /// Module names within `importedModules` that are synthetic — they exist + /// purely to drive type resolution and must NOT be emitted as + /// `import ` statements in generated Swift code. + public let syntheticImportedModuleNames: Set private var knownTypeToNominal: [SwiftKnownTypeDeclKind: SwiftNominalTypeDeclaration] = [:] private var prioritySortedImportedModules: [SwiftModuleSymbolTable] { @@ -72,12 +76,17 @@ package class SwiftSymbolTable { }) } - init(parsedModule: SwiftModuleSymbolTable, importedModules: [String: SwiftModuleSymbolTable]) { + public init( + parsedModule: SwiftModuleSymbolTable, + importedModules: [String: SwiftModuleSymbolTable], + syntheticImportedModuleNames: Set = [] + ) { self.parsedModule = parsedModule self.importedModules = importedModules + self.syntheticImportedModuleNames = syntheticImportedModuleNames } - func isModuleName(_ name: String) -> Bool { + public func isModuleName(_ name: String) -> Bool { if name == moduleName { return true } @@ -88,16 +97,17 @@ package class SwiftSymbolTable { extension SwiftSymbolTable { package static func setup( moduleName: String, - _ inputFiles: some Collection, - config: Configuration?, + _ inputFiles: some Collection, + additionalInputFiles: [SwiftInputFile] = [], + config: (any SwiftExtractConfiguration)?, sourceDependencies: SourceDependencies, - buildConfig: any BuildConfiguration = .jextractDefault, - log: Logger, + buildConfig: any BuildConfiguration = .swiftExtractDefault, + log: Logger? = nil, ) -> SwiftSymbolTable { // Prepare imported modules. // FIXME: Support arbitrary dependencies. - var modules: Set = [] + var modules: Set = [] for inputFile in inputFiles { let importedModules = importingModules(sourceFile: inputFile.syntax) modules.formUnion(importedModules) @@ -122,7 +132,10 @@ extension SwiftSymbolTable { guard importedModules[dependencyModuleName] == nil else { continue } - let dependencyInputs = sourceDependencies.swiftModuleInputs[dependencyModuleName] ?? [] + let dependencyInputs = + sourceDependencies.swiftModuleInputs[dependencyModuleName] + ?? sourceDependencies.syntheticStubInputs[dependencyModuleName] + ?? [] // TODO: build a `dependencyImportedModules` dict by scanning the dep's // own source files with `importingModules(sourceFile:)`, instead of // reusing the primary's `importedModules`. The current set is too broad @@ -138,9 +151,8 @@ extension SwiftSymbolTable { } let dependencyModule = dependencyModuleBuilder.finalize() importedModules[dependencyModuleName] = dependencyModule - log.info( - "Loaded dependency module '\(dependencyModuleName)' from \(dependencyInputs.count) source(s); " - + "top-level types [\(dependencyModule.topLevelTypes.count)]: \(dependencyModule.topLevelTypes.keys.sorted())" + log?.info( + "Loaded dependency module '\(dependencyModuleName)' from \(dependencyInputs.count) source(s); top-level types [\(dependencyModule.topLevelTypes.count)]: \(dependencyModule.topLevelTypes.keys.sorted())" ) } @@ -160,13 +172,13 @@ extension SwiftSymbolTable { stubBuilder.handle(sourceFile: sourceFile, sourceFilePath: "\(stubModuleName)_stub.swift") let stubModule = stubBuilder.finalize() importedModules[stubModuleName] = stubModule - log.info("Loaded module stub for '\(stubModuleName)' with \(declarations.count) declaration(s), top-level types: \(stubModule.topLevelTypes.keys.sorted())") + log?.info("Loaded module stub for '\(stubModuleName)' with \(declarations.count) declaration(s), top-level types: \(stubModule.topLevelTypes.keys.sorted())") } else { - log.info("Module '\(stubModuleName)' already known, skipping stub") + log?.info("Module '\(stubModuleName)' already known, skipping stub") } } } else { - log.debug("No importedModuleStubs in config") + log?.debug("No importedModuleStubs in config") } // FIXME: Support granular lookup context (file, type context). @@ -181,20 +193,24 @@ extension SwiftSymbolTable { for sourceFile in inputFiles { builder.handle(sourceFile: sourceFile.syntax, sourceFilePath: sourceFile.path) } - if let stubs = sourceDependencies.syntheticJavaWrappersSwiftSource { - builder.handle(sourceFile: stubs.syntax, sourceFilePath: stubs.path) + for sourceFile in additionalInputFiles { + builder.handle(sourceFile: sourceFile.syntax, sourceFilePath: sourceFile.path) } let parsedModule = builder.finalize() - return SwiftSymbolTable(parsedModule: parsedModule, importedModules: importedModules) + return SwiftSymbolTable( + parsedModule: parsedModule, + importedModules: importedModules, + syntheticImportedModuleNames: sourceDependencies.syntheticModuleNames, + ) } } extension SwiftSymbolTable: SwiftSymbolTableProtocol { - package var moduleName: String { parsedModule.moduleName } + public var moduleName: String { parsedModule.moduleName } /// Look for a top-level nominal type with the given name. This should only /// return nominal types within this module. - package func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { + public func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { if let parsedResult = parsedModule.lookupTopLevelNominalType(name) { return parsedResult } @@ -209,7 +225,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } /// Look for a top-level nominal type in a specific module by name - package func lookupTopLevelNominalType(_ name: String, inModule moduleName: String) -> SwiftNominalTypeDeclaration? { + public func lookupTopLevelNominalType(_ name: String, inModule moduleName: String) -> SwiftNominalTypeDeclaration? { if moduleName == self.moduleName { return parsedModule.lookupTopLevelNominalType(name) } @@ -217,7 +233,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } /// Look for a top-level typealias with the given name. - package func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { + public func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { if let parsedResult = parsedModule.lookupTopLevelTypealias(name) { return parsedResult } @@ -232,7 +248,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } // Look for a nested type with the given name. - package func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { + public func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { if let parsedResult = parsedModule.lookupNestedType(name, parent: parent) { return parsedResult } @@ -247,7 +263,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } // Look for a nested typealias with the given name. - package func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { + public func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { if let parsedResult = parsedModule.lookupNestedTypealias(name, parent: parent) { return parsedResult } @@ -264,7 +280,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { extension SwiftSymbolTable { /// Map 'SwiftKnownTypeDeclKind' to the declaration. - subscript(knownType: SwiftKnownTypeDeclKind) -> SwiftNominalTypeDeclaration! { + public subscript(knownType: SwiftKnownTypeDeclKind) -> SwiftNominalTypeDeclaration! { if let known = knownTypeToNominal[knownType] { return known } @@ -279,63 +295,3 @@ extension SwiftSymbolTable { return found } } - -extension SwiftSymbolTable { - func printImportedModules(_ printer: inout CodePrinter) { - let mainSymbolSourceModules = Set( - self.importedModules.values.filter { $0.alternativeModules?.isMainSourceOfSymbols ?? false }.map(\.moduleName) - ) - - for module in self.importedModules.keys.sorted() { - guard module != "Swift" else { - continue - } - - guard let alternativeModules = self.importedModules[module]?.alternativeModules else { - printer.print("import \(module)") - continue - } - - // Only the main source of symbols emits the conditional import block. - // Secondary modules (e.g. FoundationEssentials when Foundation is the main source) - // are skipped when their main source is already present, because the main source's - // block already covers the import. If no main source is present, fall back to a - // plain import so the module is still imported. - guard alternativeModules.isMainSourceOfSymbols else { - if mainSymbolSourceModules.isDisjoint(with: alternativeModules.moduleNames) { - printer.print("import \(module)") - } - continue - } - - var importGroups: [String: [String]] = [:] - for name in alternativeModules.moduleNames { - guard let otherModule = self.importedModules[name] else { continue } - - let groupKey = otherModule.requiredAvailablityOfModuleWithName ?? otherModule.moduleName - importGroups[groupKey, default: []].append(otherModule.moduleName) - } - - for (index, group) in importGroups.keys.sorted().enumerated() { - if index > 0 && importGroups.keys.count > 1 { - printer.print("#elseif canImport(\(group))") - } else { - printer.print("#if canImport(\(group))") - } - - for groupModule in importGroups[group] ?? [] { - printer.print("import \(groupModule)") - } - } - - if importGroups.keys.isEmpty { - printer.print("import \(module)") - } else { - printer.print("#else") - printer.print("import \(module)") - printer.print("#endif") - } - } - printer.println() - } -} diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift b/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift new file mode 100644 index 000000000..90fd7991d --- /dev/null +++ b/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftParser +import SwiftSyntax + +// ==== ----------------------------------------------------------------------- +// MARK: SwiftSyntheticTypes +// +// `SwiftType` is normally only constructable from a real source declaration +// the symbol table can resolve. A few callers — primarily downstream language +// code generators that treat unresolved names symbolically (see +// `SwiftNominalTypeDeclaration.isUnresolvedTypePlaceholder` for the +// lazy-specialization motivation) — need to build an unresolved nominal +// reference from a bare type name. This builds one by parsing a throwaway +// `struct {}` to obtain the syntax node the +// `SwiftNominalTypeDeclaration` initializer requires. + +public enum SwiftSyntheticTypes { + /// Build an unresolved nominal `SwiftType` for the given simple type name. + /// The result has `isUnresolvedTypePlaceholder == true` so a downstream pass + /// can recognize and substitute it. See + /// `SwiftNominalTypeDeclaration.isUnresolvedTypePlaceholder` for usage. + public static func unresolvedNominal( + _ name: String + ) -> SwiftType { + let source = Parser.parse(source: "struct \(name) {}") + // Fall back gracefully if `name` isn't a simple identifier (the parsed + // declaration list will be empty; reuse a placeholder syntax node and + // record the requested name on the declaration itself). + let structSyntax: StructDeclSyntax + if let s = source.statements.first?.item.as(StructDeclSyntax.self) { + structSyntax = s + } else { + structSyntax = Parser.parse(source: "struct __Synthesized {}") + .statements.first!.item.cast(StructDeclSyntax.self) + } + let decl = SwiftNominalTypeDeclaration( + name: name, + sourceFilePath: "", + moduleName: "", + parent: nil, + node: structSyntax, + isUnresolvedTypePlaceholder: true + ) + return .nominal(SwiftNominalType(nominalTypeDecl: decl)) + } +} 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 79% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 49bea52d7..8671cb73d 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,24 +55,38 @@ enum SwiftType: Equatable { /// `type1` & `type2` indirect case composite([SwiftType]) - static var void: Self { + /// `[N of T]` — fixed-size `InlineArray`. `count` is `nil` when the + /// count is a wildcard `_`, a non-literal expression, or a non-positive + /// literal — downstream consumers treat these as unsupported and drop the + /// surrounding declaration. + indirect case inlineArray(count: Int?, element: SwiftType) + + 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 - case .genericParameter, .function, .metatype, .existential, .opaque, .composite: nil + case .genericParameter, .function, .metatype, .existential, .opaque, .composite, .inlineArray: nil } } - var asNominalTypeDeclaration: SwiftNominalTypeDeclaration? { + public var asNominalTypeDeclaration: SwiftNominalTypeDeclaration? { asNominalType?.nominalTypeDecl } + /// True when this type is a synthetic placeholder produced by SwiftExtract + /// for an unresolved name — see + /// `SwiftNominalTypeDeclaration.isUnresolvedTypePlaceholder` for why these + /// exist. + public var isUnresolvedTypePlaceholder: Bool { + asNominalTypeDeclaration?.isUnresolvedTypePlaceholder ?? false + } + /// 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 +99,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,18 +115,18 @@ 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 case .metatype, .function: return true - case .genericParameter, .tuple, .existential, .opaque, .composite: + case .genericParameter, .tuple, .existential, .opaque, .composite, .inlineArray: return false } } - var isUnsignedInteger: Bool { + public var isUnsignedInteger: Bool { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { @@ -118,7 +137,7 @@ enum SwiftType: Equatable { } } - var isArchDependingInteger: Bool { + public var isArchDependingInteger: Bool { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { @@ -129,7 +148,7 @@ enum SwiftType: Equatable { } } - var isRawTypeCompatible: Bool { + public var isRawTypeCompatible: Bool { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { @@ -149,11 +168,11 @@ extension SwiftType: CustomStringConvertible { private var postfixRequiresParentheses: Bool { switch self { case .function, .existential, .opaque, .composite: true - case .genericParameter, .metatype, .nominal, .tuple: false + case .genericParameter, .metatype, .nominal, .tuple, .inlineArray: false } } - var description: String { + public var description: String { switch self { case .nominal(let nominal): return nominal.description case .genericParameter(let genericParam): return genericParam.packExpansionName @@ -182,27 +201,30 @@ extension SwiftType: CustomStringConvertible { } case .composite(let types): return types.map(\.description).joined(separator: " & ") + case .inlineArray(let count, let element): + let countStr = count.map(String.init) ?? "_" + return "[\(countStr) of \(element)]" } } } -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 +237,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 +245,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 +262,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 +289,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 +298,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) } @@ -294,9 +312,36 @@ extension SwiftType { switch type.as(TypeSyntaxEnum.self) { case .classRestrictionType, .missingType, .namedOpaqueReturnType, - .packElementType, .packExpansionType, .suppressedType, .inlineArrayType: + .packElementType, .packExpansionType, .suppressedType: throw TypeTranslationError.unimplementedType(type) + case .inlineArrayType(let inlineArrayType): + // `[N of T]` — the element is a normal type argument; the count is an + // expression argument (always an integer literal in well-formed code, + // or `_` wildcard in generic constraints). + guard case .type(let elementTy) = inlineArrayType.element.argument else { + throw TypeTranslationError.unimplementedType(type) + } + let elementType = try SwiftType(elementTy, lookupContext: lookupContext) + + let count: Int? + switch inlineArrayType.count.argument { + case .expr(let expr): + if let lit = expr.as(IntegerLiteralExprSyntax.self), + let value = lit.representedLiteralValue, value > 0 + { + count = value + } else { + count = nil + } + case .type: + // Wildcard `_` or other non-literal type expression — preserved as + // unknown; downstream gates treat this as unsupported. + count = nil + } + + self = .inlineArray(count: count, element: elementType) + case .attributedType(let attributedType): // Recognize "@convention(c)", "@convention(swift)", and "@escaping" attributes on function types. // FIXME: This string matching is a horrible hack. @@ -438,7 +483,7 @@ extension SwiftType { } } - init( + public init( originalType: TypeSyntax, parent: SwiftType?, name: TokenSyntax, @@ -464,6 +509,26 @@ extension SwiftType { typeDecl = try lookupContext.unqualifiedLookup(name: ident, from: name) } guard let typeDecl else { + // Lenient mode (opt-in via SwiftExtractConfiguration.allowUnresolvedTypeReferences): + // synthesize an unresolved nominal placeholder so a downstream pass can + // substitute or recognize it. Generic-argument names are kept (so e.g. + // `Box` becomes a synthetic nominal carrying the unresolved + // `Element` argument). + if lookupContext.allowUnresolvedTypeReferences { + self = SwiftSyntheticTypes.unresolvedNominal(name.text) + if !genericArguments.isEmpty, + case .nominal(let synth) = self + { + self = .nominal( + SwiftNominalType( + parent: parent?.asNominalType, + nominalTypeDecl: synth.nominalTypeDecl, + genericArguments: genericArguments + ) + ) + } + return + } throw TypeTranslationError.unknown(originalType) } @@ -505,7 +570,7 @@ extension SwiftType { } } - init?( + public init?( nominalDecl: NamedDeclSyntax & DeclGroupSyntax, parent: SwiftType?, symbolTable: SwiftSymbolTable @@ -532,7 +597,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 { @@ -574,12 +639,17 @@ extension SwiftType { return .opaque(inner.substituting(genericParameters: substitutions)) case .composite(let types): return .composite(types.map { $0.substituting(genericParameters: substitutions) }) + case .inlineArray(let count, let element): + return .inlineArray( + count: count, + element: element.substituting(genericParameters: substitutions) + ) } } /// 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 +658,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 +670,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 +679,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 89% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift index 664fb7463..31d66f86f 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift @@ -19,8 +19,14 @@ 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 + + /// When true, name lookups that fail to resolve fall back to a synthetic + /// unresolved nominal (via `SwiftSyntheticTypes.unresolvedNominal(_:)`) + /// instead of throwing. See + /// `SwiftExtractConfiguration.allowUnresolvedTypeReferences`. + public var allowUnresolvedTypeReferences: Bool = false private var typeDecls: [Syntax.ID: SwiftTypeDeclaration] = [:] @@ -28,7 +34,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 +43,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 +52,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 +116,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 +213,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 +224,6 @@ class SwiftTypeLookupContext { } } -enum TypeLookupError: Error { +public enum TypeLookupError: Error { case notType(Syntax) } diff --git a/Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift b/Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift new file mode 100644 index 000000000..89d4325b3 --- /dev/null +++ b/Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Minimum access level a declaration must have to be considered for extraction. +/// +/// Lives in the small `SwiftExtractConfigurationShared` target so the analysis +/// layer and language-specific configuration layers (e.g. swift-java's +/// `SwiftJavaConfigurationShared`) can both depend on it without dragging +/// SwiftSyntax into the latter. +#if compiler(>=6.2) +@nonexhaustive +#endif +public enum AccessLevelMode: String, Codable, Sendable { + case `public` + case `package` + case `internal` +} + +extension AccessLevelMode { + public static var `default`: Self { + .public + } +} diff --git a/Sources/SwiftExtractConfigurationShared/LogLevel.swift b/Sources/SwiftExtractConfigurationShared/LogLevel.swift new file mode 100644 index 000000000..d02b74ea2 --- /dev/null +++ b/Sources/SwiftExtractConfigurationShared/LogLevel.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Log verbosity levels for the analysis layer's lightweight logger. +/// +/// Lives in the small `SwiftExtractConfigurationShared` target so the +/// analysis layer (`SwiftExtract`) and language-specific configuration +/// layers (e.g. swift-java's `SwiftJavaConfigurationShared`) can both +/// depend on it without dragging SwiftSyntax into the latter — same +/// shape as `AccessLevelMode`. +public enum LogLevel: String, ExpressibleByStringLiteral, Codable, Hashable, Sendable { + case trace + case debug + case info + case notice + case warning + case error + case critical + + public init(stringLiteral value: String) { + self = LogLevel(rawValue: value) ?? .info + } +} + +extension LogLevel { + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + switch string { + case "trace": self = .trace + case "debug": self = .debug + case "info": self = .info + case "notice": self = .notice + case "warning": self = .warning + case "error": self = .error + case "critical": self = .critical + default: fatalError("Unknown value for \(LogLevel.self): \(string)") + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + let text = + switch self { + case .trace: "trace" + case .debug: "debug" + case .info: "info" + case .notice: "notice" + case .warning: "warning" + case .error: "error" + case .critical: "critical" + } + try container.encode(text) + } +} + +extension LogLevel: Comparable { + public static func < (lhs: LogLevel, rhs: LogLevel) -> Bool { + lhs.naturalIntegralValue < rhs.naturalIntegralValue + } + + var naturalIntegralValue: Int { + switch self { + case .trace: return 0 + case .debug: return 1 + case .info: return 2 + case .notice: return 3 + case .warning: return 4 + case .error: return 5 + case .critical: return 6 + } + } +} diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 1562b4bf6..50d4a6f13 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -14,6 +14,13 @@ import Foundation +// In a real module build this resolves to a separate target. In plugin builds +// the file is inlined (via symlink) alongside `AccessLevelMode.swift`, so the +// module isn't a discoverable import — guard with canImport. +#if canImport(SwiftExtractConfigurationShared) +@_exported import SwiftExtractConfigurationShared +#endif + //////////////////////////////////////////////////////////////////////////////// // This file is only supposed to be edited in `Shared/` and must be symlinked // // from everywhere else! We cannot share dependencies with or between plugins // @@ -62,8 +69,8 @@ public struct Configuration: Codable { writeEmptyFiles ?? false } - public var minimumInputAccessLevelMode: JExtractMinimumAccessLevelMode? - public var effectiveMinimumInputAccessLevelMode: JExtractMinimumAccessLevelMode { + public var minimumInputAccessLevelMode: AccessLevelMode? + public var effectiveMinimumInputAccessLevelMode: AccessLevelMode { minimumInputAccessLevelMode ?? .default } @@ -660,49 +667,3 @@ public struct SpecializationConfigEntry: Codable, Sendable { self.typeArgs = typeArgs } } - -public enum LogLevel: String, ExpressibleByStringLiteral, Codable, Hashable { - case trace = "trace" - case debug = "debug" - case info = "info" - case notice = "notice" - case warning = "warning" - case error = "error" - case critical = "critical" - - public init(stringLiteral value: String) { - self = LogLevel(rawValue: value) ?? .info - } -} - -extension LogLevel { - public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let string = try container.decode(String.self) - switch string { - case "trace": self = .trace - case "debug": self = .debug - case "info": self = .info - case "notice": self = .notice - case "warning": self = .warning - case "error": self = .error - case "critical": self = .critical - default: fatalError("Unknown value for \(LogLevel.self): \(string)") - } - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - let text = - switch self { - case .trace: "trace" - case .debug: "debug" - case .info: "info" - case .notice: "notice" - case .warning: "warning" - case .error: "error" - case .critical: "critical" - } - try container.encode(text) - } -} diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift deleted file mode 100644 index 22fead577..000000000 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift +++ /dev/null @@ -1,26 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -/// The minimum access level which -public enum JExtractMinimumAccessLevelMode: String, Codable { - case `public` - case `package` - case `internal` -} - -extension JExtractMinimumAccessLevelMode { - public static var `default`: Self { - .public - } -} diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index f901eb731..020ae8202 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -66,7 +66,7 @@ extension SwiftJava { var writeEmptyFiles: Bool? @Option(help: "The lowest access level of Swift declarations that should be extracted, defaults to 'public'.") - var minimumInputAccessLevelMode: JExtractMinimumAccessLevelMode? + var minimumInputAccessLevelMode: AccessLevelMode? @Option( help: @@ -222,6 +222,6 @@ struct IllegalModeCombinationError: Error { } extension JExtractGenerationMode: ExpressibleByArgument {} -extension JExtractMinimumAccessLevelMode: ExpressibleByArgument {} +extension AccessLevelMode: ExpressibleByArgument {} extension JExtractMemoryManagementMode: ExpressibleByArgument {} extension JExtractAsyncFuncMode: ExpressibleByArgument {} diff --git a/Sources/SwiftJavaTool/CommonOptions.swift b/Sources/SwiftJavaTool/CommonOptions.swift index 8f5e7ccc1..0be3c210c 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: LogLevel = .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..b5550cd9d 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: LogLevel { 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: LogLevel { get { self.commonOptions.logLevel } diff --git a/Sources/SwiftJavaToolLib/JavaTranslator.swift b/Sources/SwiftJavaToolLib/JavaTranslator.swift index 46682bae3..354d37cf5 100644 --- a/Sources/SwiftJavaToolLib/JavaTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator.swift @@ -60,7 +60,7 @@ package class JavaTranslator { /// The set of Swift modules that need to be imported to make the generated /// code compile. Use `getImportDecls()` to format this into a list of /// import declarations. - package var importedSwiftModules: Set = JavaTranslator.defaultImportedSwiftModules + package var importedSwiftModules: Set = JavaTranslator.defaultExtractedSwiftModules /// The canonical names of Java classes whose declared 'native' /// methods will be implemented in Swift. @@ -97,7 +97,7 @@ package class JavaTranslator { /// Clear out any per-file state when we want to start a new file. package func startNewFile() { - importedSwiftModules = Self.defaultImportedSwiftModules + importedSwiftModules = Self.defaultExtractedSwiftModules } /// Simplistic logging for all entities that couldn't be translated. @@ -111,7 +111,7 @@ extension JavaTranslator { private static let defaultFormat = BasicFormat(indentationWidth: .spaces(2)) /// Default set of modules that will always be imported. - private static let defaultImportedSwiftModules: Set = [ + private static let defaultExtractedSwiftModules: Set = [ "SwiftJava", "SwiftJavaJNICore", ] diff --git a/Tests/CodePrintingTests/InlineCommentStyleTests.swift b/Tests/CodePrintingTests/InlineCommentStyleTests.swift new file mode 100644 index 000000000..c51a325ff --- /dev/null +++ b/Tests/CodePrintingTests/InlineCommentStyleTests.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// 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 CodePrinting +import Testing + +@Suite("CodePrinter.inlineCommentStyle") +struct InlineCommentStyleSuite { + + // ==== ---------------------------------------------------------------------- + // MARK: Default behavior is `//` + + @Test func defaultStyleEmitsSlashSlashSourceLocation() { + var p = CodePrinter() + p.print("hello", .sloc, function: "fn", file: "F.swift", line: 1) + + #expect(p.contents.contains("// fn @ F.swift:1")) + #expect(!p.contents.contains("# fn @ F.swift:1")) + } + + // ==== ---------------------------------------------------------------------- + // MARK: `.hash` flips comment lead + + @Test func hashStyleEmitsHashSourceLocation() { + var p = CodePrinter() + p.inlineCommentStyle = .hash + p.print("hello", .sloc, function: "fn", file: "F.swift", line: 1) + + #expect(p.contents.contains("# fn @ F.swift:1")) + #expect(!p.contents.contains("// fn @ F.swift:1")) + } + + @Test func hashStyleFlipsPrintSeparatorBanner() { + var p = CodePrinter() + p.inlineCommentStyle = .hash + p.printSeparator("section") + + #expect(p.contents.contains("# ====")) + #expect(p.contents.contains("# section")) + #expect(!p.contents.contains("// ====")) + } + + // ==== ---------------------------------------------------------------------- + // MARK: emitSourceLocations off still respects style + + @Test func emitSourceLocationsOffSuppressesTrailerRegardlessOfStyle() { + var p = CodePrinter() + p.emitSourceLocations = false + p.inlineCommentStyle = .hash + p.print("hello", .sloc, function: "fn", file: "F.swift", line: 1) + + #expect(!p.contents.contains("# fn @")) + #expect(!p.contents.contains("// fn @")) + #expect(p.contents.hasSuffix("hello\n")) + } +} diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index 06d1d5553..645a29133 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 = makeSwiftJavaAnalyzer(config: config) 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 = makeSwiftJavaAnalyzer(config: config) 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..f00a4bec4 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 = makeSwiftJavaAnalyzer(config: config) + 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..67f7c54ee 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 = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -46,6 +47,6 @@ final class FFMNestedTypesTests { javaOutputDirectory: "/fake" ) - #expect(st.importedTypes["MyNamespace.MyNestedStruct"] != nil, "Didn't import nested type!") + #expect(st.extractedTypes["MyNamespace.MyNestedStruct"] != nil, "Didn't import nested type!") } } diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index fd9203750..7aa13994b 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -41,12 +42,12 @@ final class FuncCallbackImportTests { func func_callMeFunc_callback() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) - let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMe" }! + let funcDecl = st.extractedGlobalFuncs.first { $0.name == "callMe" }! let generator = FFMSwift2JavaGenerator( config: config, @@ -131,11 +132,11 @@ final class FuncCallbackImportTests { func func_callMeMoreFunc_callback() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) - let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMeMore" }! + let funcDecl = st.extractedGlobalFuncs.first { $0.name == "callMeMore" }! let generator = FFMSwift2JavaGenerator( config: config, @@ -246,12 +247,12 @@ final class FuncCallbackImportTests { func func_withBuffer_body() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) - let funcDecl = st.importedGlobalFuncs.first { $0.name == "withBuffer" }! + let funcDecl = st.extractedGlobalFuncs.first { $0.name == "withBuffer" }! let generator = FFMSwift2JavaGenerator( config: config, diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index c514a12fd..7bdefe2ea 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 @@ -232,17 +233,17 @@ extension FunctionDescriptorTests { _ methodIdentifier: String, javaPackage: String = "com.example.swift", swiftModuleName: String = "SwiftModule", - logLevel: Logger.Level = .trace, + logLevel: LogLevel = .trace, body: (String) throws -> Void ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = logLevel try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) - let funcDecl = st.importedGlobalFuncs.first { + let funcDecl = st.extractedGlobalFuncs.first { $0.name == methodIdentifier }! @@ -266,12 +267,12 @@ extension FunctionDescriptorTests { _ accessorKind: SwiftAPIKind, javaPackage: String = "com.example.swift", swiftModuleName: String = "SwiftModule", - logLevel: Logger.Level = .trace, + logLevel: LogLevel = .trace, body: (String) throws -> Void ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = logLevel try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) @@ -284,8 +285,8 @@ extension FunctionDescriptorTests { javaOutputDirectory: "/fake" ) - let accessorDecl: ImportedFunc? = - st.importedTypes.values.compactMap { + let accessorDecl: ExtractedFunc? = + st.extractedTypes.values.compactMap { $0.variables.first { $0.name == identifier && $0.apiKind == accessorKind } diff --git a/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift b/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift index b89e574f7..6b12465ce 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 = makeSwiftJavaAnalyzer(config: config) translator.log.logLevel = .error try translator.analyze(path: "Fake.swift", text: Self.nestedTypeSource) return translator @@ -343,37 +344,37 @@ struct JExtractFileFilterTests { @Test("swiftFilterExclude with exact nested type name") func excludeExactNested() throws { let st = try makeTranslator(exclude: ["Tank.Internal"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("swiftFilterExclude with `Type.*` excludes direct children only") func excludeDirectChildren() throws { let st = try makeTranslator(exclude: ["Tank.*"]) - #expect(st.importedTypes["Tank"] != nil, "Top-level Tank itself should not be excluded by `Tank.*`") - #expect(st.importedTypes["Tank.Fish"] == nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil, "Top-level Tank itself should not be excluded by `Tank.*`") + #expect(st.extractedTypes["Tank.Fish"] == nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("swiftFilterExclude with suffix wildcard inside nested name") func excludeSuffixWildcard() throws { let st = try makeTranslator(exclude: ["Tank.Inter*"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("swiftFilterExclude with `**.Name` matches at any depth") func excludeRecursiveLeaf() throws { let st = try makeTranslator(exclude: ["**.Internal"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("plain-name swiftFilterExclude excludes top-level type and its nested members") @@ -381,10 +382,10 @@ struct JExtractFileFilterTests { // Plain pattern matches the top-level component, so excluding `Tank` also // prevents the visitor from descending into its nested types let st = try makeTranslator(exclude: ["Tank"]) - #expect(st.importedTypes["Tank"] == nil) - #expect(st.importedTypes["Tank.Fish"] == nil) - #expect(st.importedTypes["Tank.Internal"] == nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] == nil) + #expect(st.extractedTypes["Tank.Fish"] == nil) + #expect(st.extractedTypes["Tank.Internal"] == nil) + #expect(st.extractedTypes["FishTank"] != nil) } @Test("swiftFilterInclude with `Type.**` keeps the parent and all nested members") @@ -392,10 +393,10 @@ struct JExtractFileFilterTests { // `Tank.**` is a type-name pattern; via the trailing-`**` rule it matches // both `Tank` itself and any nested type underneath let st = try makeTranslator(include: ["Tank.**"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] != nil) - #expect(st.importedTypes["FishTank"] == nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] != nil) + #expect(st.extractedTypes["FishTank"] == nil) } @Test("file-path-only filter does not interfere with nested-type extraction") @@ -403,9 +404,9 @@ struct JExtractFileFilterTests { // A file-path-only filter must not accidentally gate type-level filtering; // every nested type in the included file should still be extracted let st = try makeTranslator(include: ["**/Fake.swift", "Fake.swift"]) - #expect(st.importedTypes["Tank"] != nil) - #expect(st.importedTypes["Tank.Fish"] != nil) - #expect(st.importedTypes["Tank.Internal"] != nil) - #expect(st.importedTypes["FishTank"] != nil) + #expect(st.extractedTypes["Tank"] != nil) + #expect(st.extractedTypes["Tank.Fish"] != nil) + #expect(st.extractedTypes["Tank.Internal"] != nil) + #expect(st.extractedTypes["FishTank"] != nil) } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift index 5339e07d6..ffff38b8b 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 = makeSwiftJavaAnalyzer(config: config) 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 678693d00..e4c866e14 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 = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -84,7 +85,7 @@ final class MethodImportTests { javaOutputDirectory: "/fake" ) - let funcDecl = try #require(st.importedGlobalFuncs.first { $0.name == "helloWorld" }) + let funcDecl = try #require(st.extractedGlobalFuncs.first { $0.name == "helloWorld" }) let output = CodePrinter.toString { printer in generator.printJavaBindingWrapperMethod(&printer, funcDecl) @@ -113,13 +114,13 @@ final class MethodImportTests { func func_globalTakeInt() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "globalTakeInt" } ) @@ -162,13 +163,13 @@ final class MethodImportTests { func func_globalTakeIntLongString() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "globalTakeIntLongString" } ) @@ -208,13 +209,13 @@ final class MethodImportTests { func func_globalReturnClass() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "globalReturnClass" } ) @@ -254,13 +255,13 @@ final class MethodImportTests { func func_globalSwapRawBufferPointer() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "swapRawBufferPointer" } ) @@ -303,13 +304,13 @@ final class MethodImportTests { func method_class_helloMemberFunction() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) - let funcDecl: ImportedFunc = try #require( - st.importedTypes["MySwiftClass"]!.methods.first { + let funcDecl: ExtractedFunc = try #require( + st.extractedTypes["MySwiftClass"]!.methods.first { $0.name == "helloMemberFunction" } ) @@ -348,13 +349,13 @@ final class MethodImportTests { func method_class_makeInt() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) - let funcDecl: ImportedFunc = try #require( - st.importedTypes["MySwiftClass"]!.methods.first { + let funcDecl: ExtractedFunc = try #require( + st.extractedTypes["MySwiftClass"]!.methods.first { $0.name == "makeInt" } ) @@ -399,13 +400,13 @@ final class MethodImportTests { func class_constructor() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) - let initDecl: ImportedFunc = try #require( - st.importedTypes["MySwiftClass"]!.initializers.first { + let initDecl: ExtractedFunc = try #require( + st.extractedTypes["MySwiftClass"]!.initializers.first { $0.name == "init" } ) @@ -453,14 +454,14 @@ final class MethodImportTests { func struct_constructor() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) - let initDecl: ImportedFunc = try #require( - st.importedTypes["MySwiftStruct"]!.initializers.first { + let initDecl: ExtractedFunc = try #require( + st.extractedTypes["MySwiftStruct"]!.initializers.first { $0.name == "init" } ) @@ -508,13 +509,13 @@ final class MethodImportTests { func func_globalReturnAny() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) #expect( - !st.importedGlobalFuncs.contains { + !st.extractedGlobalFuncs.contains { $0.name == "globalReturnAny" }, "'Any' return type is not supported yet" diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index f2303d453..bfdff6d02 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -69,29 +70,29 @@ struct SpecializationTests { """# // ==== ----------------------------------------------------------------------- - // MARK: importedTypes structure + // MARK: extractedTypes structure - @Test("Multiple specializations of same base type produce distinct importedTypes") + @Test("Multiple specializations of same base type produce distinct extractedTypes") func multipleSpecializationsProduceDistinctTypes() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) // Both specialized types should be registered - #expect(translator.importedTypes["FishBox"] != nil, "FishBox should be in importedTypes") - #expect(translator.importedTypes["ToolBox"] != nil, "ToolBox should be in importedTypes") + #expect(translator.extractedTypes["FishBox"] != nil, "FishBox should be in extractedTypes") + #expect(translator.extractedTypes["ToolBox"] != nil, "ToolBox should be in extractedTypes") - // The base generic type remains in importedTypes (not removed) - let baseBox = try #require(translator.importedTypes["Box"]) + // The base generic type remains in extractedTypes (not removed) + let baseBox = try #require(translator.extractedTypes["Box"]) #expect(!baseBox.isSpecialization, "Base 'Box' should not be a specialization") #expect(baseBox.genericParameterNames == ["Element"]) #expect(baseBox.genericArguments.isEmpty) #expect(!baseBox.isFullySpecialized) // Specialized types link back to their base - let fishBox = try #require(translator.importedTypes["FishBox"]) - let toolBox = try #require(translator.importedTypes["ToolBox"]) + let fishBox = try #require(translator.extractedTypes["FishBox"]) + let toolBox = try #require(translator.extractedTypes["ToolBox"]) #expect(fishBox.isSpecialization) #expect(toolBox.isSpecialization) @@ -117,25 +118,38 @@ struct SpecializationTests { // Both wrappers delegate to the same base type #expect(fishBox.specializationBaseType === toolBox.specializationBaseType, "Both should wrap the same base Box type") - #expect(fishBox.specializationBaseType === translator.importedTypes["Box"], "Base should be the original Box") + #expect(fishBox.specializationBaseType === translator.extractedTypes["Box"], "Base should be the original Box") // Both wrappers have owned method models - let baseCountFunc: ImportedFunc = try #require(baseBox.methods.first(where: { $0.name == "count" })) - let fishCountFunc: ImportedFunc = try #require(fishBox.methods.first(where: { $0.name == "count" })) - let toolCountFunc: ImportedFunc = try #require(toolBox.methods.first(where: { $0.name == "count" })) + let baseCountFunc: ExtractedFunc = try #require(baseBox.methods.first(where: { $0.name == "count" })) + let fishCountFunc: ExtractedFunc = try #require(fishBox.methods.first(where: { $0.name == "count" })) + let toolCountFunc: ExtractedFunc = try #require(toolBox.methods.first(where: { $0.name == "count" })) #expect(baseCountFunc.parentType?.description == "Box") #expect(fishCountFunc.parentType?.description == "FishBox") #expect(toolCountFunc.parentType?.description == "ToolBox") } + @Test("JavaExtractDecider drops initializers on the unspecialized generic base") + func javaDeciderDropsBaseGenericInitializers() throws { + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = makeSwiftJavaAnalyzer(config: config) + try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) + + let baseBox = try #require(translator.extractedTypes["Box"]) + #expect(baseBox.swiftNominal.isGeneric) + #expect(!baseBox.isSpecialization) + #expect(baseBox.initializers.isEmpty, "Base 'Box' init should be dropped by JavaExtractDecider") + } + @Test("Specializations keyed by base type contain all entries") func specializationEntriesContainAll() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) - let baseBox = try #require(translator.importedTypes["Box"]) + let baseBox = try #require(translator.extractedTypes["Box"]) let specializations = try #require(translator.specializations[baseBox]) #expect(specializations.count == 2, "Should have exactly 2 specializations for Box") @@ -192,9 +206,9 @@ struct SpecializationTests { // Verify observeTheFish does NOT appear inside ToolBox's class body var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) - let toolBox = try #require(translator.importedTypes["ToolBox"]) + let toolBox = try #require(translator.extractedTypes["ToolBox"]) let methodNames = toolBox.methods.map(\.name) #expect(!methodNames.contains("observeTheFish"), "ToolBox should not have Fish-constrained method") } @@ -337,7 +351,7 @@ struct SpecializationTests { func specializeNonGenericTypeThrows() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze( path: "/fake/Fake.swiftinterface", text: """ @@ -347,7 +361,7 @@ struct SpecializationTests { """, ) - let fish = try #require(translator.importedTypes["Fish"]) + let fish = try #require(translator.extractedTypes["Fish"]) #expect(!fish.swiftNominal.isGeneric) #expect(throws: SpecializationError.self) { diff --git a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift index 864457aa2..2517b884f 100644 --- a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift +++ b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// @_spi(Testing) import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -21,38 +22,6 @@ import Testing @Suite("Swift symbol table") struct SwiftSymbolTableSuite { - @Test func lookupBindingTests() throws { - let sourceFile1: SourceFileSyntax = """ - extension X.Y { - struct Z { } - } - extension X { - struct Y {} - } - """ - let sourceFile2: SourceFileSyntax = """ - struct X {} - """ - let symbolTable = SwiftSymbolTable.setup( - moduleName: "MyModule", - [ - .init(syntax: sourceFile1, path: "Fake.swift"), - .init(syntax: sourceFile2, path: "Fake2.swift"), - ], - config: nil, - sourceDependencies: SourceDependencies(), - log: Logger(label: "swift-java", logLevel: .critical), - ) - - let x = try #require(symbolTable.lookupType("X", parent: nil)) - let xy = try #require(symbolTable.lookupType("Y", parent: x)) - let xyz = try #require(symbolTable.lookupType("Z", parent: xy)) - #expect(xyz.qualifiedName == "X.Y.Z") - - #expect(symbolTable.lookupType("Y", parent: nil) == nil) - #expect(symbolTable.lookupType("Z", parent: nil) == nil) - } - @Test(arguments: [JExtractGenerationMode.jni, .ffm]) func resolveSelfModuleName(mode: JExtractGenerationMode) throws { try assertOutput( @@ -94,35 +63,6 @@ struct SwiftSymbolTableSuite { ) } - @Test func moduleScopedLookup() throws { - let sourceFile: SourceFileSyntax = """ - public struct MyClass {} - """ - let symbolTable = SwiftSymbolTable.setup( - moduleName: "MyModule", - [ - .init(syntax: sourceFile, path: "Fake.swift") - ], - config: nil, - sourceDependencies: SourceDependencies(), - log: Logger(label: "swift-java", logLevel: .critical), - ) - - // Lookup in self-module by qualified name - let myClass = symbolTable.lookupTopLevelNominalType("MyClass", inModule: "MyModule") - #expect(myClass != nil) - #expect(myClass?.qualifiedName == "MyClass") - - // Lookup in imported module (Swift) - let swiftInt = symbolTable.lookupTopLevelNominalType("Int", inModule: "Swift") - #expect(swiftInt != nil) - #expect(swiftInt?.qualifiedName == "Int") - - // Lookup in unknown module returns nil - let unknown = symbolTable.lookupTopLevelNominalType("Foo", inModule: "NoSuchModule") - #expect(unknown == nil) - } - @Test(arguments: [JExtractGenerationMode.jni, .ffm]) func resolveQualifiedTypesInFunctionSignatures(mode: JExtractGenerationMode) throws { try assertOutput( diff --git a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift index 55cabb115..b0e9d5e56 100644 --- a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift +++ b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -48,10 +49,10 @@ struct TypealiasResolutionTests { func primitiveAliasResolvesStructMembers() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) - let user = try #require(translator.importedTypes["TypealiasUser"]) + let user = try #require(translator.extractedTypes["TypealiasUser"]) #expect(user.variables.contains { $0.name == "amount" }, "Property `amount: Amount` should be extracted") #expect(user.methods.contains { $0.name == "doubled" }, "Method `doubled() -> Amount` should be extracted") @@ -62,11 +63,11 @@ struct TypealiasResolutionTests { func primitiveAliasResolvesFreeFunc() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) #expect( - translator.importedGlobalFuncs.contains { $0.name == "makeAmount" }, + translator.extractedGlobalFuncs.contains { $0.name == "makeAmount" }, "Global func `makeAmount(_:)` should be extracted" ) } @@ -88,10 +89,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let holder = try #require(translator.importedTypes["Holder"]) + let holder = try #require(translator.extractedTypes["Holder"]) #expect(holder.variables.contains { $0.name == "value" }) #expect(!holder.initializers.isEmpty) } @@ -112,10 +113,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrapOrZero" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "unwrapOrZero" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) // The parameter type should be Optional (substituted), preserving @@ -143,10 +144,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "describe" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "describe" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { @@ -175,10 +176,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let holder = try #require(translator.importedTypes["Holder"]) + let holder = try #require(translator.extractedTypes["Holder"]) #expect(holder.variables.isEmpty, "Property `bad: OneArg` should be dropped (arity mismatch)") } @@ -200,12 +201,12 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) // The struct itself is still imported, but its members are dropped // because the alias never resolves. - let usesAlias = try #require(translator.importedTypes["UsesAlias"]) + let usesAlias = try #require(translator.extractedTypes["UsesAlias"]) #expect(usesAlias.variables.isEmpty, "Property `x: A` should be silently dropped (cycle)") } @@ -259,10 +260,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "passA" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "passA" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) #expect( paramType.description == "Int64", @@ -284,10 +285,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrap" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "unwrap" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { @@ -309,10 +310,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "first" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "first" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { @@ -343,10 +344,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "add" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "add" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) #expect( paramType.description == "Int64", @@ -375,10 +376,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let player = try #require(translator.importedTypes["Player"]) + let player = try #require(translator.extractedTypes["Player"]) let bump = try #require(player.methods.first { $0.name == "bump" }) let paramType = try #require(bump.functionSignature.parameters.first?.type) #expect(paramType.description == "Int64") @@ -400,10 +401,10 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "openIntBag" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "openIntBag" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { Issue.record("Expected Optional nominal, got \(paramType)") diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift new file mode 100644 index 000000000..703e27ef6 --- /dev/null +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -0,0 +1,404 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract +import Testing + +/// End-to-end tests that drive the analysis pipeline (Swift source → +/// `AnalysisResult`) without touching any code-generation layer. These verify +/// `SwiftExtract` produces a correct analysis snapshot independent of any +/// downstream language target. +@Suite("AnalysisResult") +struct AnalysisResultSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Top-level types + + @Test func topLevelTypesAreRecorded() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + } + public class FishTank { + public init() {} + } + public enum Status { + case open, closed + } + """ + ) + ], + moduleName: "Aquarium" + ) + + #expect(result.extractedTypes["Tank"] != nil) + #expect(result.extractedTypes["FishTank"] != nil) + #expect(result.extractedTypes["Status"] != nil) + + let tank = try #require(result.extractedTypes["Tank"]) + #expect(tank.swiftNominal.kind == .struct) + #expect(tank.swiftNominal.isGeneric) + + let fishTank = try #require(result.extractedTypes["FishTank"]) + #expect(fishTank.swiftNominal.kind == .class) + + let status = try #require(result.extractedTypes["Status"]) + #expect(status.swiftNominal.kind == .enum) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Methods on a type + + @Test func methodsAreRecordedOnEnclosingType() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class FishTank { + public init() {} + public func feed() {} + public func count() -> Int { 0 } + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let fishTank = try #require(result.extractedTypes["FishTank"]) + let methodNames = Set(fishTank.methods.map(\.name)) + #expect(methodNames == ["feed", "count"]) + #expect(fishTank.initializers.count == 1) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Properties (variables) — getter/setter pair + + @Test func storedPropertyProducesGetterAndSetter() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class FishTank { + public init() {} + public var capacity: Int = 0 + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let fishTank = try #require(result.extractedTypes["FishTank"]) + let capacityAccessors = fishTank.variables.filter { $0.name == "capacity" } + let kinds = Set(capacityAccessors.map(\.apiKind)) + #expect(kinds == [.getter, .setter]) + } + + @Test func readOnlyPropertyHasOnlyGetter() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class FishTank { + public init() {} + public var name: String { "Fish Tank" } + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let fishTank = try #require(result.extractedTypes["FishTank"]) + let nameAccessors = fishTank.variables.filter { $0.name == "name" } + let kinds = nameAccessors.map(\.apiKind) + #expect(kinds == [.getter]) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Global functions and variables + + @Test func globalFunctionLandsInImportedGlobalFuncs() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public func feedAll() {} + public func mood() -> String { "" } + """ + ) + ], + moduleName: "Aquarium" + ) + + let names = Set(result.extractedGlobalFuncs.map(\.name)) + #expect(names == ["feedAll", "mood"]) + #expect(result.extractedTypes.isEmpty) + } + + @Test func globalVariableProducesGetterSetterPair() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public var globalCounter: Int = 0 + """ + ) + ], + moduleName: "Aquarium" + ) + + let counterAccessors = result.extractedGlobalVariables.filter { $0.name == "globalCounter" } + let kinds = Set(counterAccessors.map(\.apiKind)) + #expect(kinds == [.getter, .setter]) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Effect specifiers (throws / async) + + @Test func effectSpecifiersAreCapturedOnFunctionSignatures() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public func plain() {} + public func throwing() throws {} + public func asynchronous() async {} + public func both() async throws {} + """ + ) + ], + moduleName: "Aquarium" + ) + + let byName = Dictionary(uniqueKeysWithValues: result.extractedGlobalFuncs.map { ($0.name, $0) }) + let plain = try #require(byName["plain"]) + #expect(plain.functionSignature.effectSpecifiers.isEmpty) + + let throwing = try #require(byName["throwing"]) + #expect(throwing.functionSignature.effectSpecifiers.contains(.throws)) + #expect(!throwing.functionSignature.effectSpecifiers.contains(.async)) + + let asynchronous = try #require(byName["asynchronous"]) + #expect(asynchronous.functionSignature.effectSpecifiers.contains(.async)) + #expect(!asynchronous.functionSignature.effectSpecifiers.contains(.throws)) + + let both = try #require(byName["both"]) + #expect(both.functionSignature.effectSpecifiers.contains(.async)) + #expect(both.functionSignature.effectSpecifiers.contains(.throws)) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Access-level filtering + + @Test func internalDeclarationsAreNotImportedByDefault() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class Public { + public init() {} + } + internal class Internal { + init() {} + } + private class Private { + init() {} + } + """ + ) + ], + moduleName: "Aquarium" + ) + + #expect(result.extractedTypes["Public"] != nil) + #expect(result.extractedTypes["Internal"] == nil) + #expect(result.extractedTypes["Private"] == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Filter include/exclude + + @Test func swiftFilterExcludeSkipsMatchingTypes() throws { + var config = DefaultSwiftExtractConfiguration() + config.swiftFilterExclude = ["Skip*"] + + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class Tank { + public init() {} + } + public class SkipMe { + public init() {} + } + public class SkipAlso { + public init() {} + } + """ + ) + ], + moduleName: "Aquarium", + config: config + ) + + #expect(result.extractedTypes["Tank"] != nil) + #expect(result.extractedTypes["SkipMe"] == nil) + #expect(result.extractedTypes["SkipAlso"] == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Generic typealias produces a specialization + + @Test func genericTypealiasProducesSpecializationEntry() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + } + public struct Fish {} + public typealias FishTank = Tank + """ + ) + ], + moduleName: "Aquarium" + ) + + // Both the generic base and its specialization land in extractedTypes. + #expect(result.extractedTypes["Tank"] != nil) + let fishTank = try #require(result.extractedTypes["FishTank"]) + #expect(fishTank.isSpecialization) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Empty input + + @Test func emptyModuleProducesEmptyResult() throws { + let result = try analyze( + sources: [ + ("/fake/Source.swift", "// nothing here") + ], + moduleName: "Empty" + ) + + #expect(result.extractedTypes.isEmpty) + #expect(result.extractedGlobalFuncs.isEmpty) + #expect(result.extractedGlobalVariables.isEmpty) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Configuration knobs + + /// `SwiftExtract` is language-neutral: every per-decl extraction decision + /// lives in the supplied `ExtractDecider`. The minimal + /// `DefaultAccessLevelExtractDecider` only enforces access level, so an + /// initializer of an unspecialized generic type passes through — language + /// targets that can't construct an open generic (e.g. swift-java's + /// `JavaExtractDecider`) are responsible for dropping it themselves. + @Test func unspecializedGenericInitializersFlowThroughByDefault() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + public init(capacity: Int) {} + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let tank = try #require(result.extractedTypes["Tank"]) + #expect(tank.swiftNominal.isGeneric) + #expect(!tank.isSpecialization) + #expect(tank.initializers.count == 2) + } + + /// `#if canImport()` blocks are inactive by default for modules the + /// build configuration doesn't know about — the type guarded behind them + /// must not appear in the analysis result. + @Test func canImportGuardedDeclsAreSkippedWhenModuleNotAvailable() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct AlwaysHere { + public init() {} + } + #if canImport(MadeUpModule) + public struct OnlyWhenImportable { + public init() {} + } + #endif + """ + ) + ], + moduleName: "Aquarium" + ) + + #expect(result.extractedTypes["AlwaysHere"] != nil) + #expect(result.extractedTypes["OnlyWhenImportable"] == nil) + } + + /// Adding the module to `availableImportModules` activates the + /// `#if canImport()` clause so its declarations are extracted. + @Test func availableImportModulesActivatesCanImportClause() throws { + var config = DefaultSwiftExtractConfiguration() + config.availableImportModules = ["MadeUpModule"] + + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct AlwaysHere { + public init() {} + } + #if canImport(MadeUpModule) + public struct OnlyWhenImportable { + public init() {} + } + #endif + """ + ) + ], + moduleName: "Aquarium", + config: config + ) + + #expect(result.extractedTypes["AlwaysHere"] != nil) + #expect(result.extractedTypes["OnlyWhenImportable"] != nil) + } +} diff --git a/Tests/SwiftExtractTests/InlineArrayTypeTests.swift b/Tests/SwiftExtractTests/InlineArrayTypeTests.swift new file mode 100644 index 000000000..f605c8cf9 --- /dev/null +++ b/Tests/SwiftExtractTests/InlineArrayTypeTests.swift @@ -0,0 +1,143 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Verifies that `SwiftType` recognises Swift's `InlineArray` (sugar +/// `[N of T]`) and surfaces the count + element separately so downstream +/// code generators can lower it to language-specific fixed-size shapes. +@Suite("InlineArray type parsing") +struct InlineArrayTypeSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Parsing the sugar form `[N of T]` + + @Test func sugarFormIsParsedAsInlineArray() throws { + let result = try analyze( + sources: [ + ("/fake/Source.swift", "public func take(_ a: [3 of Int]) {}") + ], + moduleName: "Test" + ) + + let fn = try #require(result.extractedGlobalFuncs.first { $0.name == "take" }) + let paramType = fn.functionSignature.parameters[0].type + + guard case .inlineArray(let count, let element) = paramType else { + Issue.record("expected .inlineArray, got \(paramType)") + return + } + #expect(count == 3) + let nominal = try #require(element.asNominalType) + #expect(nominal.nominalTypeDecl.knownTypeKind == .int) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Underscore digit separator and radix prefixes + + @Test func underscoreSeparatedCountIsParsed() throws { + let result = try analyze( + sources: [ + ("/fake/Source.swift", "public func take(_ a: [1_024 of Double]) {}") + ], + moduleName: "Test" + ) + + let fn = try #require(result.extractedGlobalFuncs.first { $0.name == "take" }) + guard case .inlineArray(let count, _) = fn.functionSignature.parameters[0].type else { + Issue.record("expected .inlineArray") + return + } + #expect(count == 1024) + } + + @Test func hexCountIsParsed() throws { + let result = try analyze( + sources: [ + ("/fake/Source.swift", "public func take(_ a: [0xA of UInt8]) {}") + ], + moduleName: "Test" + ) + + let fn = try #require(result.extractedGlobalFuncs.first { $0.name == "take" }) + guard case .inlineArray(let count, let element) = fn.functionSignature.parameters[0].type else { + Issue.record("expected .inlineArray") + return + } + #expect(count == 10) + #expect(element.asNominalType?.nominalTypeDecl.knownTypeKind == .uint8) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Returns and result types + + @Test func returnTypeIsParsedAsInlineArray() throws { + let result = try analyze( + sources: [ + ("/fake/Source.swift", "public func get() -> [4 of Float] { fatalError() }") + ], + moduleName: "Test" + ) + + let fn = try #require(result.extractedGlobalFuncs.first { $0.name == "get" }) + guard case .inlineArray(let count, let element) = fn.functionSignature.result.type else { + Issue.record("expected .inlineArray result") + return + } + #expect(count == 4) + #expect(element.asNominalType?.nominalTypeDecl.knownTypeKind == .float) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Nested inline arrays + + @Test func nestedInlineArrayIsParsed() throws { + let result = try analyze( + sources: [ + ("/fake/Source.swift", "public func take(_ a: [3 of [4 of Int]]) {}") + ], + moduleName: "Test" + ) + + let fn = try #require(result.extractedGlobalFuncs.first { $0.name == "take" }) + guard case .inlineArray(let outerCount, let outerElem) = fn.functionSignature.parameters[0].type else { + Issue.record("expected outer .inlineArray") + return + } + #expect(outerCount == 3) + guard case .inlineArray(let innerCount, let innerElem) = outerElem else { + Issue.record("expected inner .inlineArray") + return + } + #expect(innerCount == 4) + #expect(innerElem.asNominalType?.nominalTypeDecl.knownTypeKind == .int) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Description (printed form) + + @Test func descriptionUsesSugarForm() throws { + let result = try analyze( + sources: [ + ("/fake/Source.swift", "public func take(_ a: [3 of Int]) {}") + ], + moduleName: "Test" + ) + + let fn = try #require(result.extractedGlobalFuncs.first { $0.name == "take" }) + let paramType = fn.functionSignature.parameters[0].type + #expect(paramType.description == "[3 of Int]") + } +} 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/Support/TestAnalyze.swift b/Tests/SwiftExtractTests/Support/TestAnalyze.swift new file mode 100644 index 000000000..86dcb0949 --- /dev/null +++ b/Tests/SwiftExtractTests/Support/TestAnalyze.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +// ==== ---------------------------------------------------------------------- +// MARK: Test analysis helpers + +/// Drive `SwiftAnalyzer.analyze` from the test suite without spelling out an +/// `extractDecider:` argument every time. Builds a `DefaultAccessLevelExtractDecider` +/// from the supplied (or inferred) configuration's access-level setting, +/// matching the prior implicit-fallback behavior — these tests exercise the +/// language-neutral analysis layer, so the access-level-only baseline +/// decider is the right default. +func analyze( + sources: [(path: String, text: String)], + moduleName: String, + config: (any SwiftExtractConfiguration)? = nil, + sourceDependencies: SourceDependencies = SourceDependencies() +) throws -> AnalysisResult { + let effectiveConfig = config ?? DefaultSwiftExtractConfiguration(swiftModule: moduleName) + return try SwiftAnalyzer.analyze( + sources: sources, + moduleName: moduleName, + extractDecider: DefaultAccessLevelExtractDecider(accessLevel: effectiveConfig.effectiveMinimumInputAccessLevelMode), + config: effectiveConfig, + sourceDependencies: sourceDependencies + ) +} 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")) + } +} diff --git a/Tests/SwiftExtractTests/SwiftSyntheticTypesTests.swift b/Tests/SwiftExtractTests/SwiftSyntheticTypesTests.swift new file mode 100644 index 000000000..d472b701d --- /dev/null +++ b/Tests/SwiftExtractTests/SwiftSyntheticTypesTests.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// 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("SwiftSyntheticTypes") +struct SwiftSyntheticTypesSuite { + + // ==== ---------------------------------------------------------------------- + // MARK: unresolvedNominal stamps the placeholder flag + + @Test func unresolvedNominalIsMarkedAsPlaceholder() { + let type = SwiftSyntheticTypes.unresolvedNominal("Element") + + #expect(type.isUnresolvedTypePlaceholder) + #expect(type.asNominalTypeDeclaration?.isUnresolvedTypePlaceholder == true) + #expect("\(type)" == "Element") + + // Module name is empty — there's no real declaring module — and that's + // the honest answer; callers that want to recognize placeholders read + // `isUnresolvedTypePlaceholder`, not the moduleName string. + #expect(type.asNominalTypeDeclaration?.moduleName == "") + } + + // ==== ---------------------------------------------------------------------- + // MARK: Real source-derived nominals don't carry the flag + + @Test func realNominalIsNotMarkedAsPlaceholder() throws { + let result = try analyze( + sources: [ + ("/fake/Source.swift", "public struct Tank {}") + ], + moduleName: "Aquarium" + ) + + let tank = try #require(result.extractedTypes["Tank"]) + #expect(!tank.swiftType.isUnresolvedTypePlaceholder) + #expect(tank.swiftNominal.isUnresolvedTypePlaceholder == false) + } +}