From f868e0e702cfd82adf7af2c73b417fc7948602ab Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 27 May 2026 15:28:14 +0900 Subject: [PATCH 01/38] Separate out SwiftExtract, a reusable Swift analysis module This will take a while to finish up but this introduces a new core foundational module called `SwiftExtract` that allows for analysis of swift code into an analysis result that then source generators -- such as swift-java can use. This is to prepare reuse from other language generators, with a strong well maintianed analysis core that swift-java has spearheaded here. This is a massive "move stuff around" so reviewing everyting might be hard but the long term benefit will be massive. --- Package.swift | 49 ++- Sources/JExtractSwiftLib/AnalysisResult.swift | 19 -- .../Common/JavaTypeAnnotations.swift | 1 + .../Convenience/JavaType+Extensions.swift | 1 + ...sions.swift => String+JNIExtensions.swift} | 46 +-- .../FFM/CDeclLowering/CRepresentation.swift | 2 + ...Swift2JavaGenerator+FunctionLowering.swift | 1 + .../JExtractSwiftLib/FFM/ConversionStep.swift | 1 + ...FMSwift2JavaGenerator+FoundationData.swift | 1 + ...t2JavaGenerator+JavaBindingsPrinting.swift | 1 + ...MSwift2JavaGenerator+JavaTranslation.swift | 1 + ...ift2JavaGenerator+SwiftThunkPrinting.swift | 1 + .../FFM/FFMSwift2JavaGenerator.swift | 3 +- .../ImportedDecls+JavaNaming.swift | 79 +++++ Sources/JExtractSwiftLib/JNI/JNICaching.swift | 2 + .../JNI/JNIJavaTypeTranslator.swift | 1 + ...Generator+InterfaceWrapperGeneration.swift | 1 + ...t2JavaGenerator+JavaBindingsPrinting.swift | 1 + ...ISwift2JavaGenerator+JavaTranslation.swift | 1 + ...wift2JavaGenerator+NativeTranslation.swift | 1 + ...ift2JavaGenerator+SwiftThunkPrinting.swift | 1 + .../JNI/JNISwift2JavaGenerator.swift | 3 +- .../JExtractSwiftLib/JavaExtractDecider.swift | 38 +++ .../JavaIdentifierFactory.swift | 10 +- ...ies.swift => JavaSourceDependencies.swift} | 51 +-- .../Logger+ArgumentParser.swift | 27 ++ Sources/JExtractSwiftLib/Swift2Java.swift | 7 +- .../JExtractSwiftLib/SwiftKit+Printing.swift | 72 ++++ .../JExtractSwiftLib/SwiftSyntax+Java.swift | 53 +++ .../JExtractSwiftLib/ThunkNameRegistry.swift | 2 + .../TranslatedDocumentation.swift | 1 + Sources/SwiftExtract/AnalysisResult.swift | 29 ++ .../Convenience/Collection+Extensions.swift | 4 +- .../Convenience/String+Extensions.swift | 59 ++++ .../Convenience/SwiftSyntax+Extensions.swift | 42 +-- Sources/SwiftExtract/ExtractDecider.swift | 33 ++ .../ImportedDecls.swift | 141 +++----- .../Logger.swift | 13 +- .../Resources/dummy.json | 0 Sources/SwiftExtract/SourceDependencies.swift | 48 +++ .../SwiftAnalysisVisitor.swift} | 81 +++-- .../SwiftAnalyzer.swift} | 83 +++-- ...iftExtractDefaultBuildConfiguration.swift} | 40 +-- .../SwiftFileFilter.swift} | 24 +- .../SwiftTypes/ImportedSwiftModule.swift | 16 +- .../SwiftTypes/SwiftDependencyScanner.swift} | 2 +- .../SwiftTypes/SwiftEffectSpecifier.swift | 2 +- .../SwiftTypes/SwiftEnumCaseParameter.swift | 8 +- .../SwiftTypes/SwiftFunctionSignature.swift | 58 ++-- .../SwiftTypes/SwiftFunctionType.swift | 28 +- .../SwiftTypes/SwiftKnownModules.swift | 8 +- .../SwiftTypes/SwiftKnownTypeDecls.swift | 14 +- .../SwiftTypes/SwiftKnownTypes.swift | 72 ++-- .../SwiftTypes/SwiftModuleSymbolTable.swift | 32 +- .../SwiftNominalTypeDeclaration.swift | 76 ++--- .../SwiftTypes/SwiftParameter.swift | 32 +- .../SwiftParsedModuleSymbolTableBuilder.swift | 45 +-- .../SwiftTypes/SwiftQualifiedTypeName.swift | 20 +- .../SwiftTypes/SwiftResult.swift | 15 +- .../SwiftTypes/SwiftSymbolTable.swift | 135 +++----- .../SwiftTypes/SwiftType+GenericTypes.swift | 4 +- .../SwiftTypes/SwiftType.swift | 81 ++--- .../SwiftTypes/SwiftTypeLookupContext.swift | 16 +- Sources/SwiftJavaTool/CommonOptions.swift | 3 +- .../SwiftJavaBaseAsyncParsableCommand.swift | 5 +- .../Asserts/LoweringAssertions.swift | 5 +- .../Asserts/TextAssertions.swift | 7 +- .../FFMNestedTypesTests.swift | 3 +- .../FuncCallbackImportTests.swift | 7 +- .../FunctionDescriptorImportTests.swift | 5 +- .../JExtractFileFilterTests.swift | 65 ++-- .../JExtractSwiftTests/JNI/JNIEnumTests.swift | 3 +- .../JavaTypeAnnotationsTests.swift | 6 +- .../MethodImportTests.swift | 21 +- .../SpecializationTests.swift | 9 +- .../SwiftSymbolTableTests.swift | 64 +--- .../TypealiasResolutionTests.swift | 27 +- .../AnalysisResultTests.swift | 317 ++++++++++++++++++ .../SourceDependenciesTests.swift | 113 +++++++ .../Support/SymbolTableFixture.swift | 45 +++ .../SwiftKnownModuleTests.swift | 62 ++++ .../SwiftSymbolTableTests.swift | 192 +++++++++++ 82 files changed, 1879 insertions(+), 819 deletions(-) delete mode 100644 Sources/JExtractSwiftLib/AnalysisResult.swift rename Sources/JExtractSwiftLib/Convenience/{String+Extensions.swift => String+JNIExtensions.swift} (65%) create mode 100644 Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift create mode 100644 Sources/JExtractSwiftLib/JavaExtractDecider.swift rename Sources/JExtractSwiftLib/{SourceDependencies.swift => JavaSourceDependencies.swift} (58%) create mode 100644 Sources/JExtractSwiftLib/Logger+ArgumentParser.swift create mode 100644 Sources/JExtractSwiftLib/SwiftSyntax+Java.swift create mode 100644 Sources/SwiftExtract/AnalysisResult.swift rename Sources/{JExtractSwiftLib => SwiftExtract}/Convenience/Collection+Extensions.swift (91%) create mode 100644 Sources/SwiftExtract/Convenience/String+Extensions.swift rename Sources/{JExtractSwiftLib => SwiftExtract}/Convenience/SwiftSyntax+Extensions.swift (84%) create mode 100644 Sources/SwiftExtract/ExtractDecider.swift rename Sources/{JExtractSwiftLib => SwiftExtract}/ImportedDecls.swift (71%) rename Sources/{JExtractSwiftLib => SwiftExtract}/Logger.swift (89%) rename Sources/{JExtractSwiftLib => SwiftExtract}/Resources/dummy.json (100%) create mode 100644 Sources/SwiftExtract/SourceDependencies.swift rename Sources/{JExtractSwiftLib/Swift2JavaVisitor.swift => SwiftExtract/SwiftAnalysisVisitor.swift} (92%) rename Sources/{JExtractSwiftLib/Swift2JavaTranslator.swift => SwiftExtract/SwiftAnalyzer.swift} (79%) rename Sources/{JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift => SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift} (54%) rename Sources/{JExtractSwiftLib/JExtractFileFilter.swift => SwiftExtract/SwiftFileFilter.swift} (92%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/ImportedSwiftModule.swift (73%) rename Sources/{JExtractSwiftLib/SwiftTypes/DependencyScanner.swift => SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift} (97%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftEffectSpecifier.swift (92%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftEnumCaseParameter.swift (86%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftFunctionSignature.swift (92%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftFunctionType.swift (77%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftKnownModules.swift (96%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftKnownTypeDecls.swift (95%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftKnownTypes.swift (51%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftModuleSymbolTable.swift (63%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftNominalTypeDeclaration.swift (77%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftParameter.swift (79%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift (90%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftQualifiedTypeName.swift (71%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftResult.swift (64%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftSymbolTable.swift (66%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftType+GenericTypes.swift (97%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftType.swift (91%) rename Sources/{JExtractSwiftLib => SwiftExtract}/SwiftTypes/SwiftTypeLookupContext.swift (92%) create mode 100644 Tests/SwiftExtractTests/AnalysisResultTests.swift create mode 100644 Tests/SwiftExtractTests/SourceDependenciesTests.swift create mode 100644 Tests/SwiftExtractTests/Support/SymbolTableFixture.swift create mode 100644 Tests/SwiftExtractTests/SwiftKnownModuleTests.swift create mode 100644 Tests/SwiftExtractTests/SwiftSymbolTableTests.swift diff --git a/Package.swift b/Package.swift index 800a9a0ca..f8f4b455f 100644 --- a/Package.swift +++ b/Package.swift @@ -108,6 +108,11 @@ let package = Package( targets: ["SwiftRuntimeFunctions"] ), + .library( + name: "SwiftExtract", + targets: ["SwiftExtract"] + ), + .library( name: "JExtractSwiftLib", targets: ["JExtractSwiftLib"] @@ -336,6 +341,30 @@ let package = Package( ] ), + .target( + name: "SwiftExtract", + dependencies: [ + .product(name: "SwiftBasicFormat", package: "swift-syntax"), + .product(name: "SwiftIfConfig", package: "swift-syntax"), + .product(name: "SwiftLexicalLookup", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + .product(name: "Logging", package: "swift-log"), + "SwiftJavaConfigurationShared", + ], + path: "Sources/SwiftExtract", + resources: [ + .process("Resources") + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ], + plugins: [ + .plugin(name: "_StaticBuildConfigPlugin") + ] + ), + .target( name: "JExtractSwiftLib", dependencies: [ @@ -347,19 +376,14 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "SwiftJavaJNICore", package: "swift-java-jni-core"), + "SwiftExtract", "SwiftJavaShared", "SwiftJavaConfigurationShared", "CodePrinting", ], - resources: [ - .process("Resources") - ], swiftSettings: [ .swiftLanguageMode(.v5), .enableUpcomingFeature("BareSlashRegexLiterals"), - ], - plugins: [ - .plugin(name: "_StaticBuildConfigPlugin") ] ), @@ -435,6 +459,7 @@ let package = Package( name: "JExtractSwiftTests", dependencies: [ "JExtractSwiftLib", + "SwiftExtract", "CodePrinting", ], swiftSettings: [ @@ -442,6 +467,18 @@ let package = Package( ] ), + .testTarget( + name: "SwiftExtractTests", + dependencies: [ + "SwiftExtract", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ] + ), + .testTarget( name: "SwiftRuntimeFunctionsTests", dependencies: [ diff --git a/Sources/JExtractSwiftLib/AnalysisResult.swift b/Sources/JExtractSwiftLib/AnalysisResult.swift deleted file mode 100644 index 4d33bea19..000000000 --- a/Sources/JExtractSwiftLib/AnalysisResult.swift +++ /dev/null @@ -1,19 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -struct AnalysisResult { - let importedTypes: [String: ImportedNominalType] - let importedGlobalVariables: [ImportedFunc] - let importedGlobalFuncs: [ImportedFunc] -} diff --git a/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift b/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift index 7f540e527..2c733cb0e 100644 --- a/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift +++ b/Sources/JExtractSwiftLib/Common/JavaTypeAnnotations.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index b2d5394a5..023910767 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaJNICore extension JavaType { diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+JNIExtensions.swift similarity index 65% rename from Sources/JExtractSwiftLib/Convenience/String+Extensions.swift rename to Sources/JExtractSwiftLib/Convenience/String+JNIExtensions.swift index 9fdb6a64c..d317d1b06 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+JNIExtensions.swift @@ -15,34 +15,7 @@ import SwiftJavaJNICore extension String { - - var firstCharacterUppercased: String { - guard let f = first else { - return self - } - - return "\(f.uppercased())\(String(dropFirst()))" - } - - var firstCharacterLowercased: String { - guard let f = first else { - return self - } - - return "\(f.lowercased())\(String(dropFirst()))" - } - - /// Returns whether the string is of the format `isX` - var hasJavaBooleanNamingConvention: Bool { - guard self.hasPrefix("is"), self.count > 2 else { - return false - } - - let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2) - return self[thirdCharacterIndex].isUppercase - } - - /// Returns a version of the string correctly escaped for a JNI + /// Returns a version of the string correctly escaped for a JNI identifier var escapedJNIIdentifier: String { self.map { if $0 == "_" { @@ -66,15 +39,6 @@ extension String { .joined() } - /// If the string ends with `.swift`, return it without that suffix; - /// otherwise return self unchanged - func dropSwiftFileSuffix() -> String { - if hasSuffix(".swift") { - return String(dropLast(".swift".count)) - } - return self - } - /// Looks up self as a SwiftJava wrapped class name and converts it /// into a `JavaType.class` if it exists in `lookupTable`. func parseJavaClassFromSwiftJavaName(in lookupTable: [String: String]) -> JavaType? { @@ -87,14 +51,6 @@ extension String { return .class(package: javaPackageName, name: javaClassName) } - - /// Unescapes the name if it is surrounded by backticks. - var unescapedSwiftName: String { - if count >= 2 && hasPrefix("`") && hasSuffix("`") { - return String(dropFirst().dropLast()) - } - return self - } } extension Array where Element == String { diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index 58dc89263..d5956b424 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + extension CType { /// Lower the given Swift type down to a its corresponding C type. /// diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 565c5c9de..5a39f845b 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaJNICore import SwiftSyntax diff --git a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift index 03541539c..a086828f2 100644 --- a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift +++ b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftSyntax import SwiftSyntaxBuilder diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift index 7dd054f22..5d84b983f 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 1357a2492..3f84fca7c 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index bfcc02efb..2bd12a50c 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index ccecc17ee..49e71d9af 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftSyntax import SwiftSyntaxBuilder diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 118f4ad20..08890b3b9 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax @@ -69,7 +70,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { package init( config: Configuration, - translator: Swift2JavaTranslator, + translator: SwiftAnalyzer, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, diff --git a/Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift b/Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift new file mode 100644 index 000000000..6f71df330 --- /dev/null +++ b/Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract + +// ==== ----------------------------------------------------------------------- +// MARK: Java-facing name aliases for ImportedNominalType + +extension ImportedNominalType { + package var effectiveJavaTypeName: SwiftQualifiedTypeName { effectiveOutputTypeName } + package var effectiveJavaName: String { effectiveOutputName } + package var effectiveJavaSimpleName: String { effectiveOutputSimpleName } + package var javaGenericClause: String { outputGenericClause } +} + +// ==== ----------------------------------------------------------------------- +// MARK: Java-facing name aliases for ImportedFunc + +extension ImportedFunc { + /// The Java getter name for a Swift property/subscript getter, following + /// Java Beans conventions: `get` for non-boolean, `is` for + /// boolean (unless the property already starts with `is`, in which case + /// the original name is preserved). + /// + /// Returns `nil` when the underlying declaration is not a getter — i.e. a + /// regular function, initializer, enum case, or setter — since those don't + /// have a Java getter name. + package var javaGetterName: String? { + switch apiKind { + case .getter, .subscriptGetter: break + case .setter, .subscriptSetter, .function, .initializer, .enumCase: return nil + } + + let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool + + if !returnsBoolean { + return "get\(self.name.firstCharacterUppercased)" + } else if !self.name.hasJavaBooleanNamingConvention { + return "is\(self.name.firstCharacterUppercased)" + } else { + return self.name + } + } + + /// The Java setter name for a Swift property/subscript setter. If the + /// property already starts with `is` (boolean naming), the `is` prefix is + /// stripped so the setter becomes `set` per Java Beans spec. + /// + /// Returns `nil` when the underlying declaration is not a setter — i.e. a + /// regular function, initializer, enum case, or getter — since those don't + /// have a Java setter name. + package var javaSetterName: String? { + switch apiKind { + case .setter, .subscriptSetter: break + case .getter, .subscriptGetter, .function, .initializer, .enumCase: return nil + } + + let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool + + if isBooleanSetter && self.name.hasJavaBooleanNamingConvention { + // Safe to force unwrap due to `hasJavaBooleanNamingConvention` check. + let propertyName = self.name.split(separator: "is", maxSplits: 1).last! + return "set\(propertyName)" + } else { + return "set\(self.name.firstCharacterUppercased)" + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index 4ce16af9a..6cec38886 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + enum JNICaching { static func cacheName(for type: ImportedNominalType) -> String { cacheName(for: type.effectiveJavaTypeName) diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index 84fabeb77..b676a8385 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 3d36ee34f..abdc1db2a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index d6191a475..90aaafc38 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -15,6 +15,7 @@ import CodePrinting import Foundation import OrderedCollections +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 2d7762bef..f69ea1918 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index c0cdd7a06..66f89459f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 823fe7485..dac2f44aa 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaJNICore import SwiftSyntax diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 208a3228f..134c829ee 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaJNICore @@ -65,7 +66,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { package init( config: Configuration, - translator: Swift2JavaTranslator, + translator: SwiftAnalyzer, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift new file mode 100644 index 000000000..a7687a381 --- /dev/null +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract +import SwiftSyntax + +/// Java-specific extraction overrides applied on top of SwiftExtract's +/// built-in access-level filter: +/// +/// - `@JavaExport` forces extraction even of non-public decls +/// - `@JavaClass` / `@JavaInterface` / `@JavaField` / `@JavaStaticField` / +/// `@JavaMethod` / `@JavaStaticMethod` / `@JavaImplementation` are Swift +/// wrappers of Java types — skip them during extraction +public struct JavaExtractDecider: ExtractDecider { + public init() {} + + public func shouldExtract(decl: DeclSyntax, accessLevelPasses: Bool) -> Bool? { + let attrs = decl.asProtocol(WithAttributesSyntax.self)?.attributes + if attrs?.contains(where: { $0.isJavaExport }) == true { + return true + } + if attrs?.contains(where: { $0.isSwiftJavaMacro }) == true { + return false + } + return nil + } +} diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift index 2706d8ff9..dbaf85cdd 100644 --- a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + /// Detects Java method name conflicts caused by Swift overloads that differ /// only in parameter labels. When a conflict is detected, the affected methods /// get a camelCase suffix derived from their parameter labels (e.g. `takeValueA`, @@ -33,8 +35,8 @@ package struct JavaIdentifierFactory { for method in methods { let baseName: String = switch method.apiKind { - case .getter, .subscriptGetter: method.javaGetterName - case .setter, .subscriptSetter: method.javaSetterName + case .getter, .subscriptGetter: method.javaGetterName! + case .setter, .subscriptSetter: method.javaSetterName! case .function, .initializer, .enumCase: method.name } methodsByBaseName[baseName, default: []].append(method) @@ -64,8 +66,8 @@ package struct JavaIdentifierFactory { package func makeJavaMethodName(_ decl: ImportedFunc) -> String { let baseName: String = switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName + case .getter, .subscriptGetter: decl.javaGetterName! + case .setter, .subscriptSetter: decl.javaSetterName! case .function, .initializer, .enumCase: decl.name } var methodName = baseName + paramsSuffix(decl, baseName: baseName) diff --git a/Sources/JExtractSwiftLib/SourceDependencies.swift b/Sources/JExtractSwiftLib/JavaSourceDependencies.swift similarity index 58% rename from Sources/JExtractSwiftLib/SourceDependencies.swift rename to Sources/JExtractSwiftLib/JavaSourceDependencies.swift index a1128e51b..55fb17e6c 100644 --- a/Sources/JExtractSwiftLib/SourceDependencies.swift +++ b/Sources/JExtractSwiftLib/JavaSourceDependencies.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import OrderedCollections +import SwiftExtract import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -22,50 +24,25 @@ import FoundationEssentials import Foundation #endif - -package typealias SwiftModuleName = String -package typealias SwiftTypeName = String -package typealias SwiftSourceText = String package typealias JavaClassName = String package typealias JavaFullyQualifiedClassName = String package typealias JavaPackageName = String -/// Holds the inputs jextract needs for symbol resolution but does not generate -/// bindings for. -/// -/// Two flavours of "dependency" are tracked: -/// - Wrapped Java classes referenced from this module's API (e.g. `JavaInteger`). -/// - Real Swift sources from dependency Swift modules (passed via `--depends-on`), -/// parsed once and registered as imported `SwiftModuleSymbolTable`s so that -/// cross-module type references in this module's API can resolve them. -package struct SourceDependencies { - /// Swift wrapper type names for Java classes referenced from this module's - /// API (by convention `Java`, e.g. `JavaVector`). - package var javaClasses: [SwiftTypeName] = [] - - /// Parsed Swift inputs from dependency modules, keyed by Swift module name. - package var swiftModuleInputs: [SwiftModuleName: [SwiftJavaInputFile]] = [:] - - package init() {} - - /// Names of all dependency modules with associated Swift sources. - package var swiftModuleNames: Dictionary.Keys { - swiftModuleInputs.keys - } - - /// Synthetic Swift source registering `@JavaClass public class {}` stubs - package var syntheticJavaWrappersSwiftSource: SwiftJavaInputFile? { - guard !javaClasses.isEmpty else { return nil } +extension SourceDependencies { + /// Inject synthetic `@JavaClass public class {}` stubs so the symbol + /// table can resolve Java wrapper types referenced in the Swift API. + package mutating func addJavaWrapperStubs(_ javaClasses: [SwiftTypeName]) { + guard !javaClasses.isEmpty else { return } let text = javaClasses .map { "@JavaClass public class \($0) {}" } .joined(separator: "\n") - return SwiftJavaInputFile( - syntax: Parser.parse(source: text), - path: ".swift" - ) + let stub = SwiftInputFile(syntax: SwiftParser.Parser.parse(source: text), path: ".swift") + syntheticStubInputs[""] = [stub] } + /// Load Swift sources from a dependency module described by `dependency` and + /// register them in `swiftModuleInputs` for cross-module type resolution. package mutating func loadSwiftSources(from dependency: DependencyConfig, log: Logger) { guard let moduleName = dependency.swiftModuleName else { log.debug( @@ -85,15 +62,15 @@ package struct SourceDependencies { in: dependency.swiftSourcePaths, log: log, ) - var inputs: [SwiftJavaInputFile] = [] + var inputs: [SwiftInputFile] = [] let fm = FileManager.default for url in files where canExtract(from: url) { guard let data = fm.contents(atPath: url.path), let text = String(data: data, encoding: .utf8) else { continue } - let syntax = Parser.parse(source: text) - inputs.append(SwiftJavaInputFile(syntax: syntax, path: url.path)) + let syntax = SwiftParser.Parser.parse(source: text) + inputs.append(SwiftInputFile(syntax: syntax, path: url.path)) } if inputs.isEmpty { diff --git a/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift b/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift new file mode 100644 index 000000000..d6e6a828f --- /dev/null +++ b/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import SwiftExtract +import SwiftJavaConfigurationShared + +extension Logger.Level: ExpressibleByArgument { + public var defaultValueDescription: String { + "log level" + } + public private(set) static var allValueStrings: [String] = + ["trace", "debug", "info", "notice", "warning", "error", "critical"] + + public private(set) static var defaultCompletionKind: CompletionKind = .default +} diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 4350ffcde..2bf5ef037 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -14,6 +14,7 @@ import Foundation import OrderedCollections +import SwiftExtract import SwiftJavaConfigurationShared import SwiftJavaShared import SwiftParser @@ -34,7 +35,7 @@ public struct SwiftToJava { fatalError("Missing '--swift-module' name.") } - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) let log = translator.log if config.javaPackage == nil || config.javaPackage!.isEmpty { @@ -73,7 +74,7 @@ public struct SwiftToJava { // Apply jextract include/exclude filters if configured if hasFilters { let relativePath = computeRelativePath(file: file, inputPaths: inputPaths) - guard shouldJExtractFile(relativePath: relativePath, config: config) else { + guard shouldExtractSwiftFile(relativePath: relativePath, config: config) else { log.info("Skipping file (filtered out): \(file.path)") translator.filteredOutPaths.append(file.path) continue @@ -113,7 +114,7 @@ public struct SwiftToJava { partialResult[moduleName] = javaPackage } - translator.sourceDependencies.javaClasses = Array(wrappedJavaClassesLookupTable.keys) + translator.sourceDependencies.addJavaWrapperStubs(Array(wrappedJavaClassesLookupTable.keys)) for config in dependencyConfigs { translator.sourceDependencies.loadSwiftSources(from: config, log: translator.log) } diff --git a/Sources/JExtractSwiftLib/SwiftKit+Printing.swift b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift index f5ac6a4a4..4e0de0e68 100644 --- a/Sources/JExtractSwiftLib/SwiftKit+Printing.swift +++ b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift @@ -12,8 +12,10 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import Foundation import SwiftBasicFormat +import SwiftExtract import SwiftParser import SwiftSyntax @@ -56,3 +58,73 @@ extension SwiftKitPrinting.Names { } } + +// ==== ----------------------------------------------------------------------- +// MARK: SwiftSymbolTable printing helpers + +extension SwiftSymbolTable { + package func printImportedModules(_ printer: inout CodePrinter) { + let mainSymbolSourceModules = Set( + self.importedModules.values.filter { $0.alternativeModules?.isMainSourceOfSymbols ?? false }.map(\.moduleName) + ) + + for module in self.importedModules.keys.sorted() { + guard module != "Swift" else { + continue + } + + // Synthetic stub modules (e.g. ) exist purely for + // symbol-table resolution; they are not real Swift modules and must + // not be emitted as `import` statements. + guard !self.syntheticImportedModuleNames.contains(module) else { + continue + } + + guard let alternativeModules = self.importedModules[module]?.alternativeModules else { + printer.print("import \(module)") + continue + } + + // Only the main source of symbols emits the conditional import block. + // Secondary modules (e.g. FoundationEssentials when Foundation is the main source) + // are skipped when their main source is already present, because the main source's + // block already covers the import. If no main source is present, fall back to a + // plain import so the module is still imported. + guard alternativeModules.isMainSourceOfSymbols else { + if mainSymbolSourceModules.isDisjoint(with: alternativeModules.moduleNames) { + printer.print("import \(module)") + } + continue + } + + var importGroups: [String: [String]] = [:] + for name in alternativeModules.moduleNames { + guard let otherModule = self.importedModules[name] else { continue } + + let groupKey = otherModule.requiredAvailablityOfModuleWithName ?? otherModule.moduleName + importGroups[groupKey, default: []].append(otherModule.moduleName) + } + + for (index, group) in importGroups.keys.sorted().enumerated() { + if index > 0 && importGroups.keys.count > 1 { + printer.print("#elseif canImport(\(group))") + } else { + printer.print("#if canImport(\(group))") + } + + for groupModule in importGroups[group] ?? [] { + printer.print("import \(groupModule)") + } + } + + if importGroups.keys.isEmpty { + printer.print("import \(module)") + } else { + printer.print("#else") + printer.print("import \(module)") + printer.print("#endif") + } + } + printer.println() + } +} diff --git a/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift new file mode 100644 index 000000000..74acf3fff --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_spi(Testing) import SwiftExtract +import SwiftSyntax + +extension AttributeListSyntax.Element { + /// Whether this node has `SwiftJava` wrapping attributes (types that wrap Java classes). + /// These are skipped during jextract because they represent Java->Swift wrappers. + /// Note: `@JavaExport` is NOT included here — it forces export of Swift types to Java. + package var isSwiftJavaMacro: Bool { + guard case let .attribute(attr) = self else { + // FIXME: Handle #if. + return false + } + guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false } + switch attrName { + case "JavaClass", "JavaInterface", "JavaField", "JavaStaticField", "JavaMethod", "JavaStaticMethod", + "JavaImplementation": + return true + default: + return false + } + } + + /// Whether this is a `@JavaExport` attribute (used on typealiases for specialization, + /// or on struct/class/enum to force-include them even when excluded by filters) + package var isJavaExport: Bool { + guard case let .attribute(attr) = self else { return false } + guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false } + return attrName == "JavaExport" + } +} + +extension SwiftNominalType { + /// True iff the underlying Swift declaration uses one of the Java-wrapper + /// macros (`@JavaClass`, `@JavaInterface`, …) — meaning the type represents + /// a Java class wrapped for Swift, not a Swift type to be re-exported + public var isSwiftJavaWrapper: Bool { + nominalTypeDecl.syntax.attributes.contains(where: \.isSwiftJavaMacro) + } +} diff --git a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift index ac783f5c8..9b99aa5e7 100644 --- a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift +++ b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract + /// Registry of names we've already emitted as @_cdecl and must be kept unique. /// In order to avoid duplicate symbols, the registry can append some unique identifier to duplicated names package struct ThunkNameRegistry { diff --git a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift index bab21fdd9..664061973 100644 --- a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift +++ b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import CodePrinting +import SwiftExtract import SwiftJavaConfigurationShared import SwiftSyntax diff --git a/Sources/SwiftExtract/AnalysisResult.swift b/Sources/SwiftExtract/AnalysisResult.swift new file mode 100644 index 000000000..dedfbfa4a --- /dev/null +++ b/Sources/SwiftExtract/AnalysisResult.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public struct AnalysisResult { + public let importedTypes: [String: ImportedNominalType] + public let importedGlobalVariables: [ImportedFunc] + public let importedGlobalFuncs: [ImportedFunc] + + public init( + importedTypes: [String: ImportedNominalType], + importedGlobalVariables: [ImportedFunc], + importedGlobalFuncs: [ImportedFunc] + ) { + self.importedTypes = importedTypes + self.importedGlobalVariables = importedGlobalVariables + self.importedGlobalFuncs = importedGlobalFuncs + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/Collection+Extensions.swift b/Sources/SwiftExtract/Convenience/Collection+Extensions.swift similarity index 91% rename from Sources/JExtractSwiftLib/Convenience/Collection+Extensions.swift rename to Sources/SwiftExtract/Convenience/Collection+Extensions.swift index 4286d9e16..e04341368 100644 --- a/Sources/JExtractSwiftLib/Convenience/Collection+Extensions.swift +++ b/Sources/SwiftExtract/Convenience/Collection+Extensions.swift @@ -27,8 +27,8 @@ extension Dictionary { } extension Collection { - typealias IsLastElement = Bool - var withIsLast: any Collection<(Element, IsLastElement)> { + package typealias IsLastElement = Bool + package var withIsLast: any Collection<(Element, IsLastElement)> { var i = 1 let totalCount = self.count diff --git a/Sources/SwiftExtract/Convenience/String+Extensions.swift b/Sources/SwiftExtract/Convenience/String+Extensions.swift new file mode 100644 index 000000000..34fcafe0c --- /dev/null +++ b/Sources/SwiftExtract/Convenience/String+Extensions.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension String { + + package var firstCharacterUppercased: String { + guard let f = first else { + return self + } + + return "\(f.uppercased())\(String(dropFirst()))" + } + + package var firstCharacterLowercased: String { + guard let f = first else { + return self + } + + return "\(f.lowercased())\(String(dropFirst()))" + } + + /// Returns whether the string is of the format `isX` + package var hasJavaBooleanNamingConvention: Bool { + guard self.hasPrefix("is"), self.count > 2 else { + return false + } + + let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2) + return self[thirdCharacterIndex].isUppercase + } + + /// If the string ends with `.swift`, return it without that suffix; + /// otherwise return self unchanged + package func dropSwiftFileSuffix() -> String { + if hasSuffix(".swift") { + return String(dropLast(".swift".count)) + } + return self + } + + /// Unescapes the name if it is surrounded by backticks. + package var unescapedSwiftName: String { + if count >= 2 && hasPrefix("`") && hasSuffix("`") { + return String(dropFirst().dropLast()) + } + return self + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift similarity index 84% rename from Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift rename to Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift index 76e5ba688..3b0194db2 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift @@ -95,7 +95,7 @@ extension DeclModifierSyntax { } extension WithModifiersSyntax { - func isPublic(in type: NominalTypeDeclSyntaxNode?) -> Bool { + package func isPublic(in type: NominalTypeDeclSyntaxNode?) -> Bool { if let protocolDecl = type?.as(ProtocolDeclSyntax.self) { return protocolDecl.isPublic(in: nil) } @@ -105,13 +105,13 @@ extension WithModifiersSyntax { } } - var isAtLeastPackage: Bool { + package var isAtLeastPackage: Bool { self.modifiers.contains { modifier in modifier.isAtLeastPackage } } - var isAtLeastInternal: Bool { + package var isAtLeastInternal: Bool { if self.modifiers.isEmpty { // we assume that default access level is internal return true @@ -123,37 +123,9 @@ extension WithModifiersSyntax { } } -extension AttributeListSyntax.Element { - /// Whether this node has `SwiftJava` wrapping attributes (types that wrap Java classes). - /// These are skipped during jextract because they represent Java->Swift wrappers. - /// Note: `@JavaExport` is NOT included here — it forces export of Swift types to Java. - var isSwiftJavaMacro: Bool { - guard case let .attribute(attr) = self else { - // FIXME: Handle #if. - return false - } - guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false } - switch attrName { - case "JavaClass", "JavaInterface", "JavaField", "JavaStaticField", "JavaMethod", "JavaStaticMethod", - "JavaImplementation": - return true - default: - return false - } - } - - /// Whether this is a `@JavaExport` attribute (used on typealiases for specialization, - /// or on struct/class/enum to force-include them even when excluded by filters) - var isJavaExport: Bool { - guard case let .attribute(attr) = self else { return false } - guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false } - return attrName == "JavaExport" - } -} - extension DeclSyntaxProtocol { /// Find inner most "decl" node in ancestors. - var ancestorDecl: DeclSyntax? { + package var ancestorDecl: DeclSyntax? { var node: Syntax = Syntax(self) while let parent = node.parent { if let decl = parent.as(DeclSyntax.self) { @@ -165,7 +137,7 @@ extension DeclSyntaxProtocol { } /// Declaration name primarily for debugging. - var nameForDebug: String { + package var nameForDebug: String { switch DeclSyntax(self).as(DeclSyntaxEnum.self) { case .accessorDecl(let node): node.accessorSpecifier.text @@ -233,7 +205,7 @@ extension DeclSyntaxProtocol { } /// Qualified declaration name primarily for debugging. - var qualifiedNameForDebug: String { + package var qualifiedNameForDebug: String { if let parent = ancestorDecl { parent.qualifiedNameForDebug + "." + nameForDebug } else { @@ -242,7 +214,7 @@ extension DeclSyntaxProtocol { } /// Signature part of the declaration. I.e. without body or member block. - var signatureString: String { + package var signatureString: String { switch DeclSyntax(self.detached).as(DeclSyntaxEnum.self) { case .functionDecl(let node): node.with(\.body, nil).triviaSanitizedDescription diff --git a/Sources/SwiftExtract/ExtractDecider.swift b/Sources/SwiftExtract/ExtractDecider.swift new file mode 100644 index 000000000..9e576660c --- /dev/null +++ b/Sources/SwiftExtract/ExtractDecider.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// A pluggable extraction decision for downstream language generators +/// +/// The built-in analyzer always applies its access-level filter; a supplied +/// `ExtractDecider` can override that decision on a per-decl basis to encode +/// language-specific rules. For example, the Java target uses one to honor +/// `@JavaExport` (force-include even when access-level would skip) and to +/// skip Swift wrappers of Java types (`@JavaClass`, `@JavaInterface`, …) +public protocol ExtractDecider { + /// - Parameters: + /// - decl: the declaration being considered + /// - accessLevelPasses: whether the analyzer's built-in access-level + /// check admits the decl + /// - Returns: `true` to force-extract (even when `accessLevelPasses` + /// is `false`), `false` to skip (even when `accessLevelPasses` is + /// `true`), or `nil` to defer to the default behavior + func shouldExtract(decl: DeclSyntax, accessLevelPasses: Bool) -> Bool? +} diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/SwiftExtract/ImportedDecls.swift similarity index 71% rename from Sources/JExtractSwiftLib/ImportedDecls.swift rename to Sources/SwiftExtract/ImportedDecls.swift index 1f6b80d28..81f622cdb 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/SwiftExtract/ImportedDecls.swift @@ -14,10 +14,10 @@ import SwiftSyntax -/// Any imported (Swift) declaration -protocol ImportedDecl: AnyObject {} +/// Any extracted Swift declaration +public protocol ExtractedSwiftDecl: AnyObject {} -package enum SwiftAPIKind: Equatable { +public enum SwiftAPIKind: Equatable { case function case initializer case getter @@ -34,51 +34,50 @@ package enum SwiftAPIKind: Equatable { /// (e.g. `FishBox` specializing `Box` with `Element` = `Fish`). /// The specialization delegates its member collections to the base type /// so that extensions discovered later are visible through all specializations. -package final class ImportedNominalType: ImportedDecl { - let swiftNominal: SwiftNominalTypeDeclaration +public final class ImportedNominalType: ExtractedSwiftDecl { + public let swiftNominal: SwiftNominalTypeDeclaration - /// If this type is a specialization (FishTank), then this points at the Tank base type of the specialization. - /// His allows simplified - package let specializationBaseType: ImportedNominalType? + /// If this type is a specialization (FishTank), it points at the Tank base type of the specialization + public let specializationBaseType: ImportedNominalType? // The short path from module root to the file in which this nominal was originally declared. // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. - package var sourceFilePath: String { + public var sourceFilePath: String { self.swiftNominal.sourceFilePath } // Backing storage for member collections - package var initializers: [ImportedFunc] = [] - package var methods: [ImportedFunc] = [] - package var variables: [ImportedFunc] = [] - package var cases: [ImportedEnumCase] = [] - var inheritedTypes: [SwiftType] - package var parent: SwiftNominalTypeDeclaration? + public var initializers: [ImportedFunc] = [] + public var methods: [ImportedFunc] = [] + public var variables: [ImportedFunc] = [] + public var cases: [ImportedEnumCase] = [] + public var inheritedTypes: [SwiftType] + public var parent: SwiftNominalTypeDeclaration? /// The Swift base type name, e.g. "Box" — always the unparameterized name - package var baseTypeName: String { swiftNominal.qualifiedName } + public var baseTypeName: String { swiftNominal.qualifiedName } - /// The specialized/Java-facing name, e.g. "FishBox" — nil for base types - package private(set) var specializedTypeName: String? + /// The specialized output-facing name, e.g. "FishBox" — nil for base types + public private(set) var specializedTypeName: String? /// Whether this type is a specialization of a generic type - package var isSpecialization: Bool { specializationBaseType != nil } + public var isSpecialization: Bool { specializationBaseType != nil } /// Generic parameter names (e.g. ["Element"] for Box). Empty for non-generic types - package var genericParameterNames: [String] { + public var genericParameterNames: [String] { swiftNominal.genericParameters.map(\.name) } /// Maps generic parameter -> concrete type argument. Empty for unspecialized types /// e.g. {"Element": "Fish"} for FishBox - package var genericArguments: [String: String] = [:] + public var genericArguments: [String: String] = [:] /// True when all generic parameters have corresponding arguments - package var isFullySpecialized: Bool { + public var isFullySpecialized: Bool { !genericParameterNames.isEmpty && genericParameterNames.allSatisfy { genericArguments.keys.contains($0) } } - init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { + public init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { self.swiftNominal = swiftNominal self.specializationBaseType = nil self.inheritedTypes = @@ -119,41 +118,41 @@ package final class ImportedNominalType: ImportedDecl { self.swiftType = selfType } - let swiftType: SwiftType + public let swiftType: SwiftType - /// Structured Java-facing type name — "FishBox" for specialized, "Box" for base - package var effectiveJavaTypeName: SwiftQualifiedTypeName { + /// Structured output-facing type name — "FishBox" for specialized, "Box" for base + public var effectiveOutputTypeName: SwiftQualifiedTypeName { if let specializedTypeName { return SwiftQualifiedTypeName(specializedTypeName) } return swiftNominal.qualifiedTypeName } - /// The effective Java-facing name — "FishBox" for specialized, "Box" for base - var effectiveJavaName: String { - effectiveJavaTypeName.fullName + /// The effective output-facing name — "FishBox" for specialized, "Box" for base + public var effectiveOutputName: String { + effectiveOutputTypeName.fullName } - /// The simple Java class name (no qualification) for file naming purposes - var effectiveJavaSimpleName: String { + /// The simple output-facing class name (no qualification) for file naming purposes + public var effectiveOutputSimpleName: String { specializedTypeName ?? swiftNominal.name } /// The Swift type for thunk generation — "Box" for specialized, "Box" for base /// Computed from baseTypeName + genericArguments - var effectiveSwiftTypeName: String { + public var effectiveSwiftTypeName: String { guard !genericArguments.isEmpty else { return baseTypeName } let orderedArgs = genericParameterNames.compactMap { genericArguments[$0] } guard !orderedArgs.isEmpty else { return baseTypeName } return "\(baseTypeName)<\(orderedArgs.joined(separator: ", "))>" } - var qualifiedName: String { + public var qualifiedName: String { self.swiftNominal.qualifiedName } - /// The Java generic clause, e.g. "" for generic base types, "" for specialized or non-generic - var javaGenericClause: String { + /// The output generic clause, e.g. "" for generic base types, "" for specialized or non-generic + public var outputGenericClause: String { if isSpecialization { "" } else if genericParameterNames.isEmpty { @@ -164,7 +163,7 @@ package final class ImportedNominalType: ImportedDecl { } /// Create a specialized version of this generic type - package func specialize( + public func specialize( as specializedName: String, with substitutions: [String: String], ) throws -> ImportedNominalType { @@ -187,7 +186,7 @@ package final class ImportedNominalType: ImportedDecl { } /// Checks if this type, or any of types it inherits from, conforms to the passed in protocol. - package func conformsTo(_ protocolName: String, in importedTypes: [String: ImportedNominalType]) -> Bool { + public func conformsTo(_ protocolName: String, in importedTypes: [String: ImportedNominalType]) -> Bool { var visited: Set = [] var queue: [ImportedNominalType] = [self] while let current = queue.popLast() { @@ -203,25 +202,25 @@ package final class ImportedNominalType: ImportedDecl { } } -struct SpecializationError: Error { - let message: String +public struct SpecializationError: Error { + public let message: String } -public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { +public final class ImportedEnumCase: ExtractedSwiftDecl, CustomStringConvertible { /// The case name public let name: String /// The enum parameters - let parameters: [SwiftEnumCaseParameter] + public let parameters: [SwiftEnumCaseParameter] - let swiftDecl: any DeclSyntaxProtocol + public let swiftDecl: any DeclSyntaxProtocol - let enumType: SwiftNominalType + public let enumType: SwiftNominalType /// A function that represents the Swift static "initializer" for cases - let caseFunction: ImportedFunc + public let caseFunction: ImportedFunc - init( + public init( name: String, parameters: [SwiftEnumCaseParameter], swiftDecl: any DeclSyntaxProtocol, @@ -247,7 +246,7 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { """ } - func clone(for parent: SwiftType) -> ImportedEnumCase { + public func clone(for parent: SwiftType) -> ImportedEnumCase { ImportedEnumCase( name: name, parameters: parameters, @@ -267,7 +266,7 @@ extension ImportedEnumCase: Hashable { } } -public final class ImportedFunc: ImportedDecl, CustomStringConvertible { +public final class ImportedFunc: ExtractedSwiftDecl, CustomStringConvertible { /// Swift module name (e.g. the target name where a type or function was declared) public let module: String @@ -277,26 +276,26 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { public let swiftDecl: any DeclSyntaxProtocol - package let apiKind: SwiftAPIKind + public let apiKind: SwiftAPIKind - let functionSignature: SwiftFunctionSignature + public let functionSignature: SwiftFunctionSignature public var signatureString: String { self.swiftDecl.signatureString } - var parentType: SwiftType? { + public var parentType: SwiftType? { functionSignature.selfParameter?.selfType } - var isStatic: Bool { + public var isStatic: Bool { if case .staticMethod = functionSignature.selfParameter { return true } return false } - var isInitializer: Bool { + public var isInitializer: Bool { if case .initializer = functionSignature.selfParameter { return true } @@ -305,8 +304,6 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { /// If this function/method is member of a class/struct/protocol, /// this will contain that declaration's imported name. - /// - /// This is necessary when rendering accessor Java code we need the type that "self" is expecting to have. public var hasParent: Bool { functionSignature.selfParameter != nil } /// A display name to use to refer to the Swift declaration with its @@ -332,15 +329,15 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { return prefix + context + self.name } - var isThrowing: Bool { + public var isThrowing: Bool { self.functionSignature.effectSpecifiers.contains(.throws) } - var isAsync: Bool { + public var isAsync: Bool { self.functionSignature.isAsync } - init( + public init( module: String, swiftDecl: any DeclSyntaxProtocol, name: String, @@ -365,7 +362,7 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { """ } - func clone(for parent: SwiftType) -> ImportedFunc { + public func clone(for parent: SwiftType) -> ImportedFunc { var functionSignature = functionSignature assert(functionSignature.selfParameter?.selfType != nil) functionSignature.selfParameter?.selfType = parent @@ -388,34 +385,6 @@ extension ImportedFunc: Hashable { } } -extension ImportedFunc { - var javaGetterName: String { - let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool - - if !returnsBoolean { - return "get\(self.name.firstCharacterUppercased)" - } else if !self.name.hasJavaBooleanNamingConvention { - return "is\(self.name.firstCharacterUppercased)" - } else { - return self.name - } - } - - var javaSetterName: String { - let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool - - // If the variable is already named "isX", then we make - // the setter "setX" to match beans spec. - if isBooleanSetter && self.name.hasJavaBooleanNamingConvention { - // Safe to force unwrap due to `hasJavaBooleanNamingConvention` check. - let propertyName = self.name.split(separator: "is", maxSplits: 1).last! - return "set\(propertyName)" - } else { - return "set\(self.name.firstCharacterUppercased)" - } - } -} - extension ImportedNominalType: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) diff --git a/Sources/JExtractSwiftLib/Logger.swift b/Sources/SwiftExtract/Logger.swift similarity index 89% rename from Sources/JExtractSwiftLib/Logger.swift rename to Sources/SwiftExtract/Logger.swift index 5c4267830..aae0148af 100644 --- a/Sources/JExtractSwiftLib/Logger.swift +++ b/Sources/SwiftExtract/Logger.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -import ArgumentParser import Foundation import SwiftJavaConfigurationShared import SwiftSyntax @@ -117,16 +116,6 @@ extension Logger { public typealias Level = SwiftJavaConfigurationShared.LogLevel } -extension Logger.Level: ExpressibleByArgument { - public var defaultValueDescription: String { - "log level" - } - public private(set) static var allValueStrings: [String] = - ["trace", "debug", "info", "notice", "warning", "error", "critical"] - - public private(set) static var defaultCompletionKind: CompletionKind = .default -} - extension Logger.Level { var naturalIntegralValue: Int { switch self { diff --git a/Sources/JExtractSwiftLib/Resources/dummy.json b/Sources/SwiftExtract/Resources/dummy.json similarity index 100% rename from Sources/JExtractSwiftLib/Resources/dummy.json rename to Sources/SwiftExtract/Resources/dummy.json diff --git a/Sources/SwiftExtract/SourceDependencies.swift b/Sources/SwiftExtract/SourceDependencies.swift new file mode 100644 index 000000000..6d97a9637 --- /dev/null +++ b/Sources/SwiftExtract/SourceDependencies.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public typealias SwiftModuleName = String +public typealias SwiftTypeName = String +public typealias SwiftSourceText = String + +/// Holds inputs for symbol resolution that are not themselves the primary +/// extraction target: real Swift sources from dependency modules. +/// +/// Dependency inputs are parsed once and registered as imported +/// `SwiftModuleSymbolTable`s so that cross-module type references in the +/// analysed module's API can resolve them. +public struct SourceDependencies { + /// Parsed Swift inputs from dependency modules, keyed by Swift module name. + public var swiftModuleInputs: [SwiftModuleName: [SwiftInputFile]] = [:] + + /// Synthetic stub inputs keyed by a synthetic module name (e.g. for + /// generated `@JavaClass` placeholders). These are needed for symbol-table + /// resolution but must NOT be emitted as `import ` statements in + /// generated Swift code, because their names are not real Swift modules. + public var syntheticStubInputs: [SwiftModuleName: [SwiftInputFile]] = [:] + + public init() {} + + /// Names of all dependency modules (real + synthetic) with associated Swift + /// sources. Used by callers that need to resolve types belonging to either. + public var swiftModuleNames: Set { + Set(swiftModuleInputs.keys).union(syntheticStubInputs.keys) + } + + /// Names of synthetic stub modules. These should be skipped at Swift import + /// printing time. + public var syntheticModuleNames: Set { + Set(syntheticStubInputs.keys) + } +} diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift similarity index 92% rename from Sources/JExtractSwiftLib/Swift2JavaVisitor.swift rename to Sources/SwiftExtract/SwiftAnalysisVisitor.swift index ed6b4ed8b..84458800f 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,18 +13,19 @@ //===----------------------------------------------------------------------===// import Foundation +import Logging import SwiftIfConfig import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -final class Swift2JavaVisitor { - let translator: Swift2JavaTranslator +final class SwiftAnalysisVisitor { + let translator: SwiftAnalyzer var config: Configuration { self.translator.config } - init(translator: Swift2JavaTranslator) { + init(translator: SwiftAnalyzer) { self.translator = translator } @@ -38,7 +39,7 @@ final class Swift2JavaVisitor { } private var deferredConstrainedExtensions: [DeferredConstrainedExtension] = [] - func visit(inputFile: SwiftJavaInputFile) { + func visit(inputFile: SwiftInputFile) { let node = inputFile.syntax for codeItem in node.statements { if let declNode = codeItem.item.as(DeclSyntax.self) { @@ -179,7 +180,7 @@ final class Swift2JavaVisitor { in typeContext: ImportedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return } @@ -281,7 +282,7 @@ final class Swift2JavaVisitor { in typeContext: ImportedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return } @@ -328,7 +329,7 @@ final class Swift2JavaVisitor { self.log.info("Initializer must be within a current type; \(node)") return } - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return } @@ -369,7 +370,7 @@ final class Swift2JavaVisitor { subscriptDecl node: SubscriptDeclSyntax, in typeContext: ImportedNominalType?, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return } @@ -517,7 +518,7 @@ final class Swift2JavaVisitor { in typeContext: ImportedNominalType?, sourceFilePath: String, ) { - let javaName = node.name.text + let outputName = node.name.text let rhsType = node.initializer.value let genericArgs: [String] @@ -539,7 +540,7 @@ final class Swift2JavaVisitor { } registerSpecialization( - javaName: javaName, + outputName: outputName, baseType: baseType, genericArgs: genericArgs, rhsDescription: rhsType.trimmedDescription, @@ -548,7 +549,7 @@ final class Swift2JavaVisitor { /// Register a specialization from a typealias that specializes a generic type private func registerSpecialization( - javaName: String, + outputName: String, baseType: ImportedNominalType, genericArgs: [String], rhsDescription: String, @@ -566,13 +567,13 @@ final class Swift2JavaVisitor { let specialized: ImportedNominalType do { - specialized = try baseType.specialize(as: javaName, with: substitutions) + specialized = try baseType.specialize(as: outputName, with: substitutions) } catch { - log.warning("Failed to specialize \(baseType.baseTypeName) as \(javaName): \(error)") + log.warning("Failed to specialize \(baseType.baseTypeName) as \(outputName): \(error)") return } translator.specializations[baseType, default: []].insert(specialized) - log.info("Registered specialization: \(javaName) = \(rhsDescription)") + log.info("Registered specialization: \(outputName) = \(rhsDescription)") } // ==== ----------------------------------------------------------------------- @@ -585,8 +586,8 @@ final class Swift2JavaVisitor { } for specialized in specializations { - translator.importedTypes[specialized.effectiveJavaName] = specialized - log.info("Applied specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + translator.importedTypes[specialized.effectiveOutputName] = specialized + log.info("Applied specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } @@ -595,11 +596,11 @@ final class Swift2JavaVisitor { func applyPendingSpecializations() { for (_, specializations) in translator.specializations { for specialized in specializations { - if translator.importedTypes[specialized.effectiveJavaName] != nil { + if translator.importedTypes[specialized.effectiveOutputName] != nil { continue } - translator.importedTypes[specialized.effectiveJavaName] = specialized - log.info("Applied pending specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + translator.importedTypes[specialized.effectiveOutputName] = specialized + log.info("Applied pending specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } @@ -712,30 +713,40 @@ final class Swift2JavaVisitor { } extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { - func shouldExtract(config: Configuration, log: Logger, in parent: ImportedNominalType?) -> Bool { - // @JavaExport overrides all filters — always extract - if attributes.contains(where: { $0.isJavaExport }) { - return true - } - - let meetsRequiredAccessLevel: Bool = + /// Decide whether this declaration should be extracted + /// + /// Built-in logic checks only the access level required by `config`. An + /// optional `decider` supplied by a downstream language target can override + /// the result on a per-decl basis (e.g. Java honors `@JavaExport` / + /// `@JavaClass` here) + func shouldExtract( + config: Configuration, + log: Logger, + in parent: ImportedNominalType?, + decider: (any ExtractDecider)? + ) -> Bool { + let accessLevelPasses: Bool = switch config.effectiveMinimumInputAccessLevelMode { case .public: self.isPublic(in: parent?.swiftNominal.syntax) case .package: self.isAtLeastPackage case .internal: self.isAtLeastInternal } - guard meetsRequiredAccessLevel else { + if let override = decider?.shouldExtract( + decl: DeclSyntax(self), + accessLevelPasses: accessLevelPasses + ) { + if !override { + log.debug("Skip import '\(self.qualifiedNameForDebug)': decider rejected") + } + return override + } + + if !accessLevelPasses { log.debug( "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)" ) - return false - } - guard !attributes.contains(where: { $0.isSwiftJavaMacro }) else { - log.debug("Skip import '\(self.qualifiedNameForDebug)': is Java") - return false } - - return true + return accessLevelPasses } } diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift similarity index 79% rename from Sources/JExtractSwiftLib/Swift2JavaTranslator.swift rename to Sources/SwiftExtract/SwiftAnalyzer.swift index 6afa5b385..6a4818d34 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,35 +13,39 @@ //===----------------------------------------------------------------------===// import Foundation -import SwiftBasicFormat +import Logging import SwiftIfConfig import SwiftJavaConfigurationShared -import SwiftJavaJNICore import SwiftParser import SwiftSyntax -/// Takes swift interfaces and translates them into Java used to access those. -public final class Swift2JavaTranslator { +/// Drives the analysis of Swift source code into an `AnalysisResult` that +/// downstream language generators (e.g. Java/JNI/FFM, others) can consume +/// +/// The analysis is language-neutral; language-specific extraction rules +/// (such as honoring Java's `@JavaExport` or skipping `@JavaClass`-wrapped +/// types) are layered in via an optional `ExtractDecider` +public final class SwiftAnalyzer { static let SWIFT_INTERFACE_SUFFIX = ".swiftinterface" package var log: Logger - let config: Configuration + package let config: Configuration /// The build configuration used to resolve #if conditional compilation blocks. - let buildConfig: any BuildConfiguration + package let buildConfig: any BuildConfiguration /// The name of the Swift module being translated. - let swiftModuleName: String + package let swiftModuleName: String // ==== Input - var inputs: [SwiftJavaInputFile] = [] + package var inputs: [SwiftInputFile] = [] /// File paths that were skipped by swift filters but still need empty output /// files written (when --write-empty-files is set) so SwiftPM doesn't /// complain about missing declared outputs - var filteredOutPaths: [String] = [] + package var filteredOutPaths: [String] = [] /// Sources jextract needs for symbol resolution but does not generate bindings /// for: wrapped Java classes plus real Swift sources from dependency modules. @@ -61,14 +65,19 @@ public final class Swift2JavaTranslator { /// Specializations of generic types that will get their concrete Java declarations, "as if" they were independent types package var specializations: [ImportedNominalType: Set] = [:] - var lookupContext: SwiftTypeLookupContext! = nil + package var lookupContext: SwiftTypeLookupContext! = nil - var symbolTable: SwiftSymbolTable! { + package var symbolTable: SwiftSymbolTable! { lookupContext?.symbolTable } + /// Optional language-specific extraction decider that can override the + /// built-in access-level filter on a per-decl basis + package let extractDecider: (any ExtractDecider)? + public init( - config: Configuration + config: Configuration, + extractDecider: (any ExtractDecider)? = nil ) { guard let swiftModule = config.swiftModule else { fatalError("Missing 'swiftModule' name.") // FIXME: can we make it required in config? but we shared config for many cases @@ -76,6 +85,7 @@ public final class Swift2JavaTranslator { self.log = Logger(label: "translator", logLevel: config.logLevel ?? .info) self.config = config self.swiftModuleName = swiftModule + self.extractDecider = extractDecider if let staticBuildConfigPath = config.staticBuildConfigurationFile { do { @@ -87,7 +97,7 @@ public final class Swift2JavaTranslator { fatalError("Failed to load static build configuration from '\(staticBuildConfigPath)': \(error)") } } else { - self.buildConfig = .jextractDefault + self.buildConfig = .swiftExtractDefault } } } @@ -95,8 +105,9 @@ public final class Swift2JavaTranslator { // ===== -------------------------------------------------------------------------------------------------------------- // MARK: Analysis -extension Swift2JavaTranslator { - var result: AnalysisResult { +extension SwiftAnalyzer { + /// Snapshot of the analysis state as a value-typed `AnalysisResult`. + public var result: AnalysisResult { AnalysisResult( importedTypes: self.importedTypes, importedGlobalVariables: self.importedGlobalVariables, @@ -107,7 +118,7 @@ extension Swift2JavaTranslator { package func add(filePath: String, text: String) { log.debug("Adding: \(filePath)") let sourceFileSyntax = Parser.parse(source: text) - self.inputs.append(SwiftJavaInputFile(syntax: sourceFileSyntax, path: filePath)) + self.inputs.append(SwiftInputFile(syntax: sourceFileSyntax, path: filePath)) } /// Convenient method for analyzing single file. @@ -117,10 +128,10 @@ extension Swift2JavaTranslator { } /// Analyze registered inputs. - func analyze() throws { + package func analyze() throws { prepareForTranslation() - let visitor = Swift2JavaVisitor(translator: self) + let visitor = SwiftAnalysisVisitor(translator: self) for input in self.inputs { log.trace("Analyzing \(input.path)") @@ -133,7 +144,28 @@ extension Swift2JavaTranslator { self.visitFoundationDeclsIfNeeded(with: visitor) } - private func visitFoundationDeclsIfNeeded(with visitor: Swift2JavaVisitor) { + /// Top-level convenience: run analysis on the given Swift sources and return + /// the resulting `AnalysisResult`. Useful for tests and for callers that only + /// need analysis (no code generation). + public static func analyze( + sources: [(path: String, text: String)], + moduleName: String, + config: Configuration? = nil, + sourceDependencies: SourceDependencies = SourceDependencies(), + extractDecider: (any ExtractDecider)? = nil + ) throws -> AnalysisResult { + var effectiveConfig = config ?? Configuration() + effectiveConfig.swiftModule = moduleName + let translator = SwiftAnalyzer(config: effectiveConfig, extractDecider: extractDecider) + translator.sourceDependencies = sourceDependencies + for source in sources { + translator.add(filePath: source.path, text: source.text) + } + try translator.analyze() + return translator.result + } + + private func visitFoundationDeclsIfNeeded(with visitor: SwiftAnalysisVisitor) { // Each entry pairs a Foundation/FoundationEssentials counterpart so the // user-code reference can match either. Entries within the same group are // visited together when any one of the candidates is referenced — so using @@ -206,7 +238,6 @@ extension Swift2JavaTranslator { config: self.config, sourceDependencies: self.sourceDependencies, buildConfig: self.buildConfig, - log: self.log, ) self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) } @@ -269,13 +300,13 @@ extension Swift2JavaTranslator { // ==== ---------------------------------------------------------------------------------------------------------------- // MARK: Type translation -extension Swift2JavaTranslator { +extension SwiftAnalyzer { /// Try to resolve the given nominal declaration node into its imported representation. func importedNominalType( _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, parent: ImportedNominalType?, ) -> ImportedNominalType? { - if !nominalNode.shouldExtract(config: config, log: log, in: parent) { + if !nominalNode.shouldExtract(config: config, log: log, in: parent, decider: extractDecider) { return nil } @@ -303,7 +334,7 @@ extension Swift2JavaTranslator { return nil } - guard swiftNominalDecl.syntax.shouldExtract(config: config, log: log, in: nil) else { + guard swiftNominalDecl.syntax.shouldExtract(config: config, log: log, in: nil as ImportedNominalType?, decider: extractDecider) else { return nil } @@ -313,7 +344,7 @@ extension Swift2JavaTranslator { func importedNominalType(_ nominal: SwiftNominalTypeDeclaration) -> ImportedNominalType? { let fullName = nominal.qualifiedName - guard shouldJExtractType(qualifiedName: fullName, config: config) else { + guard shouldExtractSwiftType(qualifiedName: fullName, config: config) else { log.debug("Skip import '\(fullName)': filtered by swiftFilterInclude/swiftFilterExclude") return nil } @@ -332,7 +363,7 @@ extension Swift2JavaTranslator { // ==== ----------------------------------------------------------------------- // MARK: Errors -public struct Swift2JavaTranslatorError: Error { +public struct SwiftAnalyzerError: Error { let message: String public init(message: String) { diff --git a/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift similarity index 54% rename from Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift rename to Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift index 65adf8ff5..41f588974 100644 --- a/Sources/JExtractSwiftLib/JExtractDefaultBuildConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift @@ -17,12 +17,12 @@ import SwiftIfConfig import SwiftSyntax /// A default, fixed build configuration during static analysis for interface extraction. -struct JExtractDefaultBuildConfiguration: BuildConfiguration { - static let shared = JExtractDefaultBuildConfiguration() +package struct SwiftExtractDefaultBuildConfiguration: BuildConfiguration { + package static let shared = SwiftExtractDefaultBuildConfiguration() private var base: StaticBuildConfiguration - init() { + package init() { guard let url = Bundle.module.url(forResource: "static-build-config", withExtension: "json") else { fatalError("static-build-config.json is not found in module bundle") } @@ -35,69 +35,69 @@ struct JExtractDefaultBuildConfiguration: BuildConfiguration { } } - func isCustomConditionSet(name: String) throws -> Bool { + package func isCustomConditionSet(name: String) throws -> Bool { base.isCustomConditionSet(name: name) } - func hasFeature(name: String) throws -> Bool { + package func hasFeature(name: String) throws -> Bool { base.hasFeature(name: name) } - func hasAttribute(name: String) throws -> Bool { + package func hasAttribute(name: String) throws -> Bool { base.hasAttribute(name: name) } - func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { + package func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { try base.canImport(importPath: importPath, version: version) } - func isActiveTargetOS(name: String) throws -> Bool { + package func isActiveTargetOS(name: String) throws -> Bool { true } - func isActiveTargetArchitecture(name: String) throws -> Bool { + package func isActiveTargetArchitecture(name: String) throws -> Bool { true } - func isActiveTargetEnvironment(name: String) throws -> Bool { + package func isActiveTargetEnvironment(name: String) throws -> Bool { true } - func isActiveTargetRuntime(name: String) throws -> Bool { + package func isActiveTargetRuntime(name: String) throws -> Bool { true } - func isActiveTargetPointerAuthentication(name: String) throws -> Bool { + package func isActiveTargetPointerAuthentication(name: String) throws -> Bool { true } - func isActiveTargetObjectFormat(name: String) throws -> Bool { + package func isActiveTargetObjectFormat(name: String) throws -> Bool { true } - var targetPointerBitWidth: Int { + package var targetPointerBitWidth: Int { base.targetPointerBitWidth } - var targetAtomicBitWidths: [Int] { + package var targetAtomicBitWidths: [Int] { base.targetAtomicBitWidths } - var endianness: Endianness { + package var endianness: Endianness { base.endianness } - var languageVersion: VersionTuple { + package var languageVersion: VersionTuple { base.languageVersion } - var compilerVersion: VersionTuple { + package var compilerVersion: VersionTuple { base.compilerVersion } } -extension BuildConfiguration where Self == JExtractDefaultBuildConfiguration { - static var jextractDefault: JExtractDefaultBuildConfiguration { +extension BuildConfiguration where Self == SwiftExtractDefaultBuildConfiguration { + package static var swiftExtractDefault: SwiftExtractDefaultBuildConfiguration { .shared } } diff --git a/Sources/JExtractSwiftLib/JExtractFileFilter.swift b/Sources/SwiftExtract/SwiftFileFilter.swift similarity index 92% rename from Sources/JExtractSwiftLib/JExtractFileFilter.swift rename to Sources/SwiftExtract/SwiftFileFilter.swift index aef41240f..40c6ef6b3 100644 --- a/Sources/JExtractSwiftLib/JExtractFileFilter.swift +++ b/Sources/SwiftExtract/SwiftFileFilter.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -20,7 +20,7 @@ import SwiftJavaConfigurationShared /// A filter pattern is either a file-path pattern (uses `/` or a Swift file /// extension) or a type-name pattern (uses `.`). Plain names with neither /// match both -enum SwiftFilterPatternKind { +package enum SwiftFilterPatternKind { /// Pattern contains `/`, ends in a Swift source extension, or is a bare `**` /// glob — matches against relative file paths case filePath @@ -31,7 +31,7 @@ enum SwiftFilterPatternKind { case plain } -func classifyPattern(_ pattern: String) -> SwiftFilterPatternKind { +package func classifyPattern(_ pattern: String) -> SwiftFilterPatternKind { // Directory separator is the strongest signal for a file path if pattern.contains("/") { return .filePath @@ -167,7 +167,7 @@ private func matchSegment(_ segment: String, against pattern: String) -> Bool { /// - `**` matches zero or more path segments /// - `*` at the end of a segment matches any suffix (e.g. `Us*` matches `User.swift`) /// - exact segment match otherwise -func matchesFilePathFilter(relativePath: String, pattern: String) -> Bool { +package func matchesFilePathFilter(relativePath: String, pattern: String) -> Bool { matchesGlob(value: relativePath, pattern: pattern, separator: "/") } @@ -181,7 +181,7 @@ func matchesFilePathFilter(relativePath: String, pattern: String) -> Bool { /// - `**` matches zero or more name components /// - `*` at the end of a component matches any suffix /// - exact component match otherwise -func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bool { +package func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bool { matchesGlob(value: qualifiedName, pattern: pattern, separator: ".") } @@ -189,13 +189,13 @@ func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bool { // MARK: Combined filter application /// Determine whether a file at the given `relativePath` (including `.swift` -/// extension) should be included in jextract processing, based on the -/// include/exclude filters in `config`. +/// extension) should be included in extraction, based on the include/exclude +/// filters in `config`. /// /// Only file-path patterns (containing `/`) and plain patterns (no `/` or `.`) -/// are checked here. Type-name patterns are skipped — use `shouldJExtractType` -/// for those -func shouldJExtractFile(relativePath: String, config: Configuration) -> Bool { +/// are checked here. Type-name patterns are skipped — use +/// `shouldExtractSwiftType` for those +package func shouldExtractSwiftFile(relativePath: String, config: Configuration) -> Bool { if let includeFilters = config.swiftFilterInclude, !includeFilters.isEmpty { // Must match at least one file-level include pattern. // If all include patterns are type-name patterns, don't filter at file level @@ -244,9 +244,9 @@ private func matchesFilePattern(relativePath: String, pattern: String) -> Bool { /// `config`. /// /// Only type-name patterns (containing `.`) and plain patterns (no `/` or `.`) -/// are checked here. File-path patterns are skipped — use `shouldJExtractFile` +/// are checked here. File-path patterns are skipped — use `shouldExtractSwiftFile` /// for those -func shouldJExtractType(qualifiedName: String, config: Configuration) -> Bool { +package func shouldExtractSwiftType(qualifiedName: String, config: Configuration) -> Bool { if let includeFilters = config.swiftFilterInclude, !includeFilters.isEmpty { let typePatterns = includeFilters.filter { classifyPattern($0) != .filePath } if !typePatterns.isEmpty { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift b/Sources/SwiftExtract/SwiftTypes/ImportedSwiftModule.swift similarity index 73% rename from Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift rename to Sources/SwiftExtract/SwiftTypes/ImportedSwiftModule.swift index 8db800d63..5c22b2654 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/ImportedSwiftModule.swift +++ b/Sources/SwiftExtract/SwiftTypes/ImportedSwiftModule.swift @@ -12,13 +12,13 @@ // //===----------------------------------------------------------------------===// -struct ImportedSwiftModule: Hashable { - let name: String - let availableWithModuleName: String? - var alternativeModuleNames: Set - var isMainSourceOfSymbols: Bool +public struct ImportedSwiftModule: Hashable { + public let name: String + public let availableWithModuleName: String? + public var alternativeModuleNames: Set + public var isMainSourceOfSymbols: Bool - init( + public init( name: String, availableWithModuleName: String? = nil, alternativeModuleNames: Set = [], @@ -30,11 +30,11 @@ struct ImportedSwiftModule: Hashable { self.isMainSourceOfSymbols = isMainSourceOfSymbols } - static func == (lhs: Self, rhs: Self) -> Bool { + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.name == rhs.name } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(name) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift b/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift similarity index 97% rename from Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift index a36475c7b..e24c2eea7 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift @@ -15,7 +15,7 @@ import SwiftSyntax /// Scan importing modules. -func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftModule] { +package func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftModule] { var importingModuleNames: [ImportedSwiftModule] = [] for item in sourceFile.statements { if let importDecl = item.item.as(ImportDeclSyntax.self) { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift b/Sources/SwiftExtract/SwiftTypes/SwiftEffectSpecifier.swift similarity index 92% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftEffectSpecifier.swift index fb28586b1..c59d316d0 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftEffectSpecifier.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -enum SwiftEffectSpecifier: Equatable { +public enum SwiftEffectSpecifier: Equatable { case `throws` case `async` } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift b/Sources/SwiftExtract/SwiftTypes/SwiftEnumCaseParameter.swift similarity index 86% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftEnumCaseParameter.swift index 55682152d..335739397 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftEnumCaseParameter.swift @@ -14,13 +14,13 @@ import SwiftSyntax -struct SwiftEnumCaseParameter: Equatable { - var name: String? - var type: SwiftType +public struct SwiftEnumCaseParameter: Equatable { + public var name: String? + public var type: SwiftType } extension SwiftEnumCaseParameter { - init( + public init( _ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext ) throws { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift similarity index 92% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index a957c0ea1..9a0261c14 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -15,7 +15,7 @@ import SwiftSyntax import SwiftSyntaxBuilder -enum SwiftGenericRequirement: Equatable { +public enum SwiftGenericRequirement: Equatable { case inherits(SwiftType, SwiftType) case equals(SwiftType, SwiftType) } @@ -23,22 +23,22 @@ enum SwiftGenericRequirement: Equatable { /// Provides a complete signature for a Swift function, which includes its /// parameters and return type. public struct SwiftFunctionSignature: Equatable { - var selfParameter: SwiftSelfParameter? - var parameters: [SwiftParameter] - var result: SwiftResult - var effectSpecifiers: [SwiftEffectSpecifier] - var genericParameters: [SwiftGenericParameterDeclaration] - var genericRequirements: [SwiftGenericRequirement] - - var isAsync: Bool { + public var selfParameter: SwiftSelfParameter? + public var parameters: [SwiftParameter] + public var result: SwiftResult + public var effectSpecifiers: [SwiftEffectSpecifier] + public var genericParameters: [SwiftGenericParameterDeclaration] + public var genericRequirements: [SwiftGenericRequirement] + + public var isAsync: Bool { effectSpecifiers.contains(.async) } - var isThrowing: Bool { + public var isThrowing: Bool { effectSpecifiers.contains(.throws) } - init( + public init( selfParameter: SwiftSelfParameter? = nil, parameters: [SwiftParameter], result: SwiftResult, @@ -56,7 +56,7 @@ public struct SwiftFunctionSignature: Equatable { } /// Describes the "self" parameter of a Swift function signature. -enum SwiftSelfParameter: Equatable { +public enum SwiftSelfParameter: Equatable { /// 'self' is an instance parameter. case instance(convention: SwiftParameterConvention, swiftType: SwiftType) @@ -68,7 +68,7 @@ enum SwiftSelfParameter: Equatable { /// to form the call. case initializer(SwiftType) - var selfType: SwiftType { + public var selfType: SwiftType { get { switch self { case .instance(_, let swiftType), .staticMethod(let swiftType), .initializer(let swiftType): @@ -89,7 +89,7 @@ enum SwiftSelfParameter: Equatable { } extension SwiftFunctionSignature { - init( + public init( _ node: InitializerDeclSyntax, enclosingType: SwiftType?, lookupContext: SwiftTypeLookupContext @@ -126,7 +126,7 @@ extension SwiftFunctionSignature { ) } - init( + public init( _ node: EnumCaseElementSyntax, enclosingType: SwiftType, lookupContext: SwiftTypeLookupContext @@ -145,7 +145,7 @@ extension SwiftFunctionSignature { ) } - init( + public init( _ node: FunctionDeclSyntax, enclosingType: SwiftType?, lookupContext: SwiftTypeLookupContext @@ -212,7 +212,7 @@ extension SwiftFunctionSignature { ) } - static func translateGenericParameters( + public static func translateGenericParameters( parameterClause: GenericParameterClauseSyntax?, whereClause: GenericWhereClauseSyntax?, lookupContext: SwiftTypeLookupContext @@ -270,7 +270,7 @@ extension SwiftFunctionSignature { /// Translate the function signature, returning the list of translated /// parameters and effect specifiers. - static func translateFunctionSignature( + public static func translateFunctionSignature( _ signature: FunctionSignatureSyntax, lookupContext: SwiftTypeLookupContext ) throws -> ([SwiftParameter], [SwiftEffectSpecifier]) { @@ -289,7 +289,7 @@ extension SwiftFunctionSignature { return (parameters, effectSpecifiers) } - init( + public init( _ varNode: VariableDeclSyntax, isSet: Bool, enclosingType: SwiftType?, @@ -340,7 +340,7 @@ extension SwiftFunctionSignature { self.genericRequirements = [] } - init( + public init( _ subscriptNode: SubscriptDeclSyntax, isSet: Bool, enclosingType: SwiftType?, @@ -443,7 +443,7 @@ extension VariableDeclSyntax { /// /// - Parameters: /// - binding the pattern binding in this declaration. - func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { + public func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { if self.bindingSpecifier.tokenKind == .keyword(.let) { return [.get] } @@ -457,15 +457,19 @@ extension VariableDeclSyntax { } extension AccessorBlockSyntax { - struct SupportedAccessorKinds: OptionSet { - var rawValue: UInt8 + public struct SupportedAccessorKinds: OptionSet { + public var rawValue: UInt8 - static var get: Self = .init(rawValue: 1 << 0) - static var set: Self = .init(rawValue: 1 << 1) + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static var get: Self = .init(rawValue: 1 << 0) + public static var set: Self = .init(rawValue: 1 << 1) } /// Determine what operations (i.e. get and/or set) supported in this `AccessorBlockSyntax` - func supportedAccessorKinds() -> SupportedAccessorKinds { + public func supportedAccessorKinds() -> SupportedAccessorKinds { switch self.accessors { case .getter: return [.get] @@ -485,7 +489,7 @@ extension AccessorBlockSyntax { } } -enum SwiftFunctionTranslationError: Error { +public enum SwiftFunctionTranslationError: Error { case `throws`(ThrowsClauseSyntax) case async(TokenSyntax) case classMethod(TokenSyntax) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift similarity index 77% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift index 628cbc7a1..b127e6fb7 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift @@ -14,20 +14,32 @@ import SwiftSyntax -struct SwiftFunctionType: Equatable { - enum Convention: Equatable { +public struct SwiftFunctionType: Equatable { + public enum Convention: Equatable { case swift case c } - var convention: Convention - var parameters: [SwiftParameter] - var resultType: SwiftType - var isEscaping: Bool = false + public var convention: Convention + public var parameters: [SwiftParameter] + public var resultType: SwiftType + public var isEscaping: Bool = false + + public init( + convention: Convention, + parameters: [SwiftParameter], + resultType: SwiftType, + isEscaping: Bool = false + ) { + self.convention = convention + self.parameters = parameters + self.resultType = resultType + self.isEscaping = isEscaping + } } extension SwiftFunctionType: CustomStringConvertible { - var description: String { + public var description: String { let parameterString = parameters.map { $0.descriptionInType }.joined(separator: ", ") let conventionPrefix = switch convention { @@ -40,7 +52,7 @@ extension SwiftFunctionType: CustomStringConvertible { } extension SwiftFunctionType { - init( + public init( _ node: FunctionTypeSyntax, convention: Convention, isEscaping: Bool = false, diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift similarity index 96% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift index 696b1f1a5..47e360146 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift @@ -15,16 +15,16 @@ import SwiftSyntax import SwiftSyntaxBuilder -enum SwiftKnownModule: String { +public enum SwiftKnownModule: String { case swift = "Swift" case foundation = "Foundation" case foundationEssentials = "FoundationEssentials" - var name: String { + public var name: String { self.rawValue } - var symbolTable: SwiftModuleSymbolTable { + public var symbolTable: SwiftModuleSymbolTable { switch self { case .swift: swiftSymbolTable case .foundation: foundationSymbolTable @@ -32,7 +32,7 @@ enum SwiftKnownModule: String { } } - var sourceFile: SourceFileSyntax { + public var sourceFile: SourceFileSyntax { switch self { case .swift: swiftSourceFile case .foundation: foundationEssentialsSourceFile diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift similarity index 95% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift index 5b1e28933..57a851b1d 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift @@ -14,7 +14,7 @@ import SwiftSyntax -enum SwiftKnownType: Equatable { +public enum SwiftKnownType: Equatable { case bool case int case uint @@ -56,7 +56,7 @@ enum SwiftKnownType: Equatable { // SwiftRuntimeFunctions case swiftJavaError - init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { + public init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { switch kind { case .bool: self = .bool case .int: self = .int @@ -113,7 +113,7 @@ enum SwiftKnownType: Equatable { } } - var kind: SwiftKnownTypeDeclKind { + public var kind: SwiftKnownTypeDeclKind { switch self { case .bool: .bool case .int: .int @@ -155,7 +155,7 @@ enum SwiftKnownType: Equatable { } } -enum SwiftKnownTypeDeclKind: String, Hashable { +public enum SwiftKnownTypeDeclKind: String, Hashable { // Swift case bool = "Swift.Bool" case int = "Swift.Int" @@ -198,7 +198,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { // SwiftRuntimeFunctions case swiftJavaError = "SwiftRuntimeFunctions.SwiftJavaError" - var moduleAndName: (module: String, name: String) { + public var moduleAndName: (module: String, name: String) { let qualified = self.rawValue let period = qualified.firstIndex(of: ".")! return ( @@ -207,7 +207,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { ) } - var isPointer: Bool { + public var isPointer: Bool { switch self { case .unsafePointer, .unsafeMutablePointer, .unsafeRawPointer, .unsafeMutableRawPointer: return true @@ -221,7 +221,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { /// /// This means we do not have to perform any mapping when passing /// this type between jextract and wrap-java - var isDirectlyTranslatedToWrapJava: Bool { + public var isDirectlyTranslatedToWrapJava: Bool { switch self { case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string, .void: diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypes.swift similarity index 51% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftKnownTypes.swift index 198a1c14d..6c9c93a5d 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypes.swift @@ -12,50 +12,50 @@ // //===----------------------------------------------------------------------===// -struct SwiftKnownTypes { +public struct SwiftKnownTypes { private let symbolTable: SwiftSymbolTable - init(symbolTable: SwiftSymbolTable) { + public init(symbolTable: SwiftSymbolTable) { self.symbolTable = symbolTable } - var bool: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.bool])) } - var int: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int])) } - var uint: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint])) } - var int8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int8])) } - var uint8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint8])) } - var int16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int16])) } - var uint16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint16])) } - var int32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int32])) } - var uint32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint32])) } - var int64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int64])) } - var uint64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint64])) } - var float: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.float])) } - var double: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.double])) } - var unsafeRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawPointer])) } - var unsafeRawBufferPointer: SwiftType { + public var bool: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.bool])) } + public var int: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int])) } + public var uint: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint])) } + public var int8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int8])) } + public var uint8: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint8])) } + public var int16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int16])) } + public var uint16: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint16])) } + public var int32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int32])) } + public var uint32: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint32])) } + public var int64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.int64])) } + public var uint64: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.uint64])) } + public var float: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.float])) } + public var double: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.double])) } + public var unsafeRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawPointer])) } + public var unsafeRawBufferPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawBufferPointer])) } - var unsafeMutableRawPointer: SwiftType { + public var unsafeMutableRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeMutableRawPointer])) } - var string: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.string])) } + public var string: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.string])) } - var foundationDataProtocol: SwiftType { + public var foundationDataProtocol: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationDataProtocol])) } - var foundationData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationData])) } - var essentialsDataProtocol: SwiftType { + public var foundationData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationData])) } + public var essentialsDataProtocol: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsDataProtocol])) } - var essentialsData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsData])) } - var foundationUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationUUID])) } - var essentialsUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsUUID])) } + public var essentialsData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsData])) } + public var foundationUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationUUID])) } + public var essentialsUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsUUID])) } /// `(UnsafeRawPointer, Long) -> ()` function type. /// /// Commonly used to initialize a buffer using the passed bytes and length. - var functionInitializeByteBuffer: SwiftType { + public var functionInitializeByteBuffer: SwiftType { .function( SwiftFunctionType( convention: .c, @@ -68,7 +68,7 @@ struct SwiftKnownTypes { ) } - func unsafePointer(_ pointeeType: SwiftType) -> SwiftType { + public func unsafePointer(_ pointeeType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafePointer], @@ -77,7 +77,7 @@ struct SwiftKnownTypes { ) } - func unsafeMutablePointer(_ pointeeType: SwiftType) -> SwiftType { + public func unsafeMutablePointer(_ pointeeType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafeMutablePointer], @@ -86,7 +86,7 @@ struct SwiftKnownTypes { ) } - func unsafeBufferPointer(_ elementType: SwiftType) -> SwiftType { + public func unsafeBufferPointer(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafeBufferPointer], @@ -95,7 +95,7 @@ struct SwiftKnownTypes { ) } - func unsafeMutableBufferPointer(_ elementType: SwiftType) -> SwiftType { + public func unsafeMutableBufferPointer(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.unsafeMutableBufferPointer], @@ -104,7 +104,7 @@ struct SwiftKnownTypes { ) } - func optionalSugar(_ wrappedType: SwiftType) -> SwiftType { + public func optionalSugar(_ wrappedType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( sugarName: .optional, @@ -114,7 +114,7 @@ struct SwiftKnownTypes { ) } - func arraySugar(_ elementType: SwiftType) -> SwiftType { + public func arraySugar(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( sugarName: .array, @@ -124,7 +124,7 @@ struct SwiftKnownTypes { ) } - func dictionarySugar(_ keyType: SwiftType, _ valueType: SwiftType) -> SwiftType { + public func dictionarySugar(_ keyType: SwiftType, _ valueType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( sugarName: .dictionary, @@ -134,7 +134,7 @@ struct SwiftKnownTypes { ) } - func set(_ elementType: SwiftType) -> SwiftType { + public func set(_ elementType: SwiftType) -> SwiftType { .nominal( SwiftNominalType( nominalTypeDecl: symbolTable[.set], @@ -145,13 +145,13 @@ struct SwiftKnownTypes { /// Returns the known representative concrete type if there is one for the /// given protocol kind. E.g. `Data` for `DataProtocol` - func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftType? { + public func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftType? { guard let kind = Self.representativeType(of: knownProtocol) else { return nil } return .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[kind])) } /// Returns the representative concrete type kind for a protocol, if one exists - static func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftKnownTypeDeclKind? { + public static func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftKnownTypeDeclKind? { switch knownProtocol { case .foundationDataProtocol: return .foundationData case .essentialsDataProtocol: return .essentialsData diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift b/Sources/SwiftExtract/SwiftTypes/SwiftModuleSymbolTable.swift similarity index 63% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftModuleSymbolTable.swift index c77038596..5e4f5ba6a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftModuleSymbolTable.swift @@ -15,63 +15,63 @@ import SwiftSyntax import SwiftSyntaxBuilder -struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { +public struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { /// The name of this module. - let moduleName: String + public let moduleName: String /// The name of module required to be imported and checked via canImport statement. - let requiredAvailablityOfModuleWithName: String? + public let requiredAvailablityOfModuleWithName: String? /// Data about alternative modules which provides desired symbos e.g. FoundationEssentials is non-Darwin platform alternative for Foundation - let alternativeModules: AlternativeModuleNamesData? + public let alternativeModules: AlternativeModuleNamesData? /// The top-level nominal types, found by name. - var topLevelTypes: [String: SwiftNominalTypeDeclaration] = [:] + public var topLevelTypes: [String: SwiftNominalTypeDeclaration] = [:] /// The top-level typealias declarations, found by name. - var topLevelTypeAliases: [String: SwiftTypeAliasDeclaration] = [:] + public var topLevelTypeAliases: [String: SwiftTypeAliasDeclaration] = [:] /// The nested types defined within this module. The map itself is indexed by the /// identifier of the nominal type declaration, and each entry is a map from the nested /// type name to the nominal type declaration. - var nestedTypes: [SwiftNominalTypeDeclaration: [String: SwiftNominalTypeDeclaration]] = [:] + public var nestedTypes: [SwiftNominalTypeDeclaration: [String: SwiftNominalTypeDeclaration]] = [:] /// The nested typealias declarations defined within this module. The map itself is indexed /// by the nominal type declaration, and each entry is a map from the nested typealias /// name to the typealias declaration. - var nestedTypeAliases: [SwiftNominalTypeDeclaration: [String: SwiftTypeAliasDeclaration]] = [:] + public var nestedTypeAliases: [SwiftNominalTypeDeclaration: [String: SwiftTypeAliasDeclaration]] = [:] /// Look for a top-level type with the given name. - func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { + public func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { topLevelTypes[name] } /// Look for a top-level typealias with the given name. - func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { + public func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { topLevelTypeAliases[name] } // Look for a nested type with the given name. - func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { + public func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { nestedTypes[parent]?[name] } // Look for a nested typealias with the given name. - func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { + public func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { nestedTypeAliases[parent]?[name] } - func isAlternative(for moduleName: String) -> Bool { + public func isAlternative(for moduleName: String) -> Bool { alternativeModules.flatMap { $0.moduleNames.contains(moduleName) } ?? false } } extension SwiftModuleSymbolTable { - struct AlternativeModuleNamesData { + public struct AlternativeModuleNamesData { /// Flag indicating module should be used as source of symbols to avoid duplication of symbols. - let isMainSourceOfSymbols: Bool + public let isMainSourceOfSymbols: Bool /// Names of modules which are alternative for currently checked module. - let moduleNames: Set + public let moduleNames: Set } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift similarity index 77% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift index 07543bf23..b6f6399e8 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -19,21 +19,21 @@ import SwiftSyntax public typealias NominalTypeDeclSyntaxNode = any DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax -package class SwiftTypeDeclaration { +public class SwiftTypeDeclaration { // The short path from module root to the file in which this nominal was originally declared. // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. - let sourceFilePath: String + public let sourceFilePath: String /// The module in which this nominal type is defined. If this is a nested type, the /// module might be different from that of the parent type, if this nominal type /// is defined in an extension within another module. - let moduleName: String + public let moduleName: String /// The name of this nominal type, e.g. 'MyCollection'. - let name: String + public let name: String - init(sourceFilePath: String, moduleName: String, name: String) { + public init(sourceFilePath: String, moduleName: String, name: String) { self.sourceFilePath = sourceFilePath self.moduleName = moduleName self.name = name @@ -41,11 +41,11 @@ package class SwiftTypeDeclaration { } /// A syntax node paired with a simple file path -package struct SwiftJavaInputFile { - let syntax: SourceFileSyntax +public struct SwiftInputFile { + public let syntax: SourceFileSyntax /// Simple file path of the file from which the syntax node was parsed. - let path: String - package init(syntax: SourceFileSyntax, path: String) { + public let path: String + public init(syntax: SourceFileSyntax, path: String) { self.syntax = syntax self.path = path } @@ -53,8 +53,8 @@ package struct SwiftJavaInputFile { /// Describes a nominal type declaration, which can be of any kind (class, struct, etc.) /// and has a name, parent type (if nested), and owning module. -package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { - enum Kind { +public class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { + public enum Kind { case actor case `class` case `enum` @@ -63,27 +63,27 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } /// The syntax node this declaration is derived from. - let syntax: NominalTypeDeclSyntaxNode + @_spi(Testing) public let syntax: NominalTypeDeclSyntaxNode /// The kind of nominal type. - let kind: Kind + public let kind: Kind /// The parent nominal type when this nominal type is nested inside another type, e.g., /// MyCollection.Iterator. - let parent: SwiftNominalTypeDeclaration? + public let parent: SwiftNominalTypeDeclaration? /// The generic parameters of this nominal type. - let genericParameters: [SwiftGenericParameterDeclaration] + public let genericParameters: [SwiftGenericParameterDeclaration] /// Identify this nominal declaration as one of the known standard library /// types, like 'Swift.Int[. - private(set) lazy var knownTypeKind: SwiftKnownTypeDeclKind? = { + public private(set) lazy var knownTypeKind: SwiftKnownTypeDeclKind? = { self.computeKnownStandardLibraryType() }() /// Create a nominal type declaration from the syntax node for a nominal type /// declaration. - init( + @_spi(Testing) public init( name: String, sourceFilePath: String, moduleName: String, @@ -109,7 +109,7 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: name) } - private(set) lazy var firstInheritanceType: TypeSyntax? = { + public private(set) lazy var firstInheritanceType: TypeSyntax? = { guard let firstInheritanceType = self.syntax.inheritanceClause?.inheritedTypes.first else { return nil } @@ -117,16 +117,16 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { return firstInheritanceType.type }() - var inheritanceTypes: InheritedTypeListSyntax? { + public var inheritanceTypes: InheritedTypeListSyntax? { self.syntax.inheritanceClause?.inheritedTypes } - var genericWhereClause: GenericWhereClauseSyntax? { + public var genericWhereClause: GenericWhereClauseSyntax? { self.syntax.asProtocol(WithGenericParametersSyntax.self)?.genericWhereClause } /// Returns true if this type conforms to `Sendable` and therefore is "threadsafe". - private(set) lazy var isSendable: Bool = { + public private(set) lazy var isSendable: Bool = { // Check if Sendable is in the inheritance list guard let inheritanceClause = self.syntax.inheritanceClause else { return false @@ -152,7 +152,7 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } /// Structured qualified type name built from the parent chain - package var qualifiedTypeName: SwiftQualifiedTypeName { + public var qualifiedTypeName: SwiftQualifiedTypeName { if let parent = self.parent { return SwiftQualifiedTypeName(parent.qualifiedTypeName.components + [name]) } else { @@ -160,17 +160,17 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } } - package var qualifiedName: String { + public var qualifiedName: String { qualifiedTypeName.fullName } /// Like `qualifiedName` but with dots replaced by underscores, suitable for /// use in C symbol names and Java identifiers - package var flatName: String { + public var flatName: String { qualifiedTypeName.fullFlatName } - var isReferenceType: Bool { + public var isReferenceType: Bool { switch kind { case .actor, .class: return true @@ -179,13 +179,13 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } } - var isGeneric: Bool { + public var isGeneric: Bool { !genericParameters.isEmpty } } extension SwiftNominalTypeDeclaration: CustomStringConvertible { - package var description: String { + public var description: String { if isGeneric { "\(qualifiedName)<\(genericParameters.map(\.name).joined(separator: ", "))>" } else { @@ -194,10 +194,10 @@ extension SwiftNominalTypeDeclaration: CustomStringConvertible { } } -package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { - let syntax: GenericParameterSyntax +public class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { + public let syntax: GenericParameterSyntax - init( + public init( sourceFilePath: String, moduleName: String, node: GenericParameterSyntax @@ -206,11 +206,11 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: node.name.text) } - var hasEach: Bool { + public var hasEach: Bool { syntax.specifier?.tokenKind == .keyword(.each) } - var packReferenceName: String { + public var packReferenceName: String { if hasEach { "each \(name)" } else { @@ -218,7 +218,7 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { } } - var packExpansionName: String { + public var packExpansionName: String { if hasEach { "repeat each \(name)" } else { @@ -232,10 +232,10 @@ package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { /// A typealias used as a specialization of a generic type will be emitted as /// a new concrete type in the Java. This way we can specialize `FishBox` from /// `Box` by doing `typealias FishBox = Box`. -package final class SwiftTypeAliasDeclaration: SwiftTypeDeclaration { - let syntax: TypeAliasDeclSyntax +public final class SwiftTypeAliasDeclaration: SwiftTypeDeclaration { + public let syntax: TypeAliasDeclSyntax - init( + public init( sourceFilePath: String, moduleName: String, node: TypeAliasDeclSyntax @@ -246,13 +246,13 @@ package final class SwiftTypeAliasDeclaration: SwiftTypeDeclaration { } extension SwiftTypeDeclaration: Equatable { - package static func == (lhs: SwiftTypeDeclaration, rhs: SwiftTypeDeclaration) -> Bool { + public static func == (lhs: SwiftTypeDeclaration, rhs: SwiftTypeDeclaration) -> Bool { lhs === rhs } } extension SwiftTypeDeclaration: Hashable { - package func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift similarity index 79% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift index 56fe3cb9c..90b694a42 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift @@ -14,22 +14,34 @@ import SwiftSyntax -struct SwiftParameter: Equatable { - var convention: SwiftParameterConvention - var argumentLabel: String? - var parameterName: String? - var type: SwiftType +public struct SwiftParameter: Equatable { + public var convention: SwiftParameterConvention + public var argumentLabel: String? + public var parameterName: String? + public var type: SwiftType + + public init( + convention: SwiftParameterConvention, + argumentLabel: String? = nil, + parameterName: String? = nil, + type: SwiftType + ) { + self.convention = convention + self.argumentLabel = argumentLabel + self.parameterName = parameterName + self.type = type + } } extension SwiftParameter: CustomStringConvertible { - var description: String { + public var description: String { let argumentLabel = self.argumentLabel ?? "_" let parameterName = self.parameterName ?? "_" return "\(argumentLabel) \(parameterName): \(descriptionInType)" } - var descriptionInType: String { + public var descriptionInType: String { let conventionString: String switch convention { case .byValue: @@ -47,7 +59,7 @@ extension SwiftParameter: CustomStringConvertible { } /// Describes how a parameter is passed. -enum SwiftParameterConvention: Equatable { +public enum SwiftParameterConvention: Equatable { /// The parameter is passed by-value or borrowed. case byValue /// The parameter is passed by-value but consumed. @@ -57,7 +69,7 @@ enum SwiftParameterConvention: Equatable { } extension SwiftParameter { - init(_ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { + public init(_ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { self.convention = .byValue self.type = try SwiftType(node.type, lookupContext: lookupContext) self.argumentLabel = nil @@ -67,7 +79,7 @@ extension SwiftParameter { } extension SwiftParameter { - init(_ node: FunctionParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { + public init(_ node: FunctionParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { // Determine the convention. The default is by-value, but there are // specifiers on the type for other conventions (like `inout`). var type = node.type diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift b/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift similarity index 90% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift index 8691cdfa1..560ed9c0c 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift @@ -12,30 +12,31 @@ // //===----------------------------------------------------------------------===// +import Logging import SwiftIfConfig import SwiftSyntax -struct SwiftParsedModuleSymbolTableBuilder { - let log: Logger? +package struct SwiftParsedModuleSymbolTableBuilder { + package let log: Logger? /// The symbol table being built. - var symbolTable: SwiftModuleSymbolTable + package var symbolTable: SwiftModuleSymbolTable /// Imported modules to resolve type syntax. - let importedModules: [String: SwiftModuleSymbolTable] + package let importedModules: [String: SwiftModuleSymbolTable] /// The build configuration used to resolve #if conditional compilation blocks. - let buildConfig: any BuildConfiguration + package let buildConfig: any BuildConfiguration /// Extension decls their extended type hasn't been resolved. - var unresolvedExtensions: [ExtensionDeclSyntax] + package var unresolvedExtensions: [ExtensionDeclSyntax] - init( + package init( moduleName: String, requiredAvailablityOfModuleWithName: String? = nil, alternativeModules: SwiftModuleSymbolTable.AlternativeModuleNamesData? = nil, importedModules: [String: SwiftModuleSymbolTable], - buildConfig: any BuildConfiguration = .jextractDefault, + buildConfig: any BuildConfiguration = .swiftExtractDefault, log: Logger? = nil ) { self.log = log @@ -49,14 +50,14 @@ struct SwiftParsedModuleSymbolTableBuilder { self.unresolvedExtensions = [] } - var moduleName: String { + package var moduleName: String { symbolTable.moduleName } } extension SwiftParsedModuleSymbolTableBuilder { - mutating func handle( + package mutating func handle( sourceFile: SourceFileSyntax, sourceFilePath: String ) { @@ -66,7 +67,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( codeBlockItem node: CodeBlockItemSyntax.Item, sourceFilePath: String ) { @@ -88,7 +89,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( typeAliasDecl node: TypeAliasDeclSyntax, sourceFilePath: String ) { @@ -108,7 +109,7 @@ extension SwiftParsedModuleSymbolTableBuilder { /// Add a nominal type declaration and all of the nested types within it to the symbol /// table. - mutating func handle( + package mutating func handle( sourceFilePath: String, nominalTypeDecl node: NominalTypeDeclSyntaxNode, parent: SwiftNominalTypeDeclaration? @@ -118,7 +119,7 @@ extension SwiftParsedModuleSymbolTableBuilder { if symbolTable.lookupType(node.name.text, parent: parent) != nil || symbolTable.lookupTypealias(node.name.text, parent: parent) != nil { - log?.debug("Failed to add a decl into symbol table: redeclaration; " + node.nameForDebug) + log?.debug("Failed to add a decl into symbol table: redeclaration; \(node.nameForDebug)") return } @@ -142,7 +143,7 @@ extension SwiftParsedModuleSymbolTableBuilder { self.handle(sourceFilePath: sourceFilePath, memberBlock: node.memberBlock, parent: nominalTypeDecl) } - mutating func handle( + package mutating func handle( sourceFilePath: String, typeAliasDecl node: TypeAliasDeclSyntax, parent: SwiftNominalTypeDeclaration? @@ -150,7 +151,7 @@ extension SwiftParsedModuleSymbolTableBuilder { if symbolTable.lookupType(node.name.text, parent: parent) != nil || symbolTable.lookupTypealias(node.name.text, parent: parent) != nil { - log?.debug("Failed to add a decl into symbol table: redeclaration; " + node.nameForDebug) + log?.debug("Failed to add a decl into symbol table: redeclaration; \(node.nameForDebug)") return } @@ -167,7 +168,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( sourceFilePath: String, memberBlock node: MemberBlockSyntax, parent: SwiftNominalTypeDeclaration @@ -182,7 +183,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } } - mutating func handle( + package mutating func handle( extensionDecl node: ExtensionDeclSyntax, sourceFilePath: String ) { @@ -193,7 +194,7 @@ extension SwiftParsedModuleSymbolTableBuilder { /// Add any nested types within the given extension to the symbol table. /// If the extended nominal type can't be resolved, returns false. - mutating func tryHandle( + package mutating func tryHandle( extension node: ExtensionDeclSyntax, sourceFilePath: String ) -> Bool { @@ -217,7 +218,7 @@ extension SwiftParsedModuleSymbolTableBuilder { return true } - mutating func handle( + package mutating func handle( ifConfig node: IfConfigDeclSyntax, sourceFilePath: String ) { @@ -235,7 +236,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } /// Finalize the symbol table and return it. - mutating func finalize() -> SwiftModuleSymbolTable { + package mutating func finalize() -> SwiftModuleSymbolTable { // Handle the unresolved extensions. // The work queue is required because, the extending type might be declared // in another extension that hasn't been processed. E.g.: @@ -264,7 +265,7 @@ extension SwiftParsedModuleSymbolTableBuilder { } extension DeclSyntaxProtocol { - var asNominal: NominalTypeDeclSyntaxNode? { + package var asNominal: NominalTypeDeclSyntaxNode? { switch DeclSyntax(self).as(DeclSyntaxEnum.self) { case .actorDecl(let actorDecl): actorDecl case .classDecl(let classDecl): classDecl diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftQualifiedTypeName.swift b/Sources/SwiftExtract/SwiftTypes/SwiftQualifiedTypeName.swift similarity index 71% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftQualifiedTypeName.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftQualifiedTypeName.swift index 2c170ddb7..71cf956ca 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftQualifiedTypeName.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftQualifiedTypeName.swift @@ -18,33 +18,33 @@ /// - **qualifiedName** (`Logger.Message`) - for Swift source /// - **flatName** (`Logger_Message`) - for C symbols / `@_cdecl` and Java identifiers /// - **leafName** (`Message`) - innermost component only -package struct SwiftQualifiedTypeName: Hashable, Sendable, CustomStringConvertible { +public struct SwiftQualifiedTypeName: Hashable, Sendable, CustomStringConvertible { /// Name components from outermost to innermost, e.g. ["Logger", "Message"] - let components: [String] + public let components: [String] - init(_ components: [String]) { + public init(_ components: [String]) { precondition(!components.isEmpty) self.components = components } - init(_ leafName: String) { + public init(_ leafName: String) { self.components = [leafName] } /// Leaf name (innermost), e.g. "Message" - var leafName: String { components.last! } + public var leafName: String { components.last! } /// Dot-separated for Swift source, e.g. "Logger.Message" - var fullName: String { components.joined(separator: ".") } + public var fullName: String { components.joined(separator: ".") } /// Underscore-separated for C symbols and Java identifiers, e.g. "Logger_Message" - var fullFlatName: String { components.joined(separator: "_") } + public var fullFlatName: String { components.joined(separator: "_") } /// Dollar-separated for JNI C symbol parent names, e.g. "Logger$Message" - var jniEscapedName: String { components.joined(separator: "$") } + public var jniEscapedName: String { components.joined(separator: "$") } /// CustomStringConvertible - uses fullName - package var description: String { fullName } + public var description: String { fullName } - var isNested: Bool { components.count > 1 } + public var isNested: Bool { components.count > 1 } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftResult.swift b/Sources/SwiftExtract/SwiftTypes/SwiftResult.swift similarity index 64% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftResult.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftResult.swift index b7aa7b568..87fb57ea6 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftResult.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftResult.swift @@ -14,18 +14,23 @@ import SwiftSyntax -struct SwiftResult: Equatable { - var convention: SwiftResultConvention // currently not used. - var type: SwiftType +public struct SwiftResult: Equatable { + public var convention: SwiftResultConvention // currently not used. + public var type: SwiftType + + public init(convention: SwiftResultConvention, type: SwiftType) { + self.convention = convention + self.type = type + } } -enum SwiftResultConvention: Equatable { +public enum SwiftResultConvention: Equatable { case direct case indirect } extension SwiftResult { - static var void: Self { + public static var void: Self { Self(convention: .direct, type: .void) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift similarity index 66% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift index a4c4de73b..e8fb51020 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift @@ -12,13 +12,13 @@ // //===----------------------------------------------------------------------===// -import CodePrinting +import Logging import SwiftIfConfig import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -package protocol SwiftSymbolTableProtocol { +public protocol SwiftSymbolTableProtocol { /// The module name that this symbol table describes. var moduleName: String { get } @@ -38,7 +38,7 @@ package protocol SwiftSymbolTableProtocol { extension SwiftSymbolTableProtocol { /// Look for a type - package func lookupType(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftNominalTypeDeclaration? { + public func lookupType(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftNominalTypeDeclaration? { if let parent { return lookupNestedType(name, parent: parent) } @@ -46,7 +46,7 @@ extension SwiftSymbolTableProtocol { return lookupTopLevelNominalType(name) } - package func lookupTypealias(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftTypeAliasDeclaration? { + public func lookupTypealias(_ name: String, parent: SwiftNominalTypeDeclaration?) -> SwiftTypeAliasDeclaration? { if let parent { return lookupNestedTypealias(name, parent: parent) } @@ -55,9 +55,14 @@ extension SwiftSymbolTableProtocol { } } -package class SwiftSymbolTable { - let importedModules: [String: SwiftModuleSymbolTable] - let parsedModule: SwiftModuleSymbolTable +public class SwiftSymbolTable { + public let importedModules: [String: SwiftModuleSymbolTable] + public let parsedModule: SwiftModuleSymbolTable + + /// Module names within `importedModules` that are synthetic — they exist + /// purely to drive type resolution and must NOT be emitted as + /// `import ` statements in generated Swift code. + public let syntheticImportedModuleNames: Set private var knownTypeToNominal: [SwiftKnownTypeDeclKind: SwiftNominalTypeDeclaration] = [:] private var prioritySortedImportedModules: [SwiftModuleSymbolTable] { @@ -72,12 +77,17 @@ package class SwiftSymbolTable { }) } - init(parsedModule: SwiftModuleSymbolTable, importedModules: [String: SwiftModuleSymbolTable]) { + public init( + parsedModule: SwiftModuleSymbolTable, + importedModules: [String: SwiftModuleSymbolTable], + syntheticImportedModuleNames: Set = [] + ) { self.parsedModule = parsedModule self.importedModules = importedModules + self.syntheticImportedModuleNames = syntheticImportedModuleNames } - func isModuleName(_ name: String) -> Bool { + public func isModuleName(_ name: String) -> Bool { if name == moduleName { return true } @@ -88,11 +98,12 @@ package class SwiftSymbolTable { extension SwiftSymbolTable { package static func setup( moduleName: String, - _ inputFiles: some Collection, + _ inputFiles: some Collection, + additionalInputFiles: [SwiftInputFile] = [], config: Configuration?, sourceDependencies: SourceDependencies, - buildConfig: any BuildConfiguration = .jextractDefault, - log: Logger, + buildConfig: any BuildConfiguration = .swiftExtractDefault, + log: Logger? = nil, ) -> SwiftSymbolTable { // Prepare imported modules. @@ -122,7 +133,10 @@ extension SwiftSymbolTable { guard importedModules[dependencyModuleName] == nil else { continue } - let dependencyInputs = sourceDependencies.swiftModuleInputs[dependencyModuleName] ?? [] + let dependencyInputs = + sourceDependencies.swiftModuleInputs[dependencyModuleName] + ?? sourceDependencies.syntheticStubInputs[dependencyModuleName] + ?? [] // TODO: build a `dependencyImportedModules` dict by scanning the dep's // own source files with `importingModules(sourceFile:)`, instead of // reusing the primary's `importedModules`. The current set is too broad @@ -138,9 +152,8 @@ extension SwiftSymbolTable { } let dependencyModule = dependencyModuleBuilder.finalize() importedModules[dependencyModuleName] = dependencyModule - log.info( - "Loaded dependency module '\(dependencyModuleName)' from \(dependencyInputs.count) source(s); " - + "top-level types [\(dependencyModule.topLevelTypes.count)]: \(dependencyModule.topLevelTypes.keys.sorted())" + log?.info( + "Loaded dependency module '\(dependencyModuleName)' from \(dependencyInputs.count) source(s); top-level types [\(dependencyModule.topLevelTypes.count)]: \(dependencyModule.topLevelTypes.keys.sorted())" ) } @@ -160,13 +173,13 @@ extension SwiftSymbolTable { stubBuilder.handle(sourceFile: sourceFile, sourceFilePath: "\(stubModuleName)_stub.swift") let stubModule = stubBuilder.finalize() importedModules[stubModuleName] = stubModule - log.info("Loaded module stub for '\(stubModuleName)' with \(declarations.count) declaration(s), top-level types: \(stubModule.topLevelTypes.keys.sorted())") + log?.info("Loaded module stub for '\(stubModuleName)' with \(declarations.count) declaration(s), top-level types: \(stubModule.topLevelTypes.keys.sorted())") } else { - log.info("Module '\(stubModuleName)' already known, skipping stub") + log?.info("Module '\(stubModuleName)' already known, skipping stub") } } } else { - log.debug("No importedModuleStubs in config") + log?.debug("No importedModuleStubs in config") } // FIXME: Support granular lookup context (file, type context). @@ -181,20 +194,24 @@ extension SwiftSymbolTable { for sourceFile in inputFiles { builder.handle(sourceFile: sourceFile.syntax, sourceFilePath: sourceFile.path) } - if let stubs = sourceDependencies.syntheticJavaWrappersSwiftSource { - builder.handle(sourceFile: stubs.syntax, sourceFilePath: stubs.path) + for sourceFile in additionalInputFiles { + builder.handle(sourceFile: sourceFile.syntax, sourceFilePath: sourceFile.path) } let parsedModule = builder.finalize() - return SwiftSymbolTable(parsedModule: parsedModule, importedModules: importedModules) + return SwiftSymbolTable( + parsedModule: parsedModule, + importedModules: importedModules, + syntheticImportedModuleNames: sourceDependencies.syntheticModuleNames, + ) } } extension SwiftSymbolTable: SwiftSymbolTableProtocol { - package var moduleName: String { parsedModule.moduleName } + public var moduleName: String { parsedModule.moduleName } /// Look for a top-level nominal type with the given name. This should only /// return nominal types within this module. - package func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { + public func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { if let parsedResult = parsedModule.lookupTopLevelNominalType(name) { return parsedResult } @@ -209,7 +226,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } /// Look for a top-level nominal type in a specific module by name - package func lookupTopLevelNominalType(_ name: String, inModule moduleName: String) -> SwiftNominalTypeDeclaration? { + public func lookupTopLevelNominalType(_ name: String, inModule moduleName: String) -> SwiftNominalTypeDeclaration? { if moduleName == self.moduleName { return parsedModule.lookupTopLevelNominalType(name) } @@ -217,7 +234,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } /// Look for a top-level typealias with the given name. - package func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { + public func lookupTopLevelTypealias(_ name: String) -> SwiftTypeAliasDeclaration? { if let parsedResult = parsedModule.lookupTopLevelTypealias(name) { return parsedResult } @@ -232,7 +249,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } // Look for a nested type with the given name. - package func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { + public func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { if let parsedResult = parsedModule.lookupNestedType(name, parent: parent) { return parsedResult } @@ -247,7 +264,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { } // Look for a nested typealias with the given name. - package func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { + public func lookupNestedTypealias(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftTypeAliasDeclaration? { if let parsedResult = parsedModule.lookupNestedTypealias(name, parent: parent) { return parsedResult } @@ -264,7 +281,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { extension SwiftSymbolTable { /// Map 'SwiftKnownTypeDeclKind' to the declaration. - subscript(knownType: SwiftKnownTypeDeclKind) -> SwiftNominalTypeDeclaration! { + public subscript(knownType: SwiftKnownTypeDeclKind) -> SwiftNominalTypeDeclaration! { if let known = knownTypeToNominal[knownType] { return known } @@ -279,63 +296,3 @@ extension SwiftSymbolTable { return found } } - -extension SwiftSymbolTable { - func printImportedModules(_ printer: inout CodePrinter) { - let mainSymbolSourceModules = Set( - self.importedModules.values.filter { $0.alternativeModules?.isMainSourceOfSymbols ?? false }.map(\.moduleName) - ) - - for module in self.importedModules.keys.sorted() { - guard module != "Swift" else { - continue - } - - guard let alternativeModules = self.importedModules[module]?.alternativeModules else { - printer.print("import \(module)") - continue - } - - // Only the main source of symbols emits the conditional import block. - // Secondary modules (e.g. FoundationEssentials when Foundation is the main source) - // are skipped when their main source is already present, because the main source's - // block already covers the import. If no main source is present, fall back to a - // plain import so the module is still imported. - guard alternativeModules.isMainSourceOfSymbols else { - if mainSymbolSourceModules.isDisjoint(with: alternativeModules.moduleNames) { - printer.print("import \(module)") - } - continue - } - - var importGroups: [String: [String]] = [:] - for name in alternativeModules.moduleNames { - guard let otherModule = self.importedModules[name] else { continue } - - let groupKey = otherModule.requiredAvailablityOfModuleWithName ?? otherModule.moduleName - importGroups[groupKey, default: []].append(otherModule.moduleName) - } - - for (index, group) in importGroups.keys.sorted().enumerated() { - if index > 0 && importGroups.keys.count > 1 { - printer.print("#elseif canImport(\(group))") - } else { - printer.print("#if canImport(\(group))") - } - - for groupModule in importGroups[group] ?? [] { - printer.print("import \(groupModule)") - } - } - - if importGroups.keys.isEmpty { - printer.print("import \(module)") - } else { - printer.print("#else") - printer.print("import \(module)") - printer.print("#endif") - } - } - printer.println() - } -} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType+GenericTypes.swift similarity index 97% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftType+GenericTypes.swift index f6a8c5871..7949bf489 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType+GenericTypes.swift @@ -15,7 +15,7 @@ extension SwiftType { /// Returns a concrete type if this is a generic parameter in the list and it /// conforms to a protocol with representative concrete type. - func representativeConcreteTypeIn( + package func representativeConcreteTypeIn( knownTypes: SwiftKnownTypes, genericParameters: [SwiftGenericParameterDeclaration], genericRequirements: [SwiftGenericRequirement] @@ -29,7 +29,7 @@ extension SwiftType { } /// Returns the protocol type if this is a generic parameter in the list - func typeIn( + package func typeIn( genericParameters: [SwiftGenericParameterDeclaration], genericRequirements: [SwiftGenericRequirement] ) -> SwiftType? { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift similarity index 91% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 49bea52d7..d1b17a20e 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -15,11 +15,16 @@ import SwiftSyntax /// An element of a Swift tuple type, preserving the optional label. -struct SwiftTupleElement: Equatable, CustomStringConvertible { - var label: String? - var type: SwiftType +public struct SwiftTupleElement: Equatable, CustomStringConvertible { + public var label: String? + public var type: SwiftType - var description: String { + public init(label: String? = nil, type: SwiftType) { + self.label = label + self.type = type + } + + public var description: String { if let label { return "\(label): \(type)" } @@ -28,7 +33,7 @@ struct SwiftTupleElement: Equatable, CustomStringConvertible { } /// Describes a type in the Swift type system. -enum SwiftType: Equatable { +public enum SwiftType: Equatable { case nominal(SwiftNominalType) case genericParameter(SwiftGenericParameterDeclaration) @@ -50,11 +55,11 @@ enum SwiftType: Equatable { /// `type1` & `type2` indirect case composite([SwiftType]) - static var void: Self { + public static var void: Self { .tuple([]) } - var asNominalType: SwiftNominalType? { + public var asNominalType: SwiftNominalType? { switch self { case .nominal(let nominal): nominal case .tuple(let elements): elements.count == 1 ? elements[0].type.asNominalType : nil @@ -62,12 +67,12 @@ enum SwiftType: Equatable { } } - var asNominalTypeDeclaration: SwiftNominalTypeDeclaration? { + public var asNominalTypeDeclaration: SwiftNominalTypeDeclaration? { asNominalType?.nominalTypeDecl } /// Whether this is the "Void" type, which is actually an empty tuple. - var isVoid: Bool { + public var isVoid: Bool { switch self { case .tuple([]): return true @@ -80,7 +85,7 @@ enum SwiftType: Equatable { } /// Whether this is a pointer type. I.e 'Unsafe[Mutable][Raw]Pointer' - var isPointer: Bool { + public var isPointer: Bool { switch self { case .nominal(let nominal): if let knownType = nominal.nominalTypeDecl.knownTypeKind { @@ -96,7 +101,7 @@ enum SwiftType: Equatable { /// /// * Mutations don't require 'inout' convention. /// * The value is a pointer of the instance data, - var isReferenceType: Bool { + public var isReferenceType: Bool { switch self { case .nominal(let nominal): return nominal.nominalTypeDecl.isReferenceType @@ -107,7 +112,7 @@ enum SwiftType: Equatable { } } - var isUnsignedInteger: Bool { + public var isUnsignedInteger: Bool { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { @@ -118,7 +123,7 @@ enum SwiftType: Equatable { } } - var isArchDependingInteger: Bool { + public var isArchDependingInteger: Bool { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { @@ -129,7 +134,7 @@ enum SwiftType: Equatable { } } - var isRawTypeCompatible: Bool { + public var isRawTypeCompatible: Bool { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { @@ -153,7 +158,7 @@ extension SwiftType: CustomStringConvertible { } } - var description: String { + public var description: String { switch self { case .nominal(let nominal): return nominal.description case .genericParameter(let genericParam): return genericParam.packExpansionName @@ -186,23 +191,23 @@ extension SwiftType: CustomStringConvertible { } } -struct SwiftNominalType: Equatable { - indirect enum Parent: Equatable { +public struct SwiftNominalType: Equatable { + public indirect enum Parent: Equatable { case nominal(SwiftNominalType) } - enum SugarName: Equatable { + public enum SugarName: Equatable { case optional case array case dictionary } private var storedParent: Parent? - var sugarName: SugarName? - var nominalTypeDecl: SwiftNominalTypeDeclaration - var genericArguments: [SwiftType] + public var sugarName: SugarName? + public var nominalTypeDecl: SwiftNominalTypeDeclaration + public var genericArguments: [SwiftType] - init( + public init( parent: SwiftNominalType? = nil, sugarName: SugarName? = nil, nominalTypeDecl: SwiftNominalTypeDeclaration, @@ -215,7 +220,7 @@ struct SwiftNominalType: Equatable { self.genericArguments = genericArguments } - var parent: SwiftNominalType? { + public var parent: SwiftNominalType? { if case .nominal(let parent) = storedParent ?? .none { return parent } @@ -223,13 +228,13 @@ struct SwiftNominalType: Equatable { return nil } - package var asKnownType: SwiftKnownType? { + public var asKnownType: SwiftKnownType? { nominalTypeDecl.knownTypeKind.flatMap { SwiftKnownType(kind: $0, genericArguments: genericArguments) } } - var hasGenericParameter: Bool { + public var hasGenericParameter: Bool { genericArguments.contains { if case .genericParameter = $0 { return true @@ -240,7 +245,7 @@ struct SwiftNominalType: Equatable { } extension SwiftNominalType: CustomStringConvertible { - var description: String { + public var description: String { var resultString: String if let parent { resultString = parent.description + "." @@ -267,7 +272,7 @@ extension SwiftNominalType: CustomStringConvertible { } extension SwiftNominalType.Parent: CustomStringConvertible { - var description: String { + public var description: String { switch self { case .nominal(let nominal): return nominal.description @@ -276,17 +281,13 @@ extension SwiftNominalType.Parent: CustomStringConvertible { } extension SwiftNominalType { - var isSwiftJavaWrapper: Bool { - nominalTypeDecl.syntax.attributes.contains(where: \.isSwiftJavaMacro) - } - - var isProtocol: Bool { + public var isProtocol: Bool { nominalTypeDecl.kind == .protocol } } extension SwiftType { - init(_ type: TypeSyntax, lookupContext: SwiftTypeLookupContext) throws { + public init(_ type: TypeSyntax, lookupContext: SwiftTypeLookupContext) throws { var knownTypes: SwiftKnownTypes { SwiftKnownTypes(symbolTable: lookupContext.symbolTable) } @@ -438,7 +439,7 @@ extension SwiftType { } } - init( + public init( originalType: TypeSyntax, parent: SwiftType?, name: TokenSyntax, @@ -505,7 +506,7 @@ extension SwiftType { } } - init?( + public init?( nominalDecl: NamedDeclSyntax & DeclGroupSyntax, parent: SwiftType?, symbolTable: SwiftSymbolTable @@ -532,7 +533,7 @@ extension SwiftType { /// /// This is used e.g. by typealiases like `typealias Ano = Array`, /// so usages like `Ano` become `Array`. - func substituting(genericParameters substitutions: [String: SwiftType]) -> SwiftType { + public func substituting(genericParameters substitutions: [String: SwiftType]) -> SwiftType { guard !substitutions.isEmpty else { return self } switch self { @@ -579,7 +580,7 @@ extension SwiftType { /// Produce an expression that creates the metatype for this type in /// Swift source code. - var metatypeReferenceExprSyntax: ExprSyntax { + public var metatypeReferenceExprSyntax: ExprSyntax { let type: ExprSyntax = "\(raw: description)" if postfixRequiresParentheses { return "(\(type)).self" @@ -588,7 +589,7 @@ extension SwiftType { } } -enum TypeTranslationError: Error { +public enum TypeTranslationError: Error { /// We haven't yet implemented support for this type. case unimplementedType(TypeSyntax, file: StaticString = #file, line: Int = #line) @@ -600,7 +601,7 @@ enum TypeTranslationError: Error { } extension SwiftNominalTypeDeclaration { - var asSwiftNominalType: SwiftNominalType { + public var asSwiftNominalType: SwiftNominalType { let genericArguments = genericParameters.map { SwiftType.genericParameter($0) } return SwiftNominalType( parent: parent?.asSwiftNominalType, @@ -609,7 +610,7 @@ extension SwiftNominalTypeDeclaration { ) } - var asSwiftType: SwiftType { + public var asSwiftType: SwiftType { .nominal(asSwiftNominalType) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift similarity index 92% rename from Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift rename to Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift index 664fb7463..bee0ba061 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift @@ -19,8 +19,8 @@ import SwiftSyntax /// All type lookups should be done via this instance. This caches the /// association of `Syntax.ID` to `SwiftTypeDeclaration`, and guarantees that /// there's only one `SwiftTypeDeclaration` per declaration `Syntax`. -class SwiftTypeLookupContext { - var symbolTable: SwiftSymbolTable +public class SwiftTypeLookupContext { + public var symbolTable: SwiftSymbolTable private var typeDecls: [Syntax.ID: SwiftTypeDeclaration] = [:] @@ -28,7 +28,7 @@ class SwiftTypeLookupContext { /// cycles like `typealias A = B; typealias B = A`. private var resolvingAliases: Set = [] - init(symbolTable: SwiftSymbolTable) { + public init(symbolTable: SwiftSymbolTable) { self.symbolTable = symbolTable } @@ -37,7 +37,7 @@ class SwiftTypeLookupContext { /// - Parameters: /// - name: name to lookup /// - moduleName: the module to look in - func moduleQualifiedLookup(name: String, in moduleName: String) -> SwiftTypeDeclaration? { + public func moduleQualifiedLookup(name: String, in moduleName: String) -> SwiftTypeDeclaration? { symbolTable.lookupTopLevelNominalType(name, inModule: moduleName) } @@ -46,7 +46,7 @@ class SwiftTypeLookupContext { /// - Parameters: /// - name: name to lookup /// - node: `Syntax` node the lookup happened - func unqualifiedLookup(name: Identifier, from node: some SyntaxProtocol) throws -> SwiftTypeDeclaration? { + public func unqualifiedLookup(name: Identifier, from node: some SyntaxProtocol) throws -> SwiftTypeDeclaration? { for result in node.lookup(name) { switch result { @@ -110,7 +110,7 @@ class SwiftTypeLookupContext { /// Returns the type declaration object associated with the `Syntax` node. /// If there's no declaration created, create an instance on demand, and cache it. - func typeDeclaration(for node: some SyntaxProtocol, sourceFilePath: String) throws -> SwiftTypeDeclaration? { + public func typeDeclaration(for node: some SyntaxProtocol, sourceFilePath: String) throws -> SwiftTypeDeclaration? { if let found = typeDecls[node.id] { return found } @@ -207,7 +207,7 @@ class SwiftTypeLookupContext { } /// Resolve a typealias to the `SwiftType` of its right-hand side. - func resolve(typeAlias decl: SwiftTypeAliasDeclaration) throws -> SwiftType { + public func resolve(typeAlias decl: SwiftTypeAliasDeclaration) throws -> SwiftType { let id = decl.syntax.id guard !resolvingAliases.contains(id) else { throw TypeTranslationError.unimplementedType(TypeSyntax(decl.syntax.initializer.value)) @@ -218,6 +218,6 @@ class SwiftTypeLookupContext { } } -enum TypeLookupError: Error { +public enum TypeLookupError: Error { case notType(Syntax) } diff --git a/Sources/SwiftJavaTool/CommonOptions.swift b/Sources/SwiftJavaTool/CommonOptions.swift index 8f5e7ccc1..552cfea74 100644 --- a/Sources/SwiftJavaTool/CommonOptions.swift +++ b/Sources/SwiftJavaTool/CommonOptions.swift @@ -18,6 +18,7 @@ import JExtractSwiftLib import JavaNet import JavaUtilJar import Logging +import SwiftExtract import SwiftJava import SwiftJavaConfigurationShared import SwiftJavaShared @@ -63,7 +64,7 @@ extension SwiftJava { var inputSwift: String? = nil @Option(name: .shortAndLong, help: "Configure the level of logs that should be printed") - var logLevel: JExtractSwiftLib.Logger.Level = .info + var logLevel: SwiftExtract.Logger.Level = .info @Option(help: "A path to a custom swift-java.config to use") var config: String? = nil diff --git a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift index 19e3761b9..748114c30 100644 --- a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -19,6 +19,7 @@ import JavaLangReflect import JavaNet import JavaUtilJar import Logging +import SwiftExtract import SwiftJava import SwiftJavaConfigurationShared import SwiftJavaShared @@ -30,7 +31,7 @@ protocol SwiftJavaBaseAsyncParsableCommand: AsyncParsableCommand { var log: Logging.Logger { get } - var logLevel: JExtractSwiftLib.Logger.Level { get set } + var logLevel: SwiftExtract.Logger.Level { get set } /// Must be implemented with an `@OptionGroup` in Command implementations var commonOptions: SwiftJava.CommonOptions { get set } @@ -103,7 +104,7 @@ extension SwiftJavaBaseAsyncParsableCommand { .init(label: "swift-java") } - var logLevel: JExtractSwiftLib.Logger.Level { + var logLevel: SwiftExtract.Logger.Level { get { self.commonOptions.logLevel } diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index 06d1d5553..adc338058 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// @_spi(Testing) import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import SwiftSyntax import Testing @@ -34,7 +35,7 @@ func assertLoweredFunction( ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) if let sourceFile { translator.add(filePath: "Fake.swift", text: sourceFile) @@ -120,7 +121,7 @@ func assertLoweredVariableAccessor( ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) if let sourceFile { translator.add(filePath: "Fake.swift", text: sourceFile) diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index a4632f8ba..baeefe26d 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -51,11 +52,11 @@ func assertOutput( ) throws { var config = config ?? Configuration() config.swiftModule = swiftModuleName - let translator = Swift2JavaTranslator(config: config) - translator.sourceDependencies.javaClasses = Array(javaClassLookupTable.keys) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + translator.sourceDependencies.addJavaWrapperStubs(Array(javaClassLookupTable.keys)) for (depModule, depSource) in dependencySwiftSources { let syntax = Parser.parse(source: depSource) - let input = SwiftJavaInputFile(syntax: syntax, path: "/fake/\(depModule).swift") + let input = SwiftInputFile(syntax: syntax, path: "/fake/\(depModule).swift") translator.sourceDependencies.swiftModuleInputs[depModule] = [input] } diff --git a/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift index 4ed587812..4c4050cfc 100644 --- a/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift +++ b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -33,7 +34,7 @@ final class FFMNestedTypesTests { func test_nested_in_extension() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index fd9203750..8ff99fd82 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -41,7 +42,7 @@ final class FuncCallbackImportTests { func func_callMeFunc_callback() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) @@ -131,7 +132,7 @@ final class FuncCallbackImportTests { func func_callMeMoreFunc_callback() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) @@ -246,7 +247,7 @@ final class FuncCallbackImportTests { func func_withBuffer_body() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index c514a12fd..1733bab9e 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -237,7 +238,7 @@ extension FunctionDescriptorTests { ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = logLevel try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) @@ -271,7 +272,7 @@ extension FunctionDescriptorTests { ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = logLevel try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) diff --git a/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift b/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift index b89e574f7..643a09042 100644 --- a/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift +++ b/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -146,16 +147,16 @@ struct JExtractFileFilterTests { } // ==== ------------------------------------------------------------------- - // MARK: shouldJExtractFile tests + // MARK: shouldExtractSwiftFile tests @Test("No filters means everything passes") func noFilters() { var config = Configuration() - #expect(shouldJExtractFile(relativePath: "Anything.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Anything.swift", config: config)) config.swiftFilterInclude = [] config.swiftFilterExclude = [] - #expect(shouldJExtractFile(relativePath: "Anything.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Anything.swift", config: config)) } @Test("File include filter only") @@ -163,9 +164,9 @@ struct JExtractFileFilterTests { var config = Configuration() config.swiftFilterInclude = ["Models/**"] - #expect(shouldJExtractFile(relativePath: "Models/User.swift", config: config)) - #expect(shouldJExtractFile(relativePath: "Models/Sub/Deep.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Other/Thing.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/User.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/Sub/Deep.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Other/Thing.swift", config: config)) } @Test("File exclude filter only") @@ -173,8 +174,8 @@ struct JExtractFileFilterTests { var config = Configuration() config.swiftFilterExclude = ["Internal/*"] - #expect(shouldJExtractFile(relativePath: "Models/User.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Internal/Secret.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/User.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Internal/Secret.swift", config: config)) } @Test("File include and exclude combined") @@ -183,28 +184,28 @@ struct JExtractFileFilterTests { config.swiftFilterInclude = ["Models/**"] config.swiftFilterExclude = ["Models/Internal*"] - #expect(shouldJExtractFile(relativePath: "Models/User.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Models/InternalHelper.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Other/Thing.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/User.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Models/InternalHelper.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Other/Thing.swift", config: config)) } - @Test("Type-name patterns are ignored by shouldJExtractFile") + @Test("Type-name patterns are ignored by shouldExtractSwiftFile") func typeNamePatternsIgnoredByFileFilter() { var config = Configuration() config.swiftFilterInclude = ["Something.Other"] // Type-name-only includes should not restrict file-level filtering - #expect(shouldJExtractFile(relativePath: "Anything.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Anything.swift", config: config)) } // ==== ------------------------------------------------------------------- - // MARK: shouldJExtractType tests + // MARK: shouldExtractSwiftType tests @Test("No filters means all types pass") func noFiltersAllTypesPass() { let config = Configuration() - #expect(shouldJExtractType(qualifiedName: "Anything", config: config)) - #expect(shouldJExtractType(qualifiedName: "A.B.C", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Anything", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "A.B.C", config: config)) } @Test("Type include filter") @@ -212,8 +213,8 @@ struct JExtractFileFilterTests { var config = Configuration() config.swiftFilterInclude = ["Something.Other"] - #expect(shouldJExtractType(qualifiedName: "Something.Other", config: config)) - #expect(!shouldJExtractType(qualifiedName: "Something.Wrong", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Something.Other", config: config)) + #expect(!shouldExtractSwiftType(qualifiedName: "Something.Wrong", config: config)) } @Test("Type exclude filter") @@ -221,17 +222,17 @@ struct JExtractFileFilterTests { var config = Configuration() config.swiftFilterExclude = ["Something.Internal*"] - #expect(shouldJExtractType(qualifiedName: "Something.Other", config: config)) - #expect(!shouldJExtractType(qualifiedName: "Something.InternalHelper", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Something.Other", config: config)) + #expect(!shouldExtractSwiftType(qualifiedName: "Something.InternalHelper", config: config)) } - @Test("File-path patterns are ignored by shouldJExtractType") + @Test("File-path patterns are ignored by shouldExtractSwiftType") func filePathPatternsIgnoredByTypeFilter() { var config = Configuration() config.swiftFilterInclude = ["Models/**"] // File-path-only includes should not restrict type-level filtering - #expect(shouldJExtractType(qualifiedName: "Anything", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Anything", config: config)) } @Test("Plain pattern matches both file and type") @@ -240,12 +241,12 @@ struct JExtractFileFilterTests { config.swiftFilterInclude = ["MyType"] // Plain pattern works at file level (matched against filename segment) - #expect(shouldJExtractFile(relativePath: "MyType.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "OtherType.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "MyType.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "OtherType.swift", config: config)) // Plain pattern works at type level - #expect(shouldJExtractType(qualifiedName: "MyType", config: config)) - #expect(!shouldJExtractType(qualifiedName: "OtherType", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "MyType", config: config)) + #expect(!shouldExtractSwiftType(qualifiedName: "OtherType", config: config)) } @Test("Mixed file and type patterns in same config") @@ -254,12 +255,12 @@ struct JExtractFileFilterTests { config.swiftFilterInclude = ["Models/**", "Something.Other"] // File filter applies the file-path pattern - #expect(shouldJExtractFile(relativePath: "Models/User.swift", config: config)) - #expect(!shouldJExtractFile(relativePath: "Other/Thing.swift", config: config)) + #expect(shouldExtractSwiftFile(relativePath: "Models/User.swift", config: config)) + #expect(!shouldExtractSwiftFile(relativePath: "Other/Thing.swift", config: config)) // Type filter applies the type-name pattern - #expect(shouldJExtractType(qualifiedName: "Something.Other", config: config)) - #expect(!shouldJExtractType(qualifiedName: "Something.Wrong", config: config)) + #expect(shouldExtractSwiftType(qualifiedName: "Something.Other", config: config)) + #expect(!shouldExtractSwiftType(qualifiedName: "Something.Wrong", config: config)) } // ==== ------------------------------------------------------------------- @@ -329,12 +330,12 @@ struct JExtractFileFilterTests { private func makeTranslator( include: [String]? = nil, exclude: [String]? = nil, - ) throws -> Swift2JavaTranslator { + ) throws -> SwiftAnalyzer { var config = Configuration() config.swiftModule = "__FakeModule" config.swiftFilterInclude = include config.swiftFilterExclude = exclude - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) translator.log.logLevel = .error try translator.analyze(path: "Fake.swift", text: Self.nestedTypeSource) return translator diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift index 5339e07d6..9743dbfaa 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -354,7 +355,7 @@ struct JNIEnumTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try! translator.analyze(path: "/fake/Fake.swiftinterface", text: input) var printer: CodePrinter = CodePrinter(mode: .accumulateAll) diff --git a/Tests/JExtractSwiftTests/JavaTypeAnnotationsTests.swift b/Tests/JExtractSwiftTests/JavaTypeAnnotationsTests.swift index 031487368..9291fcde1 100644 --- a/Tests/JExtractSwiftTests/JavaTypeAnnotationsTests.swift +++ b/Tests/JExtractSwiftTests/JavaTypeAnnotationsTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import SwiftSyntax import Testing @@ -27,10 +28,9 @@ struct JavaTypeAnnotationsTests { init() { let symbolTable = SwiftSymbolTable.setup( moduleName: "TestModule", - [SwiftJavaInputFile(syntax: "" as SourceFileSyntax, path: "Fake.swift")], + [SwiftInputFile(syntax: "" as SourceFileSyntax, path: "Fake.swift")], config: nil, - sourceDependencies: SourceDependencies(), - log: Logger(label: "test", logLevel: .critical) + sourceDependencies: SourceDependencies() ) self.knownTypes = SwiftKnownTypes(symbolTable: symbolTable) self.config = Configuration() diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 678693d00..8e9bc917b 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -14,6 +14,7 @@ import CodePrinting import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -71,7 +72,7 @@ final class MethodImportTests { func method_helloWorld() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -113,7 +114,7 @@ final class MethodImportTests { func func_globalTakeInt() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -162,7 +163,7 @@ final class MethodImportTests { func func_globalTakeIntLongString() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -208,7 +209,7 @@ final class MethodImportTests { func func_globalReturnClass() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -254,7 +255,7 @@ final class MethodImportTests { func func_globalSwapRawBufferPointer() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -303,7 +304,7 @@ final class MethodImportTests { func method_class_helloMemberFunction() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -348,7 +349,7 @@ final class MethodImportTests { func method_class_makeInt() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -399,7 +400,7 @@ final class MethodImportTests { func class_constructor() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -453,7 +454,7 @@ final class MethodImportTests { func struct_constructor() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .info @@ -508,7 +509,7 @@ final class MethodImportTests { func func_globalReturnAny() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = Swift2JavaTranslator(config: config) + let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index f2303d453..876342941 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -75,7 +76,7 @@ struct SpecializationTests { func multipleSpecializationsProduceDistinctTypes() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) // Both specialized types should be registered @@ -132,7 +133,7 @@ struct SpecializationTests { func specializationEntriesContainAll() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) let baseBox = try #require(translator.importedTypes["Box"]) @@ -192,7 +193,7 @@ struct SpecializationTests { // Verify observeTheFish does NOT appear inside ToolBox's class body var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) let toolBox = try #require(translator.importedTypes["ToolBox"]) let methodNames = toolBox.methods.map(\.name) @@ -337,7 +338,7 @@ struct SpecializationTests { func specializeNonGenericTypeThrows() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze( path: "/fake/Fake.swiftinterface", text: """ diff --git a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift index 864457aa2..2517b884f 100644 --- a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift +++ b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// @_spi(Testing) import JExtractSwiftLib +import SwiftExtract import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -21,38 +22,6 @@ import Testing @Suite("Swift symbol table") struct SwiftSymbolTableSuite { - @Test func lookupBindingTests() throws { - let sourceFile1: SourceFileSyntax = """ - extension X.Y { - struct Z { } - } - extension X { - struct Y {} - } - """ - let sourceFile2: SourceFileSyntax = """ - struct X {} - """ - let symbolTable = SwiftSymbolTable.setup( - moduleName: "MyModule", - [ - .init(syntax: sourceFile1, path: "Fake.swift"), - .init(syntax: sourceFile2, path: "Fake2.swift"), - ], - config: nil, - sourceDependencies: SourceDependencies(), - log: Logger(label: "swift-java", logLevel: .critical), - ) - - let x = try #require(symbolTable.lookupType("X", parent: nil)) - let xy = try #require(symbolTable.lookupType("Y", parent: x)) - let xyz = try #require(symbolTable.lookupType("Z", parent: xy)) - #expect(xyz.qualifiedName == "X.Y.Z") - - #expect(symbolTable.lookupType("Y", parent: nil) == nil) - #expect(symbolTable.lookupType("Z", parent: nil) == nil) - } - @Test(arguments: [JExtractGenerationMode.jni, .ffm]) func resolveSelfModuleName(mode: JExtractGenerationMode) throws { try assertOutput( @@ -94,35 +63,6 @@ struct SwiftSymbolTableSuite { ) } - @Test func moduleScopedLookup() throws { - let sourceFile: SourceFileSyntax = """ - public struct MyClass {} - """ - let symbolTable = SwiftSymbolTable.setup( - moduleName: "MyModule", - [ - .init(syntax: sourceFile, path: "Fake.swift") - ], - config: nil, - sourceDependencies: SourceDependencies(), - log: Logger(label: "swift-java", logLevel: .critical), - ) - - // Lookup in self-module by qualified name - let myClass = symbolTable.lookupTopLevelNominalType("MyClass", inModule: "MyModule") - #expect(myClass != nil) - #expect(myClass?.qualifiedName == "MyClass") - - // Lookup in imported module (Swift) - let swiftInt = symbolTable.lookupTopLevelNominalType("Int", inModule: "Swift") - #expect(swiftInt != nil) - #expect(swiftInt?.qualifiedName == "Int") - - // Lookup in unknown module returns nil - let unknown = symbolTable.lookupTopLevelNominalType("Foo", inModule: "NoSuchModule") - #expect(unknown == nil) - } - @Test(arguments: [JExtractGenerationMode.jni, .ffm]) func resolveQualifiedTypesInFunctionSignatures(mode: JExtractGenerationMode) throws { try assertOutput( diff --git a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift index 55cabb115..dff3ed40f 100644 --- a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift +++ b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import SwiftExtract import SwiftJavaConfigurationShared import Testing @@ -48,7 +49,7 @@ struct TypealiasResolutionTests { func primitiveAliasResolvesStructMembers() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) let user = try #require(translator.importedTypes["TypealiasUser"]) @@ -62,7 +63,7 @@ struct TypealiasResolutionTests { func primitiveAliasResolvesFreeFunc() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) #expect( @@ -88,7 +89,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let holder = try #require(translator.importedTypes["Holder"]) @@ -112,7 +113,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrapOrZero" }) @@ -143,7 +144,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "describe" }) @@ -175,7 +176,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let holder = try #require(translator.importedTypes["Holder"]) @@ -200,7 +201,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) // The struct itself is still imported, but its members are dropped @@ -259,7 +260,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "passA" }) @@ -284,7 +285,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrap" }) @@ -309,7 +310,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "first" }) @@ -343,7 +344,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "add" }) @@ -375,7 +376,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let player = try #require(translator.importedTypes["Player"]) @@ -400,7 +401,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = Swift2JavaTranslator(config: config) + let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "openIntBag" }) diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift new file mode 100644 index 000000000..ac3123ba5 --- /dev/null +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -0,0 +1,317 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract +import SwiftJavaConfigurationShared +import Testing + +/// End-to-end tests that drive the analysis pipeline (Swift source → +/// `AnalysisResult`) without touching any code-generation layer. These verify +/// `SwiftExtract` produces a correct analysis snapshot independent of any +/// downstream language target. +@Suite("AnalysisResult") +struct AnalysisResultSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Top-level types + + @Test func topLevelTypesAreRecorded() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + } + public class FishTank { + public init() {} + } + public enum Status { + case open, closed + } + """ + ) + ], + moduleName: "Aquarium" + ) + + #expect(result.importedTypes["Tank"] != nil) + #expect(result.importedTypes["FishTank"] != nil) + #expect(result.importedTypes["Status"] != nil) + + let tank = try #require(result.importedTypes["Tank"]) + #expect(tank.swiftNominal.kind == .struct) + #expect(tank.swiftNominal.isGeneric) + + let fishTank = try #require(result.importedTypes["FishTank"]) + #expect(fishTank.swiftNominal.kind == .class) + + let status = try #require(result.importedTypes["Status"]) + #expect(status.swiftNominal.kind == .enum) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Methods on a type + + @Test func methodsAreRecordedOnEnclosingType() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class FishTank { + public init() {} + public func feed() {} + public func count() -> Int { 0 } + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let fishTank = try #require(result.importedTypes["FishTank"]) + let methodNames = Set(fishTank.methods.map(\.name)) + #expect(methodNames == ["feed", "count"]) + #expect(fishTank.initializers.count == 1) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Properties (variables) — getter/setter pair + + @Test func storedPropertyProducesGetterAndSetter() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class FishTank { + public init() {} + public var capacity: Int = 0 + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let fishTank = try #require(result.importedTypes["FishTank"]) + let capacityAccessors = fishTank.variables.filter { $0.name == "capacity" } + let kinds = Set(capacityAccessors.map(\.apiKind)) + #expect(kinds == [.getter, .setter]) + } + + @Test func readOnlyPropertyHasOnlyGetter() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class FishTank { + public init() {} + public var name: String { "Fish Tank" } + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let fishTank = try #require(result.importedTypes["FishTank"]) + let nameAccessors = fishTank.variables.filter { $0.name == "name" } + let kinds = nameAccessors.map(\.apiKind) + #expect(kinds == [.getter]) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Global functions and variables + + @Test func globalFunctionLandsInImportedGlobalFuncs() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public func feedAll() {} + public func mood() -> String { "" } + """ + ) + ], + moduleName: "Aquarium" + ) + + let names = Set(result.importedGlobalFuncs.map(\.name)) + #expect(names == ["feedAll", "mood"]) + #expect(result.importedTypes.isEmpty) + } + + @Test func globalVariableProducesGetterSetterPair() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public var globalCounter: Int = 0 + """ + ) + ], + moduleName: "Aquarium" + ) + + let counterAccessors = result.importedGlobalVariables.filter { $0.name == "globalCounter" } + let kinds = Set(counterAccessors.map(\.apiKind)) + #expect(kinds == [.getter, .setter]) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Effect specifiers (throws / async) + + @Test func effectSpecifiersAreCapturedOnFunctionSignatures() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public func plain() {} + public func throwing() throws {} + public func asynchronous() async {} + public func both() async throws {} + """ + ) + ], + moduleName: "Aquarium" + ) + + let byName = Dictionary(uniqueKeysWithValues: result.importedGlobalFuncs.map { ($0.name, $0) }) + let plain = try #require(byName["plain"]) + #expect(plain.functionSignature.effectSpecifiers.isEmpty) + + let throwing = try #require(byName["throwing"]) + #expect(throwing.functionSignature.effectSpecifiers.contains(.throws)) + #expect(!throwing.functionSignature.effectSpecifiers.contains(.async)) + + let asynchronous = try #require(byName["asynchronous"]) + #expect(asynchronous.functionSignature.effectSpecifiers.contains(.async)) + #expect(!asynchronous.functionSignature.effectSpecifiers.contains(.throws)) + + let both = try #require(byName["both"]) + #expect(both.functionSignature.effectSpecifiers.contains(.async)) + #expect(both.functionSignature.effectSpecifiers.contains(.throws)) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Access-level filtering + + @Test func internalDeclarationsAreNotImportedByDefault() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class Public { + public init() {} + } + internal class Internal { + init() {} + } + private class Private { + init() {} + } + """ + ) + ], + moduleName: "Aquarium" + ) + + #expect(result.importedTypes["Public"] != nil) + #expect(result.importedTypes["Internal"] == nil) + #expect(result.importedTypes["Private"] == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Filter include/exclude + + @Test func swiftFilterExcludeSkipsMatchingTypes() throws { + var config = Configuration() + config.swiftFilterExclude = ["Skip*"] + + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public class Tank { + public init() {} + } + public class SkipMe { + public init() {} + } + public class SkipAlso { + public init() {} + } + """ + ) + ], + moduleName: "Aquarium", + config: config + ) + + #expect(result.importedTypes["Tank"] != nil) + #expect(result.importedTypes["SkipMe"] == nil) + #expect(result.importedTypes["SkipAlso"] == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Generic typealias produces a specialization + + @Test func genericTypealiasProducesSpecializationEntry() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + } + public struct Fish {} + public typealias FishTank = Tank + """ + ) + ], + moduleName: "Aquarium" + ) + + // Both the generic base and its specialization land in importedTypes. + #expect(result.importedTypes["Tank"] != nil) + let fishTank = try #require(result.importedTypes["FishTank"]) + #expect(fishTank.isSpecialization) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Empty input + + @Test func emptyModuleProducesEmptyResult() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ("/fake/Source.swift", "// nothing here") + ], + moduleName: "Empty" + ) + + #expect(result.importedTypes.isEmpty) + #expect(result.importedGlobalFuncs.isEmpty) + #expect(result.importedGlobalVariables.isEmpty) + } +} diff --git a/Tests/SwiftExtractTests/SourceDependenciesTests.swift b/Tests/SwiftExtractTests/SourceDependenciesTests.swift new file mode 100644 index 000000000..278a1dde5 --- /dev/null +++ b/Tests/SwiftExtractTests/SourceDependenciesTests.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract +import SwiftParser +import SwiftSyntax +import Testing + +@Suite("SourceDependencies") +struct SourceDependenciesSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Real dependency module + + @Test func realDependencyModuleResolvesItsTypes() throws { + var deps = SourceDependencies() + deps.swiftModuleInputs["DepModule"] = [ + makeInputFile("public class DepClass {}", path: "Dep.swift") + ] + + let symbolTable = makeSymbolTable( + moduleName: "MyModule", + sources: ["public func use(_ x: DepClass) {}"], + sourceDependencies: deps + ) + + let dep = try #require(symbolTable.lookupTopLevelNominalType("DepClass")) + #expect(dep.moduleName == "DepModule") + #expect(dep.kind == .class) + + // Module-scoped lookup also works. + let depViaModule = symbolTable.lookupTopLevelNominalType("DepClass", inModule: "DepModule") + #expect(depViaModule === dep) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Synthetic stubs + + /// Verifies the fix that keeps `` resolvable for type lookup + /// while excluding it from anything that would emit `import `. + @Test func syntheticStubsAreResolvableButNotPrintable() throws { + var deps = SourceDependencies() + deps.syntheticStubInputs[""] = [ + makeInputFile("@JavaClass public class JavaUtilFunction {}", path: ".swift") + ] + + let symbolTable = makeSymbolTable( + moduleName: "MyModule", + sources: ["public struct Anything {}"], + sourceDependencies: deps + ) + + // The stub type is resolvable. + let stub = try #require(symbolTable.lookupTopLevelNominalType("JavaUtilFunction")) + #expect(stub.moduleName == "") + + // The synthetic name is recorded as such. + #expect(symbolTable.syntheticImportedModuleNames.contains("")) + + // It must NOT be confused with a real module — `Swift` is not synthetic. + #expect(!symbolTable.syntheticImportedModuleNames.contains("Swift")) + } + + // ==== ----------------------------------------------------------------------- + // MARK: swiftModuleNames / syntheticModuleNames + + @Test func moduleNameSetsAreSeparateAndUnion() { + var deps = SourceDependencies() + deps.swiftModuleInputs["DepModule"] = [makeInputFile("public class A {}")] + deps.syntheticStubInputs[""] = [ + makeInputFile("@JavaClass public class B {}") + ] + + #expect(deps.swiftModuleNames == Set(["DepModule", ""])) + #expect(deps.syntheticModuleNames == Set([""])) + } + + @Test func mutatingOneFieldDoesNotAffectTheOther() { + var deps = SourceDependencies() + deps.swiftModuleInputs["DepModule"] = [makeInputFile("public class A {}")] + #expect(deps.syntheticModuleNames.isEmpty) + + deps.syntheticStubInputs[""] = [ + makeInputFile("@JavaClass public class B {}") + ] + #expect(deps.swiftModuleInputs.keys.contains("DepModule")) + #expect(!deps.swiftModuleInputs.keys.contains("")) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Empty dependencies + + @Test func emptyDependenciesProduceUsableSymbolTable() throws { + let symbolTable = makeSymbolTable(sources: ["public struct X {}"]) + + // Built-in Swift module is still available even with no dependencies. + #expect(symbolTable.lookupTopLevelNominalType("Int") != nil) + + // No synthetic modules. + #expect(symbolTable.syntheticImportedModuleNames.isEmpty) + } +} diff --git a/Tests/SwiftExtractTests/Support/SymbolTableFixture.swift b/Tests/SwiftExtractTests/Support/SymbolTableFixture.swift new file mode 100644 index 000000000..4d5e36ac0 --- /dev/null +++ b/Tests/SwiftExtractTests/Support/SymbolTableFixture.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract +import SwiftParser +import SwiftSyntax + +/// Build a `SwiftSymbolTable` from inline Swift source strings. +/// +/// Each entry in `sources` becomes a synthetic `Test.swift` file feeding +/// the primary module being analysed. +func makeSymbolTable( + moduleName: String = "TestModule", + sources: [String], + sourceDependencies: SourceDependencies = SourceDependencies() +) -> SwiftSymbolTable { + let inputs: [SwiftInputFile] = sources.enumerated().map { (i, src) in + SwiftInputFile( + syntax: Parser.parse(source: src), + path: "Test\(i).swift" + ) + } + return SwiftSymbolTable.setup( + moduleName: moduleName, + inputs, + config: nil, + sourceDependencies: sourceDependencies, + ) +} + +/// Convenience: build a single `SwiftInputFile` from a source string. +func makeInputFile(_ source: String, path: String = "Dep.swift") -> SwiftInputFile { + SwiftInputFile(syntax: Parser.parse(source: source), path: path) +} diff --git a/Tests/SwiftExtractTests/SwiftKnownModuleTests.swift b/Tests/SwiftExtractTests/SwiftKnownModuleTests.swift new file mode 100644 index 000000000..a1912cc2c --- /dev/null +++ b/Tests/SwiftExtractTests/SwiftKnownModuleTests.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract +import Testing + +@Suite("SwiftKnownModule and SwiftKnownTypes") +struct SwiftKnownModuleSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Built-in Swift module catalog + + @Test(arguments: [ + "Int", "Int8", "Int16", "Int32", "Int64", + "UInt", "UInt8", "UInt16", "UInt32", "UInt64", + "Float", "Double", + "Bool", "String", + "Array", "Dictionary", "Set", "Optional", + ]) + func swiftModuleContains(_ typeName: String) throws { + let table = SwiftKnownModule.swift.symbolTable + let decl = try #require(table.lookupTopLevelNominalType(typeName)) + #expect(decl.name == typeName) + #expect(decl.moduleName == "Swift") + } + + // ==== ----------------------------------------------------------------------- + // MARK: SwiftKnownTypes accessor + + @Test func knownTypesExposeNominalSwiftStdlibTypes() throws { + let symbolTable = makeSymbolTable(sources: ["public struct X {}"]) + let known = SwiftKnownTypes(symbolTable: symbolTable) + + // Each accessor must yield a nominal type whose decl is the corresponding + // Swift stdlib type. + let int8Decl = try #require(known.int8.asNominalTypeDeclaration) + #expect(int8Decl.knownTypeKind == .int8) + + let uint8Decl = try #require(known.uint8.asNominalTypeDeclaration) + #expect(uint8Decl.knownTypeKind == .uint8) + + let stringDecl = try #require(known.string.asNominalTypeDeclaration) + #expect(stringDecl.knownTypeKind == .string) + + let boolDecl = try #require(known.bool.asNominalTypeDeclaration) + #expect(boolDecl.knownTypeKind == .bool) + + let doubleDecl = try #require(known.double.asNominalTypeDeclaration) + #expect(doubleDecl.knownTypeKind == .double) + } +} diff --git a/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift b/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift new file mode 100644 index 000000000..4b8e44dbd --- /dev/null +++ b/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift @@ -0,0 +1,192 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract +import SwiftParser +import SwiftSyntax +import Testing + +@Suite("SwiftSymbolTable") +struct SwiftSymbolTableSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Lookup binding (moved from JExtractSwiftTests) + + @Test func lookupBindingTests() throws { + let sourceFile1: SourceFileSyntax = """ + extension X.Y { + struct Z { } + } + extension X { + struct Y {} + } + """ + let sourceFile2: SourceFileSyntax = """ + struct X {} + """ + let symbolTable = SwiftSymbolTable.setup( + moduleName: "MyModule", + [ + .init(syntax: sourceFile1, path: "Fake.swift"), + .init(syntax: sourceFile2, path: "Fake2.swift"), + ], + config: nil, + sourceDependencies: SourceDependencies(), + ) + + let x = try #require(symbolTable.lookupType("X", parent: nil)) + let xy = try #require(symbolTable.lookupType("Y", parent: x)) + let xyz = try #require(symbolTable.lookupType("Z", parent: xy)) + #expect(xyz.qualifiedName == "X.Y.Z") + + #expect(symbolTable.lookupType("Y", parent: nil) == nil) + #expect(symbolTable.lookupType("Z", parent: nil) == nil) + } + + @Test func moduleScopedLookup() throws { + let symbolTable = makeSymbolTable( + moduleName: "MyModule", + sources: ["public struct MyClass {}"] + ) + + // Lookup in self-module by qualified name + let myClass = symbolTable.lookupTopLevelNominalType("MyClass", inModule: "MyModule") + #expect(myClass != nil) + #expect(myClass?.qualifiedName == "MyClass") + + // Lookup in imported module (Swift) + let swiftInt = symbolTable.lookupTopLevelNominalType("Int", inModule: "Swift") + #expect(swiftInt != nil) + #expect(swiftInt?.qualifiedName == "Int") + + // Lookup in unknown module returns nil + let unknown = symbolTable.lookupTopLevelNominalType("Foo", inModule: "NoSuchModule") + #expect(unknown == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Top-level lookup by nominal kind + + @Test func topLevelLookupResolvesEachNominalKind() throws { + let symbolTable = makeSymbolTable(sources: [ + """ + public struct S {} + public class C {} + public enum E { case a } + public actor A {} + public protocol P {} + """ + ]) + + let s = try #require(symbolTable.lookupTopLevelNominalType("S")) + #expect(s.kind == .struct) + + let c = try #require(symbolTable.lookupTopLevelNominalType("C")) + #expect(c.kind == .class) + + let e = try #require(symbolTable.lookupTopLevelNominalType("E")) + #expect(e.kind == .enum) + + let a = try #require(symbolTable.lookupTopLevelNominalType("A")) + #expect(a.kind == .actor) + + let p = try #require(symbolTable.lookupTopLevelNominalType("P")) + #expect(p.kind == .protocol) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Nested-type lookup, multiple levels + + @Test func nestedLookupTwoLevelsDeep() throws { + let symbolTable = makeSymbolTable(sources: [ + """ + public struct A { + public struct B { + public struct C {} + } + } + """ + ]) + + let a = try #require(symbolTable.lookupTopLevelNominalType("A")) + let b = try #require(symbolTable.lookupNestedType("B", parent: a)) + let c = try #require(symbolTable.lookupNestedType("C", parent: b)) + #expect(c.qualifiedName == "A.B.C") + + // C is not a top-level type + #expect(symbolTable.lookupTopLevelNominalType("C") == nil) + // B is not nested under C + #expect(symbolTable.lookupNestedType("B", parent: c) == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Negative lookups + + @Test func unknownNamesReturnNil() throws { + let symbolTable = makeSymbolTable(sources: [ + """ + public struct Known { + public struct Inner {} + } + """ + ]) + + #expect(symbolTable.lookupTopLevelNominalType("DoesNotExist") == nil) + #expect(symbolTable.lookupTopLevelTypealias("AlsoMissing") == nil) + + let known = try #require(symbolTable.lookupTopLevelNominalType("Known")) + #expect(symbolTable.lookupNestedType("Missing", parent: known) == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Typealias resolution + + @Test func topLevelTypealiasResolvesUnderlyingType() throws { + let symbolTable = makeSymbolTable(sources: [ + """ + public typealias Alias = Int + """ + ]) + + let alias = try #require(symbolTable.lookupTopLevelTypealias("Alias")) + #expect(alias.name == "Alias") + } + + // ==== ----------------------------------------------------------------------- + // MARK: Built-in module presence + + @Test func builtInSwiftModuleIsAlwaysRegistered() throws { + let symbolTable = makeSymbolTable(sources: ["public struct Anything {}"]) + + // Same lookup, two reachable paths: implicit cross-module, and module-scoped. + let int = try #require(symbolTable.lookupTopLevelNominalType("Int")) + #expect(int.moduleName == "Swift") + + let intInSwift = try #require(symbolTable.lookupTopLevelNominalType("Int", inModule: "Swift")) + #expect(intInSwift === int) + + #expect(symbolTable.lookupTopLevelNominalType("Int", inModule: "NoSuchModule") == nil) + } + + // ==== ----------------------------------------------------------------------- + // MARK: isModuleName + + @Test func isModuleNameRecognisesOwnAndImportedModules() throws { + let symbolTable = makeSymbolTable(moduleName: "MyModule", sources: ["public struct X {}"]) + + #expect(symbolTable.isModuleName("MyModule")) + #expect(symbolTable.isModuleName("Swift")) + #expect(!symbolTable.isModuleName("NotAModule")) + } +} From 8163665880a57958a3d05be5645297c17330e3dd Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 1 Jun 2026 14:23:14 +0900 Subject: [PATCH 02/38] SwiftExtract: move java bean conventions out of SwiftExtract --- .../Convenience/String+JavaNaming.swift | 26 +++++++++++++++++++ .../Convenience/String+Extensions.swift | 10 ------- 2 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 Sources/JExtractSwiftLib/Convenience/String+JavaNaming.swift 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/SwiftExtract/Convenience/String+Extensions.swift b/Sources/SwiftExtract/Convenience/String+Extensions.swift index 34fcafe0c..443984b4e 100644 --- a/Sources/SwiftExtract/Convenience/String+Extensions.swift +++ b/Sources/SwiftExtract/Convenience/String+Extensions.swift @@ -30,16 +30,6 @@ extension String { return "\(f.lowercased())\(String(dropFirst()))" } - /// Returns whether the string is of the format `isX` - package var hasJavaBooleanNamingConvention: Bool { - guard self.hasPrefix("is"), self.count > 2 else { - return false - } - - let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2) - return self[thirdCharacterIndex].isUppercase - } - /// If the string ends with `.swift`, return it without that suffix; /// otherwise return self unchanged package func dropSwiftFileSuffix() -> String { From 83bd17f69edd925eb68eae122eb807993c8d48ce Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 1 Jun 2026 15:46:07 +0900 Subject: [PATCH 03/38] SwiftExtract: remove swiftjavaerror special handling from swiftextract --- ...Swift2JavaGenerator+FunctionLowering.swift | 4 -- ...MSwift2JavaGenerator+JavaTranslation.swift | 8 +--- .../FFM/FFMSwift2JavaGenerator.swift | 42 +++++++++---------- .../JNI/JNIJavaTypeTranslator.swift | 9 ++-- .../SwiftKnownTypes+Java.swift | 40 ++++++++++++++++++ .../SwiftTypes/SwiftKnownTypeDecls.swift | 23 ---------- 6 files changed, 66 insertions(+), 60 deletions(-) create mode 100644 Sources/JExtractSwiftLib/SwiftKnownTypes+Java.swift diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 5a39f845b..31172d11b 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -365,10 +365,6 @@ struct CdeclLowering { case .foundationData, .essentialsData: break - case .swiftJavaError: - // SwiftJavaError is a class — treat as arbitrary nominal type below - break - default: // Unreachable? Should be handled by `CType(cdeclType:)` lowering above. throw LoweringError.unhandledType(type) diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 2bd12a50c..c3182c79d 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -18,7 +18,7 @@ import SwiftJavaJNICore extension FFMSwift2JavaGenerator { func translatedDecl( - for decl: ImportedFunc + for decl: ExtractedFunc ) -> TranslatedFunctionDecl? { if let cached = translatedDecls[decl] { return cached @@ -176,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) @@ -451,10 +451,6 @@ extension FFMSwift2JavaGenerator { case .foundationData, .essentialsData: break - case .swiftJavaError: - // SwiftJavaError is a class — treat as arbitrary nominal type below - break - default: throw JavaTranslationError.unhandledType(swiftType) } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 08890b3b9..5d22e3baa 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -38,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() @@ -152,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 @@ -171,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 }) } @@ -227,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 @@ -253,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 @@ -387,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 @@ -425,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) @@ -449,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) @@ -569,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()); @@ -582,7 +582,7 @@ extension FFMSwift2JavaGenerator { func printToStringMethod( _ printer: inout CodePrinter, - _ decl: ImportedNominalType, + _ decl: ExtractedNominalType, ) { printer.print( """ @@ -599,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 } @@ -615,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/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index b676a8385..3e7821d48 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -53,8 +53,7 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID, - .swiftJavaError: + .foundationUUID, .essentialsUUID: return nil } } @@ -80,8 +79,7 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID, - .swiftJavaError: + .foundationUUID, .essentialsUUID: nil } } @@ -107,8 +105,7 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID, - .swiftJavaError: + .foundationUUID, .essentialsUUID: nil } } 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/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift index 57a851b1d..7b4ea12ea 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownTypeDecls.swift @@ -53,9 +53,6 @@ public enum SwiftKnownType: Equatable { case foundationUUID case essentialsUUID - // SwiftRuntimeFunctions - case swiftJavaError - public init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { switch kind { case .bool: self = .bool @@ -109,7 +106,6 @@ public enum SwiftKnownType: Equatable { case .essentialsDate: self = .essentialsDate case .foundationUUID: self = .foundationUUID case .essentialsUUID: self = .essentialsUUID - case .swiftJavaError: self = .swiftJavaError } } @@ -150,7 +146,6 @@ public enum SwiftKnownType: Equatable { case .essentialsDate: .essentialsDate case .foundationUUID: .foundationUUID case .essentialsUUID: .essentialsUUID - case .swiftJavaError: .swiftJavaError } } } @@ -195,9 +190,6 @@ public enum SwiftKnownTypeDeclKind: String, Hashable { case foundationUUID = "Foundation.UUID" case essentialsUUID = "FoundationEssentials.UUID" - // SwiftRuntimeFunctions - case swiftJavaError = "SwiftRuntimeFunctions.SwiftJavaError" - public var moduleAndName: (module: String, name: String) { let qualified = self.rawValue let period = qualified.firstIndex(of: ".")! @@ -215,19 +207,4 @@ public 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 - public 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 - } - } } From 386a749e2dc8db4f1632d37a4b845cb6070b8b17 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 1 Jun 2026 16:03:03 +0900 Subject: [PATCH 04/38] SwiftExtract: change naming scheme; types are "extracted" since SwiftExtract --- ....swift => ExtractedDecls+JavaNaming.swift} | 15 +++- ...FMSwift2JavaGenerator+FoundationData.swift | 2 +- ...t2JavaGenerator+JavaBindingsPrinting.swift | 12 +-- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 34 ++++---- Sources/JExtractSwiftLib/JNI/JNICaching.swift | 6 +- ...Generator+InterfaceWrapperGeneration.swift | 16 ++-- ...t2JavaGenerator+JavaBindingsPrinting.swift | 50 +++++------ ...ISwift2JavaGenerator+JavaTranslation.swift | 24 +++--- ...wift2JavaGenerator+NativeTranslation.swift | 2 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 60 ++++++------- .../JNI/JNISwift2JavaGenerator.swift | 12 +-- .../JavaIdentifierFactory.swift | 10 +-- .../JExtractSwiftLib/SwiftKit+Printing.swift | 74 +--------------- .../JExtractSwiftLib/ThunkNameRegistry.swift | 4 +- .../TranslatedDocumentation.swift | 4 +- Sources/SwiftExtract/AnalysisResult.swift | 18 ++-- ...portedDecls.swift => ExtractedDecls.swift} | 58 ++++++------- .../SwiftExtract/SwiftAnalysisVisitor.swift | 84 +++++++++---------- Sources/SwiftExtract/SwiftAnalyzer.swift | 46 +++++----- ...odule.swift => ExtractedSwiftModule.swift} | 2 +- .../SwiftTypes/SwiftDependencyScanner.swift | 14 ++-- .../SwiftTypes/SwiftSymbolTable.swift | 2 +- Sources/SwiftJavaToolLib/JavaTranslator.swift | 6 +- .../FFMNestedTypesTests.swift | 2 +- .../FuncCallbackImportTests.swift | 6 +- .../FunctionDescriptorImportTests.swift | 6 +- .../JExtractFileFilterTests.swift | 56 ++++++------- .../MethodImportTests.swift | 28 +++---- .../SpecializationTests.swift | 30 +++---- .../TypealiasResolutionTests.swift | 26 +++--- .../AnalysisResultTests.swift | 50 +++++------ 31 files changed, 348 insertions(+), 411 deletions(-) rename Sources/JExtractSwiftLib/{ImportedDecls+JavaNaming.swift => ExtractedDecls+JavaNaming.swift} (87%) rename Sources/SwiftExtract/{ImportedDecls.swift => ExtractedDecls.swift} (87%) rename Sources/SwiftExtract/SwiftTypes/{ImportedSwiftModule.swift => ExtractedSwiftModule.swift} (96%) diff --git a/Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift similarity index 87% rename from Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift rename to Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift index 6f71df330..d8de44734 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls+JavaNaming.swift +++ b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift @@ -15,9 +15,16 @@ import SwiftExtract // ==== ----------------------------------------------------------------------- -// MARK: Java-facing name aliases for ImportedNominalType +// MARK: Java name typealiases -extension ImportedNominalType { +package typealias JavaClassName = String +package typealias JavaFullyQualifiedClassName = String +package typealias JavaPackageName = String + +// ==== ----------------------------------------------------------------------- +// MARK: Java-facing name aliases for ExtractedNominalType + +extension ExtractedNominalType { package var effectiveJavaTypeName: SwiftQualifiedTypeName { effectiveOutputTypeName } package var effectiveJavaName: String { effectiveOutputName } package var effectiveJavaSimpleName: String { effectiveOutputSimpleName } @@ -25,9 +32,9 @@ extension ImportedNominalType { } // ==== ----------------------------------------------------------------------- -// MARK: Java-facing name aliases for ImportedFunc +// MARK: Java-facing name aliases for ExtractedFunc -extension ImportedFunc { +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 diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift index 5d84b983f..a482041c5 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift @@ -24,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 3f84fca7c..af5bb32e7 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -20,7 +20,7 @@ import SwiftJavaJNICore extension FFMSwift2JavaGenerator { package func printFunctionDowncallMethods( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, ) { guard let _ = translatedDecl(for: decl) else { // Failed to translate. Skip. @@ -40,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)! @@ -271,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) @@ -360,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 @@ -421,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 @@ -455,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+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 49e71d9af..9e39cefdc 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -74,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 }) @@ -105,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)") @@ -148,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() { @@ -157,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) { @@ -166,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( @@ -191,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)) } @@ -206,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 @@ -233,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, @@ -248,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) @@ -265,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 [] } @@ -279,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/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index 6cec38886..4173d26f7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -15,7 +15,7 @@ import SwiftExtract enum JNICaching { - static func cacheName(for type: ImportedNominalType) -> String { + static func cacheName(for type: ExtractedNominalType) -> String { cacheName(for: type.effectiveJavaTypeName) } @@ -23,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) } @@ -39,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/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index abdc1db2a..da4a6216f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -21,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). @@ -51,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 @@ -102,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) @@ -164,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) } @@ -181,8 +181,8 @@ extension JNISwift2JavaGenerator { } private func translateVariable( - getter: ImportedFunc, - setter: ImportedFunc? + getter: ExtractedFunc, + setter: ExtractedFunc? ) throws -> JavaInterfaceSwiftWrapper.Variable { try JavaInterfaceSwiftWrapper.Variable( swiftDecl: getter.swiftDecl, // they should be the same diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 90aaafc38..6a3ad1e5f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -43,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 }) } @@ -106,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 @@ -140,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() @@ -154,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) @@ -171,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 @@ -207,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( """ @@ -245,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 } @@ -399,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 { @@ -442,7 +442,7 @@ extension JNISwift2JavaGenerator { private func printNominal( _ printer: inout CodePrinter, - _ decl: ImportedNominalType, + _ decl: ExtractedNominalType, body: (inout CodePrinter) -> Void, ) { if decl.swiftNominal.isSendable { @@ -474,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) @@ -484,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 } @@ -501,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 } @@ -558,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)'") @@ -570,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) } @@ -626,7 +626,7 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncallMethods( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, skipMethodBody: Bool = false, ) { guard translatedDecl(for: decl) != nil else { @@ -650,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 { @@ -687,7 +687,7 @@ extension JNISwift2JavaGenerator { private func printNecessarySupportTypes( _ printer: inout CodePrinter, - _ decl: ImportedFunc + _ decl: ExtractedFunc ) { let translatedDecl = translatedDecl(for: decl)! @@ -698,7 +698,7 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, - _ decl: ImportedFunc, + _ decl: ExtractedFunc, skipMethodBody: Bool, ) { guard let translatedDecl = translatedDecl(for: decl) else { @@ -710,7 +710,7 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, - importedFunc: ImportedFunc? = nil, + importedFunc: ExtractedFunc? = nil, skipMethodBody: Bool, ) { var modifiers = ["public"] @@ -879,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") @@ -897,7 +897,7 @@ extension JNISwift2JavaGenerator { } } - private func printFoundationDateHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + private func printFoundationDateHelpers(_ printer: inout CodePrinter, _ decl: ExtractedNominalType) { printer.print( """ /** @@ -950,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 f69ea1918..3012fba79 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -30,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 @@ -54,7 +54,7 @@ extension JNISwift2JavaGenerator { } func translatedEnumCase( - for decl: ImportedEnumCase + for decl: ExtractedEnumCase ) -> TranslatedEnumCase? { if let cached = translatedEnumCases[decl] { return cached @@ -72,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 { @@ -91,12 +91,12 @@ extension JNISwift2JavaGenerator { let javaClassLookupTable: JavaClassLookupTable let moduleJavaPackages: ModuleJavaPackages var knownTypes: SwiftKnownTypes - let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger var javaIdentifiers: JavaIdentifierFactory - let importedTypes: [String: ImportedNominalType] + let extractedTypes: [String: ExtractedNominalType] - func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { + func translate(enumCase: ExtractedEnumCase) throws -> TranslatedEnumCase { let methodName = "" // TODO: Used for closures, replace with better name? let parameterResults = try enumCase.parameters.enumerated().map { idx, parameter in @@ -123,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)", @@ -152,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, @@ -1579,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 66f89459f..1009f4c79 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -24,7 +24,7 @@ extension JNISwift2JavaGenerator { let javaPackage: String let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes - let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger /// Translates a Swift function into the native JNI method signature. diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index dac2f44aa..21063d3dc 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -79,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)") @@ -292,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() @@ -322,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 @@ -371,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 } @@ -379,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 } @@ -395,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 } @@ -418,7 +418,7 @@ extension JNISwift2JavaGenerator { private func printEnumGetAsCaseThunk( _ printer: inout CodePrinter, - _ enumType: ImportedNominalType, + _ enumType: ExtractedNominalType, _ enumCase: TranslatedEnumCase, ) { if let getAsCaseFunction = enumCase.getAsCaseFunction { @@ -443,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. @@ -466,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( @@ -499,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 @@ -545,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 }) @@ -557,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)") @@ -783,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 @@ -820,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 @@ -881,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 { @@ -905,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 { @@ -919,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 @@ -965,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)") } @@ -1005,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 @@ -1128,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 134c829ee..564a54c7f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -50,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() @@ -121,7 +121,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { // 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)) } } @@ -143,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/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift index dbaf85cdd..823246305 100644 --- a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -23,15 +23,15 @@ 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 { @@ -63,7 +63,7 @@ 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! @@ -77,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/SwiftKit+Printing.swift b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift index 4e0de0e68..881d33899 100644 --- a/Sources/JExtractSwiftLib/SwiftKit+Printing.swift +++ b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift @@ -23,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)") """ @@ -53,78 +53,8 @@ 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)" } } - -// ==== ----------------------------------------------------------------------- -// MARK: SwiftSymbolTable printing helpers - -extension SwiftSymbolTable { - package func printImportedModules(_ printer: inout CodePrinter) { - let mainSymbolSourceModules = Set( - self.importedModules.values.filter { $0.alternativeModules?.isMainSourceOfSymbols ?? false }.map(\.moduleName) - ) - - for module in self.importedModules.keys.sorted() { - guard module != "Swift" else { - continue - } - - // Synthetic stub modules (e.g. ) exist purely for - // symbol-table resolution; they are not real Swift modules and must - // not be emitted as `import` statements. - guard !self.syntheticImportedModuleNames.contains(module) else { - continue - } - - guard let alternativeModules = self.importedModules[module]?.alternativeModules else { - printer.print("import \(module)") - continue - } - - // Only the main source of symbols emits the conditional import block. - // Secondary modules (e.g. FoundationEssentials when Foundation is the main source) - // are skipped when their main source is already present, because the main source's - // block already covers the import. If no main source is present, fall back to a - // plain import so the module is still imported. - guard alternativeModules.isMainSourceOfSymbols else { - if mainSymbolSourceModules.isDisjoint(with: alternativeModules.moduleNames) { - printer.print("import \(module)") - } - continue - } - - var importGroups: [String: [String]] = [:] - for name in alternativeModules.moduleNames { - guard let otherModule = self.importedModules[name] else { continue } - - let groupKey = otherModule.requiredAvailablityOfModuleWithName ?? otherModule.moduleName - importGroups[groupKey, default: []].append(otherModule.moduleName) - } - - for (index, group) in importGroups.keys.sorted().enumerated() { - if index > 0 && importGroups.keys.count > 1 { - printer.print("#elseif canImport(\(group))") - } else { - printer.print("#if canImport(\(group))") - } - - for groupModule in importGroups[group] ?? [] { - printer.print("import \(groupModule)") - } - } - - if importGroups.keys.isEmpty { - printer.print("import \(module)") - } else { - printer.print("#else") - printer.print("import \(module)") - printer.print("#endif") - } - } - printer.println() - } -} diff --git a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift index 9b99aa5e7..5beb710e3 100644 --- a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift +++ b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift @@ -19,13 +19,13 @@ import SwiftExtract 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 664061973..a6b01e08b 100644 --- a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift +++ b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift @@ -19,7 +19,7 @@ import SwiftSyntax enum TranslatedDocumentation { static func printDocumentation( - importedFunc: ImportedFunc, + importedFunc: ExtractedFunc, translatedDecl: FFMSwift2JavaGenerator.TranslatedFunctionDecl, config: Configuration, in printer: inout CodePrinter @@ -39,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 index dedfbfa4a..6c6f0858e 100644 --- a/Sources/SwiftExtract/AnalysisResult.swift +++ b/Sources/SwiftExtract/AnalysisResult.swift @@ -13,17 +13,17 @@ //===----------------------------------------------------------------------===// public struct AnalysisResult { - public let importedTypes: [String: ImportedNominalType] - public let importedGlobalVariables: [ImportedFunc] - public let importedGlobalFuncs: [ImportedFunc] + public let extractedTypes: [String: ExtractedNominalType] + public let extractedGlobalVariables: [ExtractedFunc] + public let extractedGlobalFuncs: [ExtractedFunc] public init( - importedTypes: [String: ImportedNominalType], - importedGlobalVariables: [ImportedFunc], - importedGlobalFuncs: [ImportedFunc] + extractedTypes: [String: ExtractedNominalType], + extractedGlobalVariables: [ExtractedFunc], + extractedGlobalFuncs: [ExtractedFunc] ) { - self.importedTypes = importedTypes - self.importedGlobalVariables = importedGlobalVariables - self.importedGlobalFuncs = importedGlobalFuncs + self.extractedTypes = extractedTypes + self.extractedGlobalVariables = extractedGlobalVariables + self.extractedGlobalFuncs = extractedGlobalFuncs } } diff --git a/Sources/SwiftExtract/ImportedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift similarity index 87% rename from Sources/SwiftExtract/ImportedDecls.swift rename to Sources/SwiftExtract/ExtractedDecls.swift index 81f622cdb..c6a322188 100644 --- a/Sources/SwiftExtract/ImportedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -28,17 +28,17 @@ public 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. -public final class ImportedNominalType: ExtractedSwiftDecl { +public final class ExtractedNominalType: ExtractedSwiftDecl { public let swiftNominal: SwiftNominalTypeDeclaration /// If this type is a specialization (FishTank), it points at the Tank base type of the specialization - public let specializationBaseType: ImportedNominalType? + 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`. @@ -47,10 +47,10 @@ public final class ImportedNominalType: ExtractedSwiftDecl { } // Backing storage for member collections - public var initializers: [ImportedFunc] = [] - public var methods: [ImportedFunc] = [] - public var variables: [ImportedFunc] = [] - public var cases: [ImportedEnumCase] = [] + public var initializers: [ExtractedFunc] = [] + public var methods: [ExtractedFunc] = [] + public var variables: [ExtractedFunc] = [] + public var cases: [ExtractedEnumCase] = [] public var inheritedTypes: [SwiftType] public var parent: SwiftNominalTypeDeclaration? @@ -89,7 +89,7 @@ public final class ImportedNominalType: ExtractedSwiftDecl { } /// 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 @@ -166,7 +166,7 @@ public final class ImportedNominalType: ExtractedSwiftDecl { 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)'" @@ -178,7 +178,7 @@ public final class ImportedNominalType: ExtractedSwiftDecl { message: "Missing type arguments for: \(missingParams) when specializing \(baseTypeName) as \(specializedName)" ) } - return ImportedNominalType( + return ExtractedNominalType( base: self, specializedTypeName: specializedName, genericArguments: substitutions, @@ -186,14 +186,14 @@ public final class ImportedNominalType: ExtractedSwiftDecl { } /// Checks if this type, or any of types it inherits from, conforms to the passed in protocol. - public func conformsTo(_ protocolName: String, in importedTypes: [String: ImportedNominalType]) -> Bool { + public func conformsTo(_ protocolName: String, in extractedTypes: [String: ExtractedNominalType]) -> Bool { var visited: Set = [] - var queue: [ImportedNominalType] = [self] + var queue: [ExtractedNominalType] = [self] while let current = queue.popLast() { for inherited in current.inheritedTypes { guard let name = inherited.asNominalTypeDeclaration?.name else { continue } if name == protocolName { return true } - if let next = importedTypes[name], visited.insert(ObjectIdentifier(next)).inserted { + if let next = extractedTypes[name], visited.insert(ObjectIdentifier(next)).inserted { queue.append(next) } } @@ -206,7 +206,7 @@ public struct SpecializationError: Error { public let message: String } -public final class ImportedEnumCase: ExtractedSwiftDecl, CustomStringConvertible { +public final class ExtractedEnumCase: ExtractedSwiftDecl, CustomStringConvertible { /// The case name public let name: String @@ -218,14 +218,14 @@ public final class ImportedEnumCase: ExtractedSwiftDecl, CustomStringConvertible public let enumType: SwiftNominalType /// A function that represents the Swift static "initializer" for cases - public let caseFunction: ImportedFunc + public let caseFunction: ExtractedFunc public init( name: String, parameters: [SwiftEnumCaseParameter], swiftDecl: any DeclSyntaxProtocol, enumType: SwiftNominalType, - caseFunction: ImportedFunc, + caseFunction: ExtractedFunc, ) { self.name = name self.parameters = parameters @@ -236,7 +236,7 @@ public final class ImportedEnumCase: ExtractedSwiftDecl, CustomStringConvertible public var description: String { """ - ImportedEnumCase { + ExtractedEnumCase { name: \(name), parameters: \(parameters), swiftDecl: \(swiftDecl), @@ -246,8 +246,8 @@ public final class ImportedEnumCase: ExtractedSwiftDecl, CustomStringConvertible """ } - public func clone(for parent: SwiftType) -> ImportedEnumCase { - ImportedEnumCase( + public func clone(for parent: SwiftType) -> ExtractedEnumCase { + ExtractedEnumCase( name: name, parameters: parameters, swiftDecl: swiftDecl, @@ -257,16 +257,16 @@ public final class ImportedEnumCase: ExtractedSwiftDecl, 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: ExtractedSwiftDecl, 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 @@ -353,7 +353,7 @@ public final class ImportedFunc: ExtractedSwiftDecl, CustomStringConvertible { public var description: String { """ - ImportedFunc { + ExtractedFunc { apiKind: \(apiKind) module: \(module) name: \(name) @@ -362,11 +362,11 @@ public final class ImportedFunc: ExtractedSwiftDecl, CustomStringConvertible { """ } - public 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, @@ -376,20 +376,20 @@ public final class ImportedFunc: ExtractedSwiftDecl, 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 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/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index 84458800f..81b311757 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -48,7 +48,7 @@ final class SwiftAnalysisVisitor { } } - 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) @@ -87,24 +87,24 @@ final class SwiftAnalysisVisitor { func visit( nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String, ) { - guard let importedNominalType = translator.importedNominalType(node, parent: parent) else { + guard let extractedNominalType = translator.extractedNominalType(node, parent: parent) else { return } // Check if there's a specialization entry for this type - applySpecialization(to: importedNominalType) + applySpecialization(to: extractedNominalType) for memberItem in node.memberBlock.members { - self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) + self.visit(decl: memberItem.decl, in: extractedNominalType, sourceFilePath: sourceFilePath) } } func visit( enumDecl node: EnumDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String, ) { self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) @@ -114,14 +114,14 @@ final class SwiftAnalysisVisitor { func visit( extensionDecl node: ExtensionDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String, ) { guard parent == nil else { // 'extension' in a nominal type is invalid. Ignore return } - guard let importedNominalType = translator.importedNominalType(node.extendedType) else { + guard let extractedNominalType = translator.extractedNominalType(node.extendedType) else { return } @@ -134,12 +134,12 @@ final class SwiftAnalysisVisitor { guard !constraints.isEmpty else { // The extension is unconstrained: add to the base type (visible through all specializations) - importedNominalType.inheritedTypes += + extractedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap { try? SwiftType($0.type, lookupContext: translator.lookupContext) } ?? [] for memberItem in node.memberBlock.members { - self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) + self.visit(decl: memberItem.decl, in: extractedNominalType, sourceFilePath: sourceFilePath) } return } @@ -156,7 +156,7 @@ final class SwiftAnalysisVisitor { } let matchingSpecializations = findMatchingSpecializations( - extendedType: importedNominalType, + extendedType: extractedNominalType, whereConstraints: constraints, ) if matchingSpecializations.isEmpty { @@ -177,7 +177,7 @@ final class SwiftAnalysisVisitor { func visit( functionDecl node: FunctionDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { @@ -210,7 +210,7 @@ final class SwiftAnalysisVisitor { return } - let imported = ImportedFunc( + let imported = ExtractedFunc( module: translator.swiftModuleName, swiftDecl: node, name: node.name.text.unescapedSwiftName, @@ -222,13 +222,13 @@ final class SwiftAnalysisVisitor { if let typeContext { typeContext.methods.append(imported) } else { - translator.importedGlobalFuncs.append(imported) + translator.extractedGlobalFuncs.append(imported) } } func visit( enumCaseDecl node: EnumCaseDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, ) { guard let typeContext else { self.log.info("Enum case must be within a current type; \(node)") @@ -250,7 +250,7 @@ final class SwiftAnalysisVisitor { ) let caseName = caseElement.name.text.unescapedSwiftName - let caseFunction = ImportedFunc( + let caseFunction = ExtractedFunc( module: translator.swiftModuleName, swiftDecl: node, name: caseName, @@ -258,7 +258,7 @@ final class SwiftAnalysisVisitor { functionSignature: signature, ) - let importedCase = ImportedEnumCase( + let importedCase = ExtractedEnumCase( name: caseName, parameters: parameters ?? [], swiftDecl: node, @@ -279,7 +279,7 @@ final class SwiftAnalysisVisitor { func visit( variableDecl node: VariableDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { @@ -323,7 +323,7 @@ final class SwiftAnalysisVisitor { 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)") @@ -355,7 +355,7 @@ final class SwiftAnalysisVisitor { ) return } - let imported = ImportedFunc( + let imported = ExtractedFunc( module: translator.swiftModuleName, swiftDecl: node, name: "init", @@ -368,7 +368,7 @@ final class SwiftAnalysisVisitor { private func visit( subscriptDecl node: SubscriptDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, ) { guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { return @@ -409,7 +409,7 @@ final class SwiftAnalysisVisitor { private func visit( ifConfigDecl node: IfConfigDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, sourceFilePath: String ) { let (clause, _) = node.activeClause(in: translator.buildConfig) @@ -433,7 +433,7 @@ final class SwiftAnalysisVisitor { private func importAccessor( from node: DeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, kind: SwiftAPIKind, name: String, ) throws { @@ -459,7 +459,7 @@ final class SwiftAnalysisVisitor { return } - let imported = ImportedFunc( + let imported = ExtractedFunc( module: translator.swiftModuleName, swiftDecl: node, name: name, @@ -473,15 +473,15 @@ final class SwiftAnalysisVisitor { if let typeContext { typeContext.variables.append(imported) } else { - translator.importedGlobalVariables.append(imported) + translator.extractedGlobalVariables.append(imported) } } private func synthesizeRawRepresentableConformance( enumDecl node: EnumDeclSyntax, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, ) { - guard let imported = translator.importedNominalType(node, parent: parent) else { + guard let imported = translator.extractedNominalType(node, parent: parent) else { return } @@ -515,7 +515,7 @@ final class SwiftAnalysisVisitor { func visit( typeAliasDecl node: TypeAliasDeclSyntax, - in typeContext: ImportedNominalType?, + in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { let outputName = node.name.text @@ -534,7 +534,7 @@ final class SwiftAnalysisVisitor { guard !genericArgs.isEmpty else { return } // Resolve the base type through the symbol table - guard let baseType = translator.importedNominalType(rhsType) else { + guard let baseType = translator.extractedNominalType(rhsType) else { log.debug("Could not resolve base type for specialization: \(rhsType.trimmedDescription)") return } @@ -550,7 +550,7 @@ final class SwiftAnalysisVisitor { /// Register a specialization from a typealias that specializes a generic type private func registerSpecialization( outputName: String, - baseType: ImportedNominalType, + baseType: ExtractedNominalType, genericArgs: [String], rhsDescription: String, ) { @@ -565,7 +565,7 @@ final class SwiftAnalysisVisitor { } } - let specialized: ImportedNominalType + let specialized: ExtractedNominalType do { specialized = try baseType.specialize(as: outputName, with: substitutions) } catch { @@ -580,13 +580,13 @@ final class SwiftAnalysisVisitor { // MARK: Specialization support /// Apply specializations to a type if matching entries exist - func applySpecialization(to importedType: ImportedNominalType) { + func applySpecialization(to importedType: ExtractedNominalType) { guard let specializations = translator.specializations[importedType] else { return } for specialized in specializations { - translator.importedTypes[specialized.effectiveOutputName] = specialized + translator.extractedTypes[specialized.effectiveOutputName] = specialized log.info("Applied specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } @@ -596,17 +596,17 @@ final class SwiftAnalysisVisitor { func applyPendingSpecializations() { for (_, specializations) in translator.specializations { for specialized in specializations { - if translator.importedTypes[specialized.effectiveOutputName] != nil { + if translator.extractedTypes[specialized.effectiveOutputName] != nil { continue } - translator.importedTypes[specialized.effectiveOutputName] = specialized + translator.extractedTypes[specialized.effectiveOutputName] = specialized log.info("Applied pending specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } // Process constrained extensions that were deferred for deferred in deferredConstrainedExtensions { - guard let baseType = translator.importedNominalType(deferred.node.extendedType) else { + guard let baseType = translator.extractedNominalType(deferred.node.extendedType) else { continue } let matchingSpecializations = findMatchingSpecializations( @@ -668,9 +668,9 @@ final class SwiftAnalysisVisitor { /// Find specializations whose type args match the given where-clause constraints private func findMatchingSpecializations( - extendedType: ImportedNominalType, + extendedType: ExtractedNominalType, whereConstraints: [ParsedWhereConstraint], - ) -> [ImportedNominalType] { + ) -> [ExtractedNominalType] { guard let specializations = translator.specializations[extendedType] else { return [] } @@ -683,7 +683,7 @@ final class SwiftAnalysisVisitor { /// Where-clauses are conjunctive: every constraint must hold. private func constraintsMatchSpecialization( _ constraints: [ParsedWhereConstraint], - specialized: ImportedNominalType, + specialized: ExtractedNominalType, ) -> Bool { for constraint in constraints { switch constraint { @@ -696,10 +696,10 @@ final class SwiftAnalysisVisitor { guard let concreteName = specialized.genericArguments[typeParam] else { return false } - guard let concreteType = translator.importedTypes[concreteName] else { + guard let concreteType = translator.extractedTypes[concreteName] else { return false } - guard concreteType.conformsTo(proto, in: translator.importedTypes) else { + guard concreteType.conformsTo(proto, in: translator.extractedTypes) else { return false } } @@ -722,7 +722,7 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn func shouldExtract( config: Configuration, log: Logger, - in parent: ImportedNominalType?, + in parent: ExtractedNominalType?, decider: (any ExtractDecider)? ) -> Bool { let accessLevelPasses: Bool = diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 6a4818d34..909468d53 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -54,16 +54,16 @@ public final class SwiftAnalyzer { // ==== Output state - package var importedGlobalVariables: [ImportedFunc] = [] + package var extractedGlobalVariables: [ExtractedFunc] = [] - package var importedGlobalFuncs: [ImportedFunc] = [] + package var extractedGlobalFuncs: [ExtractedFunc] = [] /// A mapping from Swift type names (e.g., A.B) over to the imported nominal /// type representation. - package var importedTypes: [String: ImportedNominalType] = [:] + package var extractedTypes: [String: ExtractedNominalType] = [:] /// Specializations of generic types that will get their concrete Java declarations, "as if" they were independent types - package var specializations: [ImportedNominalType: Set] = [:] + package var specializations: [ExtractedNominalType: Set] = [:] package var lookupContext: SwiftTypeLookupContext! = nil @@ -109,9 +109,9 @@ extension SwiftAnalyzer { /// Snapshot of the analysis state as a value-typed `AnalysisResult`. public var result: AnalysisResult { AnalysisResult( - importedTypes: self.importedTypes, - importedGlobalVariables: self.importedGlobalVariables, - importedGlobalFuncs: self.importedGlobalFuncs, + extractedTypes: self.extractedTypes, + extractedGlobalVariables: self.extractedGlobalVariables, + extractedGlobalFuncs: self.extractedGlobalFuncs, ) } @@ -267,7 +267,7 @@ extension SwiftAnalyzer { } } - func check(_ fn: ImportedFunc) -> Bool { + func check(_ fn: ExtractedFunc) -> Bool { if check(fn.functionSignature.result.type) { return true } @@ -277,13 +277,13 @@ extension SwiftAnalyzer { return false } - if self.importedGlobalFuncs.contains(where: check) { + if self.extractedGlobalFuncs.contains(where: check) { return true } - if self.importedGlobalVariables.contains(where: check) { + if self.extractedGlobalVariables.contains(where: check) { return true } - for importedType in self.importedTypes.values { + for importedType in self.extractedTypes.values { if importedType.initializers.contains(where: check) { return true } @@ -302,10 +302,10 @@ extension SwiftAnalyzer { // MARK: Type translation extension SwiftAnalyzer { /// Try to resolve the given nominal declaration node into its imported representation. - func importedNominalType( + func extractedNominalType( _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, - parent: ImportedNominalType?, - ) -> ImportedNominalType? { + parent: ExtractedNominalType?, + ) -> ExtractedNominalType? { if !nominalNode.shouldExtract(config: config, log: log, in: parent, decider: extractDecider) { return nil } @@ -313,13 +313,13 @@ extension SwiftAnalyzer { guard let nominal = symbolTable.lookupType(nominalNode.name.text, parent: parent?.swiftNominal) else { return nil } - return self.importedNominalType(nominal) + return self.extractedNominalType(nominal) } /// Try to resolve the given nominal type node into its imported representation. - func importedNominalType( + func extractedNominalType( _ typeNode: TypeSyntax - ) -> ImportedNominalType? { + ) -> ExtractedNominalType? { guard let swiftType = try? SwiftType(typeNode, lookupContext: lookupContext) else { return nil } @@ -334,14 +334,14 @@ extension SwiftAnalyzer { return nil } - guard swiftNominalDecl.syntax.shouldExtract(config: config, log: log, in: nil as ImportedNominalType?, decider: extractDecider) else { + guard swiftNominalDecl.syntax.shouldExtract(config: config, log: log, in: nil as ExtractedNominalType?, decider: extractDecider) else { return nil } - return importedNominalType(swiftNominalDecl) + return extractedNominalType(swiftNominalDecl) } - func importedNominalType(_ nominal: SwiftNominalTypeDeclaration) -> ImportedNominalType? { + func extractedNominalType(_ nominal: SwiftNominalTypeDeclaration) -> ExtractedNominalType? { let fullName = nominal.qualifiedName guard shouldExtractSwiftType(qualifiedName: fullName, config: config) else { @@ -349,13 +349,13 @@ extension SwiftAnalyzer { return nil } - if let alreadyImported = importedTypes[fullName] { + if let alreadyImported = extractedTypes[fullName] { return alreadyImported } - let importedNominal = try? ImportedNominalType(swiftNominal: nominal, lookupContext: lookupContext) + let importedNominal = try? ExtractedNominalType(swiftNominal: nominal, lookupContext: lookupContext) - importedTypes[fullName] = importedNominal + extractedTypes[fullName] = importedNominal return importedNominal } } diff --git a/Sources/SwiftExtract/SwiftTypes/ImportedSwiftModule.swift b/Sources/SwiftExtract/SwiftTypes/ExtractedSwiftModule.swift similarity index 96% rename from Sources/SwiftExtract/SwiftTypes/ImportedSwiftModule.swift rename to Sources/SwiftExtract/SwiftTypes/ExtractedSwiftModule.swift index 5c22b2654..f8c1a6d6f 100644 --- a/Sources/SwiftExtract/SwiftTypes/ImportedSwiftModule.swift +++ b/Sources/SwiftExtract/SwiftTypes/ExtractedSwiftModule.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -public struct ImportedSwiftModule: Hashable { +public struct ExtractedSwiftModule: Hashable { public let name: String public let availableWithModuleName: String? public var alternativeModuleNames: Set diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift b/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift index e24c2eea7..ed20722e6 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftDependencyScanner.swift @@ -15,15 +15,15 @@ import SwiftSyntax /// Scan importing modules. -package 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 @@ package func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftMod 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/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift index e8fb51020..5e38f43d6 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift @@ -108,7 +108,7 @@ extension 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) diff --git a/Sources/SwiftJavaToolLib/JavaTranslator.swift b/Sources/SwiftJavaToolLib/JavaTranslator.swift index 46682bae3..354d37cf5 100644 --- a/Sources/SwiftJavaToolLib/JavaTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator.swift @@ -60,7 +60,7 @@ package class JavaTranslator { /// The set of Swift modules that need to be imported to make the generated /// code compile. Use `getImportDecls()` to format this into a list of /// import declarations. - package var importedSwiftModules: Set = JavaTranslator.defaultImportedSwiftModules + package var importedSwiftModules: Set = JavaTranslator.defaultExtractedSwiftModules /// The canonical names of Java classes whose declared 'native' /// methods will be implemented in Swift. @@ -97,7 +97,7 @@ package class JavaTranslator { /// Clear out any per-file state when we want to start a new file. package func startNewFile() { - importedSwiftModules = Self.defaultImportedSwiftModules + importedSwiftModules = Self.defaultExtractedSwiftModules } /// Simplistic logging for all entities that couldn't be translated. @@ -111,7 +111,7 @@ extension JavaTranslator { private static let defaultFormat = BasicFormat(indentationWidth: .spaces(2)) /// Default set of modules that will always be imported. - private static let defaultImportedSwiftModules: Set = [ + private static let defaultExtractedSwiftModules: Set = [ "SwiftJava", "SwiftJavaJNICore", ] diff --git a/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift index 4c4050cfc..f8b17c893 100644 --- a/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift +++ b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift @@ -47,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 8ff99fd82..32be4c646 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -47,7 +47,7 @@ final class FuncCallbackImportTests { 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, @@ -136,7 +136,7 @@ final class FuncCallbackImportTests { 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, @@ -252,7 +252,7 @@ final class FuncCallbackImportTests { 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 1733bab9e..6cdacab12 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -243,7 +243,7 @@ extension FunctionDescriptorTests { try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) - let funcDecl = st.importedGlobalFuncs.first { + let funcDecl = st.extractedGlobalFuncs.first { $0.name == methodIdentifier }! @@ -285,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 643a09042..077db64ce 100644 --- a/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift +++ b/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift @@ -344,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") @@ -382,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") @@ -393,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") @@ -404,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/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 8e9bc917b..fce7ed3a7 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -85,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) @@ -120,7 +120,7 @@ final class MethodImportTests { try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "globalTakeInt" } ) @@ -169,7 +169,7 @@ final class MethodImportTests { try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "globalTakeIntLongString" } ) @@ -215,7 +215,7 @@ final class MethodImportTests { try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "globalReturnClass" } ) @@ -261,7 +261,7 @@ final class MethodImportTests { try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = try #require( - st.importedGlobalFuncs.first { + st.extractedGlobalFuncs.first { $0.name == "swapRawBufferPointer" } ) @@ -309,8 +309,8 @@ final class MethodImportTests { 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" } ) @@ -354,8 +354,8 @@ final class MethodImportTests { 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" } ) @@ -405,8 +405,8 @@ final class MethodImportTests { 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" } ) @@ -460,8 +460,8 @@ final class MethodImportTests { 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" } ) @@ -515,7 +515,7 @@ final class MethodImportTests { 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 876342941..ef4797713 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -70,9 +70,9 @@ 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" @@ -80,19 +80,19 @@ struct SpecializationTests { 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) @@ -118,12 +118,12 @@ struct SpecializationTests { // Both wrappers delegate to the same base type #expect(fishBox.specializationBaseType === toolBox.specializationBaseType, "Both should wrap the same base Box type") - #expect(fishBox.specializationBaseType === translator.importedTypes["Box"], "Base should be the original Box") + #expect(fishBox.specializationBaseType === translator.extractedTypes["Box"], "Base should be the original Box") // Both wrappers have owned method models - let baseCountFunc: ImportedFunc = try #require(baseBox.methods.first(where: { $0.name == "count" })) - let fishCountFunc: ImportedFunc = try #require(fishBox.methods.first(where: { $0.name == "count" })) - let toolCountFunc: ImportedFunc = try #require(toolBox.methods.first(where: { $0.name == "count" })) + let baseCountFunc: ExtractedFunc = try #require(baseBox.methods.first(where: { $0.name == "count" })) + let fishCountFunc: ExtractedFunc = try #require(fishBox.methods.first(where: { $0.name == "count" })) + let toolCountFunc: ExtractedFunc = try #require(toolBox.methods.first(where: { $0.name == "count" })) #expect(baseCountFunc.parentType?.description == "Box") #expect(fishCountFunc.parentType?.description == "FishBox") #expect(toolCountFunc.parentType?.description == "ToolBox") @@ -136,7 +136,7 @@ struct SpecializationTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) - let baseBox = try #require(translator.importedTypes["Box"]) + let baseBox = try #require(translator.extractedTypes["Box"]) let specializations = try #require(translator.specializations[baseBox]) #expect(specializations.count == 2, "Should have exactly 2 specializations for Box") @@ -195,7 +195,7 @@ struct SpecializationTests { config.swiftModule = "SwiftModule" let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) - let toolBox = try #require(translator.importedTypes["ToolBox"]) + let toolBox = try #require(translator.extractedTypes["ToolBox"]) let methodNames = toolBox.methods.map(\.name) #expect(!methodNames.contains("observeTheFish"), "ToolBox should not have Fish-constrained method") } @@ -348,7 +348,7 @@ struct SpecializationTests { """, ) - let fish = try #require(translator.importedTypes["Fish"]) + let fish = try #require(translator.extractedTypes["Fish"]) #expect(!fish.swiftNominal.isGeneric) #expect(throws: SpecializationError.self) { diff --git a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift index dff3ed40f..a849b2474 100644 --- a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift +++ b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift @@ -52,7 +52,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) - let user = try #require(translator.importedTypes["TypealiasUser"]) + let user = try #require(translator.extractedTypes["TypealiasUser"]) #expect(user.variables.contains { $0.name == "amount" }, "Property `amount: Amount` should be extracted") #expect(user.methods.contains { $0.name == "doubled" }, "Method `doubled() -> Amount` should be extracted") @@ -67,7 +67,7 @@ struct TypealiasResolutionTests { 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" ) } @@ -92,7 +92,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let holder = try #require(translator.importedTypes["Holder"]) + let holder = try #require(translator.extractedTypes["Holder"]) #expect(holder.variables.contains { $0.name == "value" }) #expect(!holder.initializers.isEmpty) } @@ -116,7 +116,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrapOrZero" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "unwrapOrZero" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) // The parameter type should be Optional (substituted), preserving @@ -147,7 +147,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "describe" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "describe" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { @@ -179,7 +179,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let holder = try #require(translator.importedTypes["Holder"]) + let holder = try #require(translator.extractedTypes["Holder"]) #expect(holder.variables.isEmpty, "Property `bad: OneArg` should be dropped (arity mismatch)") } @@ -206,7 +206,7 @@ struct TypealiasResolutionTests { // 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)") } @@ -263,7 +263,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "passA" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "passA" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) #expect( paramType.description == "Int64", @@ -288,7 +288,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "unwrap" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "unwrap" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { @@ -313,7 +313,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "first" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "first" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { @@ -347,7 +347,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "add" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "add" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) #expect( paramType.description == "Int64", @@ -379,7 +379,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let player = try #require(translator.importedTypes["Player"]) + let player = try #require(translator.extractedTypes["Player"]) let bump = try #require(player.methods.first { $0.name == "bump" }) let paramType = try #require(bump.functionSignature.parameters.first?.type) #expect(paramType.description == "Int64") @@ -404,7 +404,7 @@ struct TypealiasResolutionTests { let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) try translator.analyze(path: "/fake/Fake.swift", text: input) - let fn = try #require(translator.importedGlobalFuncs.first { $0.name == "openIntBag" }) + let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "openIntBag" }) let paramType = try #require(fn.functionSignature.parameters.first?.type) guard case .nominal(let nominal) = paramType else { Issue.record("Expected Optional nominal, got \(paramType)") diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index ac3123ba5..420da6ec2 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -47,18 +47,18 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - #expect(result.importedTypes["Tank"] != nil) - #expect(result.importedTypes["FishTank"] != nil) - #expect(result.importedTypes["Status"] != nil) + #expect(result.extractedTypes["Tank"] != nil) + #expect(result.extractedTypes["FishTank"] != nil) + #expect(result.extractedTypes["Status"] != nil) - let tank = try #require(result.importedTypes["Tank"]) + let tank = try #require(result.extractedTypes["Tank"]) #expect(tank.swiftNominal.kind == .struct) #expect(tank.swiftNominal.isGeneric) - let fishTank = try #require(result.importedTypes["FishTank"]) + let fishTank = try #require(result.extractedTypes["FishTank"]) #expect(fishTank.swiftNominal.kind == .class) - let status = try #require(result.importedTypes["Status"]) + let status = try #require(result.extractedTypes["Status"]) #expect(status.swiftNominal.kind == .enum) } @@ -82,7 +82,7 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - let fishTank = try #require(result.importedTypes["FishTank"]) + let fishTank = try #require(result.extractedTypes["FishTank"]) let methodNames = Set(fishTank.methods.map(\.name)) #expect(methodNames == ["feed", "count"]) #expect(fishTank.initializers.count == 1) @@ -107,7 +107,7 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - let fishTank = try #require(result.importedTypes["FishTank"]) + 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]) @@ -129,7 +129,7 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - let fishTank = try #require(result.importedTypes["FishTank"]) + let fishTank = try #require(result.extractedTypes["FishTank"]) let nameAccessors = fishTank.variables.filter { $0.name == "name" } let kinds = nameAccessors.map(\.apiKind) #expect(kinds == [.getter]) @@ -152,9 +152,9 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - let names = Set(result.importedGlobalFuncs.map(\.name)) + let names = Set(result.extractedGlobalFuncs.map(\.name)) #expect(names == ["feedAll", "mood"]) - #expect(result.importedTypes.isEmpty) + #expect(result.extractedTypes.isEmpty) } @Test func globalVariableProducesGetterSetterPair() throws { @@ -170,7 +170,7 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - let counterAccessors = result.importedGlobalVariables.filter { $0.name == "globalCounter" } + let counterAccessors = result.extractedGlobalVariables.filter { $0.name == "globalCounter" } let kinds = Set(counterAccessors.map(\.apiKind)) #expect(kinds == [.getter, .setter]) } @@ -194,7 +194,7 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - let byName = Dictionary(uniqueKeysWithValues: result.importedGlobalFuncs.map { ($0.name, $0) }) + let byName = Dictionary(uniqueKeysWithValues: result.extractedGlobalFuncs.map { ($0.name, $0) }) let plain = try #require(byName["plain"]) #expect(plain.functionSignature.effectSpecifiers.isEmpty) @@ -235,9 +235,9 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - #expect(result.importedTypes["Public"] != nil) - #expect(result.importedTypes["Internal"] == nil) - #expect(result.importedTypes["Private"] == nil) + #expect(result.extractedTypes["Public"] != nil) + #expect(result.extractedTypes["Internal"] == nil) + #expect(result.extractedTypes["Private"] == nil) } // ==== ----------------------------------------------------------------------- @@ -268,9 +268,9 @@ struct AnalysisResultSuite { config: config ) - #expect(result.importedTypes["Tank"] != nil) - #expect(result.importedTypes["SkipMe"] == nil) - #expect(result.importedTypes["SkipAlso"] == nil) + #expect(result.extractedTypes["Tank"] != nil) + #expect(result.extractedTypes["SkipMe"] == nil) + #expect(result.extractedTypes["SkipAlso"] == nil) } // ==== ----------------------------------------------------------------------- @@ -293,9 +293,9 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - // Both the generic base and its specialization land in importedTypes. - #expect(result.importedTypes["Tank"] != nil) - let fishTank = try #require(result.importedTypes["FishTank"]) + // 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) } @@ -310,8 +310,8 @@ struct AnalysisResultSuite { moduleName: "Empty" ) - #expect(result.importedTypes.isEmpty) - #expect(result.importedGlobalFuncs.isEmpty) - #expect(result.importedGlobalVariables.isEmpty) + #expect(result.extractedTypes.isEmpty) + #expect(result.extractedGlobalFuncs.isEmpty) + #expect(result.extractedGlobalVariables.isEmpty) } } From f8d39a0a6df1505641c60c59483b914bbc0f1def Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 1 Jun 2026 16:54:17 +0900 Subject: [PATCH 05/38] SwiftExtract: move typealiases --- .../JavaSourceDependencies.swift | 4 - .../SymbolTable+Printing.swift | 83 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 Sources/JExtractSwiftLib/SymbolTable+Printing.swift diff --git a/Sources/JExtractSwiftLib/JavaSourceDependencies.swift b/Sources/JExtractSwiftLib/JavaSourceDependencies.swift index 55fb17e6c..a9c64567f 100644 --- a/Sources/JExtractSwiftLib/JavaSourceDependencies.swift +++ b/Sources/JExtractSwiftLib/JavaSourceDependencies.swift @@ -24,10 +24,6 @@ import FoundationEssentials import Foundation #endif -package typealias JavaClassName = String -package typealias JavaFullyQualifiedClassName = String -package typealias JavaPackageName = String - extension SourceDependencies { /// Inject synthetic `@JavaClass public class {}` stubs so the symbol /// table can resolve Java wrapper types referenced in the Swift API. 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() + } +} From de8c8b5b3fd87ab256dd53ce5c87ed8759815c88 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 2 Jun 2026 11:29:28 +0900 Subject: [PATCH 06/38] SwiftExtract: decouple from Java config + optional operator extraction Introduce SwiftExtractConfiguration protocol (neutral AccessLevelMode / Logger.Level) so SwiftExtract no longer depends on SwiftJavaConfigurationShared; Configuration conforms via JExtractSwiftLib/Configuration+SwiftExtract.swift. Gate operator extraction behind config.extractsOperators (default off -> Java unchanged). Expose ExtractedNominalType.declAttributes / declGroupSyntax. All SwiftExtractTests + JExtractSwiftTests pass. --- Package.swift | 1 - .../Configuration+SwiftExtract.swift | 61 ++++++++++ .../FFM/FFMSwift2JavaGenerator.swift | 2 +- .../JNI/JNISwift2JavaGenerator.swift | 4 +- Sources/SwiftExtract/ExtractedDecls.swift | 14 +++ Sources/SwiftExtract/Logger.swift | 15 ++- .../SwiftExtract/SwiftAnalysisVisitor.swift | 19 +-- Sources/SwiftExtract/SwiftAnalyzer.swift | 17 ++- .../SwiftExtractConfiguration.swift | 114 ++++++++++++++++++ Sources/SwiftExtract/SwiftFileFilter.swift | 6 +- .../SwiftTypes/SwiftSymbolTable.swift | 3 +- .../SwiftJavaBaseAsyncParsableCommand.swift | 2 +- .../AnalysisResultTests.swift | 3 +- 13 files changed, 230 insertions(+), 31 deletions(-) create mode 100644 Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift create mode 100644 Sources/SwiftExtract/SwiftExtractConfiguration.swift diff --git a/Package.swift b/Package.swift index f8f4b455f..617329217 100644 --- a/Package.swift +++ b/Package.swift @@ -351,7 +351,6 @@ let package = Package( .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "Logging", package: "swift-log"), - "SwiftJavaConfigurationShared", ], path: "Sources/SwiftExtract", resources: [ diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift new file mode 100644 index 000000000..eef586b89 --- /dev/null +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftExtract +import SwiftJavaConfigurationShared + +/// Bridges swift-java's `Configuration` onto the language-neutral +/// `SwiftExtractConfiguration` surface consumed by `SwiftExtract`. +/// +/// Most members are satisfied directly by `Configuration`'s own properties; only +/// the two enum-typed members need a mapping from swift-java's enums onto the +/// neutral `AccessLevelMode` / `Logger.Level`. +extension Configuration: SwiftExtractConfiguration { + public var swiftExtractAccessLevel: AccessLevelMode { + switch effectiveMinimumInputAccessLevelMode { + case .public: .public + case .package: .package + case .internal: .internal + } + } + + public var swiftExtractLogLevel: SwiftExtract.Logger.Level? { + guard let logLevel else { return nil } + switch logLevel { + case .trace: return .trace + case .debug: return .debug + case .info: return .info + case .notice: return .notice + case .warning: return .warning + case .error: return .error + case .critical: return .critical + } + } +} + +extension LogLevel { + /// Bridges from the analysis layer's neutral `Logger.Level` (used by the CLI's + /// `--log-level` option) onto swift-java's own `LogLevel`. + public init(_ level: SwiftExtract.Logger.Level) { + switch level { + case .trace: self = .trace + case .debug: self = .debug + case .info: self = .info + case .notice: self = .notice + case .warning: self = .warning + case .error: self = .error + case .critical: self = .critical + } + } +} diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 5d22e3baa..4a9f899ce 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -86,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 { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 564a54c7f..b0379e2f4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -86,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 { @@ -117,7 +117,7 @@ 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. diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index c6a322188..af5011124 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -151,6 +151,20 @@ public final class ExtractedNominalType: ExtractedSwiftDecl { self.swiftNominal.qualifiedName } + /// 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 { diff --git a/Sources/SwiftExtract/Logger.swift b/Sources/SwiftExtract/Logger.swift index aae0148af..5a89412db 100644 --- a/Sources/SwiftExtract/Logger.swift +++ b/Sources/SwiftExtract/Logger.swift @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import Foundation -import SwiftJavaConfigurationShared import SwiftSyntax // Placeholder for some better logger, we could depend on swift-log @@ -113,7 +112,19 @@ public struct Logger { } extension Logger { - public typealias Level = SwiftJavaConfigurationShared.LogLevel + /// Log verbosity levels for the analysis layer's lightweight logger. + /// + /// Language-neutral; language-specific configuration modules map their own + /// log-level enums onto this via `SwiftExtractConfiguration`. + public enum Level: String, Codable, Hashable, Sendable { + case trace + case debug + case info + case notice + case warning + case error + case critical + } } extension Logger.Level { diff --git a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index 81b311757..74bd6b2cf 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -15,13 +15,12 @@ import Foundation import Logging import SwiftIfConfig -import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax final class SwiftAnalysisVisitor { let translator: SwiftAnalyzer - var config: Configuration { + var config: any SwiftExtractConfiguration { self.translator.config } @@ -186,8 +185,14 @@ final class SwiftAnalysisVisitor { switch node.name.tokenKind { case .binaryOperator, .prefixOperator, .postfixOperator: - self.log.debug("Skip importing: '\(node.qualifiedNameForDebug)'; Operators are not supported.") - return + // Operators are extracted as ordinary `.function`s only when the target + // opts in (other language code generators may map them to language + // constructs in a post-pass). Most targets (e.g. Java) cannot express + // Swift operators and skip them. + guard config.extractsOperators else { + self.log.debug("Skip importing: '\(node.qualifiedNameForDebug)'; Operators are not supported.") + return + } default: break } @@ -720,13 +725,13 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn /// the result on a per-decl basis (e.g. Java honors `@JavaExport` / /// `@JavaClass` here) func shouldExtract( - config: Configuration, + config: any SwiftExtractConfiguration, log: Logger, in parent: ExtractedNominalType?, decider: (any ExtractDecider)? ) -> Bool { let accessLevelPasses: Bool = - switch config.effectiveMinimumInputAccessLevelMode { + switch config.swiftExtractAccessLevel { case .public: self.isPublic(in: parent?.swiftNominal.syntax) case .package: self.isAtLeastPackage case .internal: self.isAtLeastInternal @@ -744,7 +749,7 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn if !accessLevelPasses { log.debug( - "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)" + "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.swiftExtractAccessLevel)" ) } return accessLevelPasses diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 909468d53..dad00cd5e 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -15,7 +15,6 @@ import Foundation import Logging import SwiftIfConfig -import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -30,7 +29,7 @@ public final class SwiftAnalyzer { package var log: Logger - package let config: Configuration + package let config: any SwiftExtractConfiguration /// The build configuration used to resolve #if conditional compilation blocks. package let buildConfig: any BuildConfiguration @@ -76,13 +75,14 @@ public final class SwiftAnalyzer { package let extractDecider: (any ExtractDecider)? public init( - config: Configuration, + config: any SwiftExtractConfiguration, + moduleName: String? = nil, extractDecider: (any ExtractDecider)? = nil ) { - guard let swiftModule = config.swiftModule else { + guard let swiftModule = moduleName ?? config.swiftModule else { fatalError("Missing 'swiftModule' name.") // FIXME: can we make it required in config? but we shared config for many cases } - self.log = Logger(label: "translator", logLevel: config.logLevel ?? .info) + self.log = Logger(label: "translator", logLevel: config.swiftExtractLogLevel ?? .info) self.config = config self.swiftModuleName = swiftModule self.extractDecider = extractDecider @@ -150,13 +150,12 @@ extension SwiftAnalyzer { public static func analyze( sources: [(path: String, text: String)], moduleName: String, - config: Configuration? = nil, + config: (any SwiftExtractConfiguration)? = nil, sourceDependencies: SourceDependencies = SourceDependencies(), extractDecider: (any ExtractDecider)? = nil ) throws -> AnalysisResult { - var effectiveConfig = config ?? Configuration() - effectiveConfig.swiftModule = moduleName - let translator = SwiftAnalyzer(config: effectiveConfig, extractDecider: extractDecider) + let effectiveConfig = config ?? DefaultSwiftExtractConfiguration(swiftModule: moduleName) + let translator = SwiftAnalyzer(config: effectiveConfig, moduleName: moduleName, extractDecider: extractDecider) translator.sourceDependencies = sourceDependencies for source in sources { translator.add(filePath: source.path, text: source.text) diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift new file mode 100644 index 000000000..b2bb25fd0 --- /dev/null +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// 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. +/// +/// Language-neutral counterpart to a configuration's access-level setting. The +/// concrete `Configuration` types in language layers (e.g. swift-java, or other +/// language code generators) map their own enums onto this. +public enum AccessLevelMode: String, Sendable { + case `public` + case `package` + case `internal` +} + +/// 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`. +/// +/// The two enum-typed members use `swiftExtract`-prefixed names so a conforming +/// type can keep its own, differently-typed `logLevel` / +/// `effectiveMinimumInputAccessLevelMode` members without a name collision. +public protocol SwiftExtractConfiguration { + /// Name of the Swift module being analyzed. + var swiftModule: String? { get } + + /// Optional path to a JSON `StaticBuildConfiguration` used to resolve `#if`. + var staticBuildConfigurationFile: String? { get } + + /// Glob patterns selecting which Swift files/types to extract. + var swiftFilterInclude: [String]? { get } + + /// Glob patterns excluding Swift files/types from extraction. + var swiftFilterExclude: [String]? { get } + + /// Stub declarations for imported modules whose source is unavailable to the + /// analyzer. Keyed by module name; values are Swift declaration strings parsed + /// as if they belonged to that module. + var importedModuleStubs: [String: [String]]? { get } + + /// Minimum access level required for a declaration to be extracted. + var swiftExtractAccessLevel: AccessLevelMode { get } + + /// Whether operator declarations (e.g. `static func + (…)`) should be + /// extracted as ordinary `.function`s. Most targets (e.g. Java) cannot express + /// Swift operators and leave this `false`; other language code generators that + /// map operators to language constructs set it `true` and recognize the + /// operator functions in a post-analysis pass. + var extractsOperators: Bool { get } + + /// Verbosity for the analyzer's logger; `nil` falls back to `.info`. + var swiftExtractLogLevel: Logger.Level? { get } + + /// Whether the given module name has stub declarations configured. + func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool +} + +extension SwiftExtractConfiguration { + public var extractsOperators: 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 swiftExtractAccessLevel: AccessLevelMode + public var swiftExtractLogLevel: Logger.Level? + public var extractsOperators: Bool + + public init( + swiftModule: String? = nil, + accessLevel: AccessLevelMode = .public, + logLevel: Logger.Level? = nil, + extractsOperators: Bool = false, + staticBuildConfigurationFile: String? = nil, + swiftFilterInclude: [String]? = nil, + swiftFilterExclude: [String]? = nil, + importedModuleStubs: [String: [String]]? = nil + ) { + self.swiftModule = swiftModule + self.swiftExtractAccessLevel = accessLevel + self.swiftExtractLogLevel = logLevel + self.extractsOperators = extractsOperators + self.staticBuildConfigurationFile = staticBuildConfigurationFile + self.swiftFilterInclude = swiftFilterInclude + self.swiftFilterExclude = swiftFilterExclude + self.importedModuleStubs = importedModuleStubs + } +} diff --git a/Sources/SwiftExtract/SwiftFileFilter.swift b/Sources/SwiftExtract/SwiftFileFilter.swift index 40c6ef6b3..e0e0e7741 100644 --- a/Sources/SwiftExtract/SwiftFileFilter.swift +++ b/Sources/SwiftExtract/SwiftFileFilter.swift @@ -12,8 +12,6 @@ // //===----------------------------------------------------------------------===// -import SwiftJavaConfigurationShared - // ==== ----------------------------------------------------------------------- // MARK: Swift filter pattern classification @@ -195,7 +193,7 @@ package func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bo /// Only file-path patterns (containing `/`) and plain patterns (no `/` or `.`) /// are checked here. Type-name patterns are skipped — use /// `shouldExtractSwiftType` for those -package func shouldExtractSwiftFile(relativePath: String, config: Configuration) -> Bool { +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 @@ -246,7 +244,7 @@ private func matchesFilePattern(relativePath: String, pattern: String) -> Bool { /// Only type-name patterns (containing `.`) and plain patterns (no `/` or `.`) /// are checked here. File-path patterns are skipped — use `shouldExtractSwiftFile` /// for those -package func shouldExtractSwiftType(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/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift index 5e38f43d6..45557b7ac 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftSymbolTable.swift @@ -14,7 +14,6 @@ import Logging import SwiftIfConfig -import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax @@ -100,7 +99,7 @@ extension SwiftSymbolTable { moduleName: String, _ inputFiles: some Collection, additionalInputFiles: [SwiftInputFile] = [], - config: Configuration?, + config: (any SwiftExtractConfiguration)?, sourceDependencies: SourceDependencies, buildConfig: any BuildConfiguration = .swiftExtractDefault, log: Logger? = nil, diff --git a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift index 748114c30..95eaf7d0a 100644 --- a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -183,7 +183,7 @@ extension SwiftJavaBaseAsyncParsableCommand { config = Configuration() } // override configuration with options from command line - config.logLevel = command.logLevel + config.logLevel = LogLevel(command.logLevel) return config } } diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index 420da6ec2..ad9cbec7c 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import SwiftExtract -import SwiftJavaConfigurationShared import Testing /// End-to-end tests that drive the analysis pipeline (Swift source → @@ -244,7 +243,7 @@ struct AnalysisResultSuite { // MARK: Filter include/exclude @Test func swiftFilterExcludeSkipsMatchingTypes() throws { - var config = Configuration() + var config = DefaultSwiftExtractConfiguration() config.swiftFilterExclude = ["Skip*"] let result = try SwiftAnalyzer.analyze( From 0d21d114eb4b391fbab277fde572ccf056bcb08f Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 2 Jun 2026 13:48:37 +0900 Subject: [PATCH 07/38] SwiftExtract: include a few general-purpose `is...` checks --- .../SwiftTypes/SwiftFunctionSignature.swift | 5 +++ .../SwiftTypes/SwiftParameter.swift | 38 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 9a0261c14..477fb5f2e 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -38,6 +38,11 @@ public struct SwiftFunctionSignature: Equatable { effectSpecifiers.contains(.throws) } + /// Whether any parameter is variadic (`T...`). + public var hasVariadicParams: Bool { + parameters.contains(where: \.isVariadic) + } + public init( selfParameter: SwiftSelfParameter? = nil, parameters: [SwiftParameter], diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift index 90b694a42..71db093df 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift @@ -20,16 +20,44 @@ public struct SwiftParameter: Equatable { 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 + 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 } } @@ -75,6 +103,9 @@ extension SwiftParameter { 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 } } @@ -113,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 From a282d7bd365b0d4ce24617f5bcdc3bebb1be5d6d Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 3 Jun 2026 22:15:17 +0900 Subject: [PATCH 08/38] SwiftExtract: configurable importable modules + generic-type initializer extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two opt-in, language-neutral knobs on `SwiftExtractConfiguration` (both default to the prior behavior, so the Java path is unchanged): - `availableImportModules: Set` — module names treated as importable when resolving `#if canImport()`. The analyzer wraps its build configuration in an `ImportOverlayBuildConfiguration` so declarations guarded behind `#if canImport(MyModule)` are extracted (e.g. another language code generator may declare its runtime module importable here, even when the static build config doesn't otherwise know about it). - `extractsGenericTypeInitializers: Bool` — extract initializers of generic nominal types even when not specialized. swift-java skips these by default (an open generic isn't directly constructible); other language code generators that specialize generics in a post-analysis pass need the base type's initializers available to clone onto the specialization. --- .../SwiftExtract/SwiftAnalysisVisitor.swift | 4 +- Sources/SwiftExtract/SwiftAnalyzer.swift | 16 +++++++- .../SwiftExtractConfiguration.swift | 24 +++++++++++- ...wiftExtractDefaultBuildConfiguration.swift | 38 +++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index 74bd6b2cf..d7c0a43f1 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -338,7 +338,9 @@ final class SwiftAnalysisVisitor { return } - if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization { + if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization + && !config.extractsGenericTypeInitializers + { log.debug("Skip Importing generic type initializer \(node.kind) '\(node.qualifiedNameForDebug)'") return } diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index dad00cd5e..9c16013bb 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -91,15 +91,27 @@ public final class SwiftAnalyzer { do { let data = try Data(contentsOf: URL(fileURLWithPath: staticBuildConfigPath)) let decoder = JSONDecoder() - self.buildConfig = try decoder.decode(StaticBuildConfiguration.self, from: data) + let staticConfig = try decoder.decode(StaticBuildConfiguration.self, from: data) + self.buildConfig = SwiftAnalyzer.overlayingAvailableModules(staticConfig, config.availableImportModules) self.log.info("Using custom static build configuration from: \(staticBuildConfigPath)") } catch { fatalError("Failed to load static build configuration from '\(staticBuildConfigPath)': \(error)") } } else { - self.buildConfig = .swiftExtractDefault + 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) + } } // ===== -------------------------------------------------------------------------------------------------------------- diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index b2bb25fd0..090912599 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -67,6 +67,21 @@ public protocol SwiftExtractConfiguration { /// Verbosity for the analyzer's logger; `nil` falls back to `.info`. var swiftExtractLogLevel: Logger.Level? { get } + /// Whether to extract initializers of *generic* nominal types even when they + /// are not (yet) specialized. swift-java skips these by default (an open + /// generic isn't directly constructible); other language code generators that + /// specialize generics in a post-analysis pass set this `true` so the base + /// type's initializers are available to clone onto the specialization. + /// Default: false. + var extractsGenericTypeInitializers: Bool { get } + + /// Module names that should be treated as importable when resolving + /// `#if canImport()` conditions, in addition to whatever the build + /// configuration already knows. Lets a target opt-in to extracting code + /// guarded behind `#if canImport(MyModule)` (e.g. another language code + /// generator can declare its runtime module importable here). Default: empty. + var availableImportModules: Set { get } + /// Whether the given module name has stub declarations configured. func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool } @@ -74,6 +89,10 @@ public protocol SwiftExtractConfiguration { extension SwiftExtractConfiguration { public var extractsOperators: Bool { false } + public var extractsGenericTypeInitializers: Bool { false } + + public var availableImportModules: Set { [] } + public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool { importedModuleStubs?.keys.contains(moduleName) ?? false } @@ -91,6 +110,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { public var swiftExtractAccessLevel: AccessLevelMode public var swiftExtractLogLevel: Logger.Level? public var extractsOperators: Bool + public var availableImportModules: Set public init( swiftModule: String? = nil, @@ -100,7 +120,8 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { staticBuildConfigurationFile: String? = nil, swiftFilterInclude: [String]? = nil, swiftFilterExclude: [String]? = nil, - importedModuleStubs: [String: [String]]? = nil + importedModuleStubs: [String: [String]]? = nil, + availableImportModules: Set = [] ) { self.swiftModule = swiftModule self.swiftExtractAccessLevel = accessLevel @@ -110,5 +131,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { self.swiftFilterInclude = swiftFilterInclude self.swiftFilterExclude = swiftFilterExclude self.importedModuleStubs = importedModuleStubs + self.availableImportModules = availableImportModules } } diff --git a/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift index 41f588974..a11d76d73 100644 --- a/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift @@ -101,3 +101,41 @@ extension BuildConfiguration where Self == 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 } +} From b3d4de32da043e4790b4024b295b7f8a65ff7fa5 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 3 Jun 2026 22:15:38 +0900 Subject: [PATCH 09/38] SwiftExtract: include URL in the Foundation known-module overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Foundation/FoundationEssentials known-module source files declared Data, Date, and UUID, but URL was missing — so any user code using URL (e.g. `func getHost(url: URL)`) failed to import with an unresolved-type warning, dropping the function silently. Other consumers of the language-neutral analyzer rely on URL for bridging tests; add it alongside the other Foundation built-ins, declaring just the failable `init(string:)` and `absoluteString` property the language-neutral analyzer needs to resolve uses. --- Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift b/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift index 47e360146..689c24f61 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftKnownModules.swift @@ -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 { From 6c56f7526f9ed5c07a84231312dfa731ee48ae91 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 4 Jun 2026 08:50:02 +0900 Subject: [PATCH 10/38] SwiftExtract: cover extractsGenericTypeInitializers & availableImportModules `DefaultSwiftExtractConfiguration` exposed `extractsOperators` and `availableImportModules` as stored properties with init parameters but omitted `extractsGenericTypeInitializers`, so callers using the default config could not opt into it programmatically and silently fell back to the protocol-extension default of `false`. Add it as a stored property and init parameter alongside the other two. Also add four targeted tests in `AnalysisResultTests` exercising the non-default paths of the two analysis-shaping knobs: - `unspecializedGenericInitializersAreSkippedByDefault` and `extractsGenericTypeInitializersKeepsBaseInitializers` confirm the base `Tank` flips between 0 and 2 initializers as the knob toggles. - `canImportGuardedDeclsAreSkippedWhenModuleNotAvailable` and `availableImportModulesActivatesCanImportClause` confirm `#if canImport(MadeUpModule)`-guarded types are extracted only when the module is listed in `availableImportModules`. --- .../SwiftExtractConfiguration.swift | 3 + .../AnalysisResultTests.swift | 114 ++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index 090912599..cb6840495 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -110,6 +110,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { public var swiftExtractAccessLevel: AccessLevelMode public var swiftExtractLogLevel: Logger.Level? public var extractsOperators: Bool + public var extractsGenericTypeInitializers: Bool public var availableImportModules: Set public init( @@ -117,6 +118,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { accessLevel: AccessLevelMode = .public, logLevel: Logger.Level? = nil, extractsOperators: Bool = false, + extractsGenericTypeInitializers: Bool = false, staticBuildConfigurationFile: String? = nil, swiftFilterInclude: [String]? = nil, swiftFilterExclude: [String]? = nil, @@ -127,6 +129,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { self.swiftExtractAccessLevel = accessLevel self.swiftExtractLogLevel = logLevel self.extractsOperators = extractsOperators + self.extractsGenericTypeInitializers = extractsGenericTypeInitializers self.staticBuildConfigurationFile = staticBuildConfigurationFile self.swiftFilterInclude = swiftFilterInclude self.swiftFilterExclude = swiftFilterExclude diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index ad9cbec7c..64ff1d3c1 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -313,4 +313,118 @@ struct AnalysisResultSuite { #expect(result.extractedGlobalFuncs.isEmpty) #expect(result.extractedGlobalVariables.isEmpty) } + + // ==== ----------------------------------------------------------------------- + // MARK: Configuration knobs + + /// By default, initializers on an unspecialized generic type are NOT + /// extracted: the open generic isn't directly constructible, so swift-java + /// (and any caller leaving the knob at its default `false`) drops them. + @Test func unspecializedGenericInitializersAreSkippedByDefault() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + public init(capacity: Int) {} + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let tank = try #require(result.extractedTypes["Tank"]) + #expect(tank.swiftNominal.isGeneric) + #expect(!tank.isSpecialization) + #expect(tank.initializers.isEmpty) + } + + /// Opting into `extractsGenericTypeInitializers` keeps the base type's + /// initializers in the analysis result so post-analysis specializers can + /// clone them onto a concrete `Tank`. + @Test func extractsGenericTypeInitializersKeepsBaseInitializers() throws { + var config = DefaultSwiftExtractConfiguration() + config.extractsGenericTypeInitializers = true + + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tank { + public init() {} + public init(capacity: Int) {} + } + """ + ) + ], + moduleName: "Aquarium", + config: config + ) + + let tank = try #require(result.extractedTypes["Tank"]) + #expect(tank.swiftNominal.isGeneric) + #expect(!tank.isSpecialization) + #expect(tank.initializers.count == 2) + } + + /// `#if canImport()` blocks are inactive by default for modules the + /// build configuration doesn't know about — the type guarded behind them + /// must not appear in the analysis result. + @Test func canImportGuardedDeclsAreSkippedWhenModuleNotAvailable() throws { + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct AlwaysHere { + public init() {} + } + #if canImport(MadeUpModule) + public struct OnlyWhenImportable { + public init() {} + } + #endif + """ + ) + ], + moduleName: "Aquarium" + ) + + #expect(result.extractedTypes["AlwaysHere"] != nil) + #expect(result.extractedTypes["OnlyWhenImportable"] == nil) + } + + /// Adding the module to `availableImportModules` activates the + /// `#if canImport()` clause so its declarations are extracted. + @Test func availableImportModulesActivatesCanImportClause() throws { + var config = DefaultSwiftExtractConfiguration() + config.availableImportModules = ["MadeUpModule"] + + let result = try SwiftAnalyzer.analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct AlwaysHere { + public init() {} + } + #if canImport(MadeUpModule) + public struct OnlyWhenImportable { + public init() {} + } + #endif + """ + ) + ], + moduleName: "Aquarium", + config: config + ) + + #expect(result.extractedTypes["AlwaysHere"] != nil) + #expect(result.extractedTypes["OnlyWhenImportable"] != nil) + } } From dd0d77a17708ad6f7e019aae30fde45013a4c8b9 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 4 Jun 2026 08:50:17 +0900 Subject: [PATCH 11/38] SwiftExtract: document the dummy.json placeholder in Package.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Resources/dummy.json` placeholder exists only so SwiftPM emits a `Bundle.module` for the SwiftExtract target — the real `static-build-config.json` is generated at build time by `_StaticBuildConfigPlugin`. Note that on the `.process("Resources")` line so a future reader doesn't try to delete the empty-looking file. --- Package.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Package.swift b/Package.swift index 617329217..f8cfc6762 100644 --- a/Package.swift +++ b/Package.swift @@ -354,6 +354,9 @@ let package = Package( ], 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: [ From 14b7ede4a959cfeaa1eefe643f1c32c8b3983cc4 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 4 Jun 2026 12:02:30 +0900 Subject: [PATCH 12/38] SwiftExtract: doc cleanup, drop default knob impls, drop swift-5 mode on tests Three small review-driven cleanups: - `SwiftAnalyzer` doc: drop the parenthetical example in the lead-in and fix "language-neutral" to "output is language-neutral"; drop the "useful for tests / no code generation" sentence on the static `analyze` convenience. - `SwiftExtractConfiguration`: remove the protocol-extension defaults for `extractsOperators` and `extractsGenericTypeInitializers`. Both are semantic decisions about what the analysis layer should do for a given language target; making conformers state their position keeps a new language code generator from silently inheriting the Java-specific defaults. `Configuration` (swift-java) now declares both as `false` explicitly. - `SwiftExtractTests`: drop the `.swiftLanguageMode(.v5)` override; the target compiles and runs cleanly under the package's default Swift 6 mode. --- Package.swift | 3 --- Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift | 5 +++++ Sources/SwiftExtract/SwiftAnalyzer.swift | 7 +++---- Sources/SwiftExtract/SwiftExtractConfiguration.swift | 4 ---- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index f8cfc6762..c67314238 100644 --- a/Package.swift +++ b/Package.swift @@ -475,9 +475,6 @@ let package = Package( "SwiftExtract", .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftParser", package: "swift-syntax"), - ], - swiftSettings: [ - .swiftLanguageMode(.v5) ] ), diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift index eef586b89..50bf71225 100644 --- a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -42,6 +42,11 @@ extension Configuration: SwiftExtractConfiguration { case .critical: return .critical } } + + // swift-java targets Java, which cannot express Swift operators or + // construct open generic types directly: leave both knobs off + public var extractsOperators: Bool { false } + public var extractsGenericTypeInitializers: Bool { false } } extension LogLevel { diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 9c16013bb..ccea5c0e4 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -19,9 +19,9 @@ import SwiftParser import SwiftSyntax /// Drives the analysis of Swift source code into an `AnalysisResult` that -/// downstream language generators (e.g. Java/JNI/FFM, others) can consume +/// downstream language generators can consume. /// -/// The analysis is language-neutral; language-specific extraction rules +/// The analysis output is language-neutral; language-specific extraction rules /// (such as honoring Java's `@JavaExport` or skipping `@JavaClass`-wrapped /// types) are layered in via an optional `ExtractDecider` public final class SwiftAnalyzer { @@ -157,8 +157,7 @@ extension SwiftAnalyzer { } /// Top-level convenience: run analysis on the given Swift sources and return - /// the resulting `AnalysisResult`. Useful for tests and for callers that only - /// need analysis (no code generation). + /// the resulting `AnalysisResult`. public static func analyze( sources: [(path: String, text: String)], moduleName: String, diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index cb6840495..03e768421 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -87,10 +87,6 @@ public protocol SwiftExtractConfiguration { } extension SwiftExtractConfiguration { - public var extractsOperators: Bool { false } - - public var extractsGenericTypeInitializers: Bool { false } - public var availableImportModules: Set { [] } public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool { From a49b07ad96dcce016cd186f57d89ab14148f1f45 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 4 Jun 2026 12:03:46 +0900 Subject: [PATCH 13/38] SwiftExtract: extract AccessLevelMode into SwiftExtractConfigurationShared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer asked: "why does swift-java need to map at all, can it not use this exact enum?" — yes, after introducing a small shared target. `SwiftJavaConfigurationShared` is intentionally lightweight (stdlib + Foundation only): it must be symlinked into each plugin's source tree because SwiftPM plugins can't have target dependencies. Pulling SwiftSyntax in via `SwiftExtract` would balloon plugin builds. Instead, introduce a sibling `SwiftExtractConfigurationShared` target that holds nothing but the small `AccessLevelMode` enum. Both `SwiftExtract` and `SwiftJavaConfigurationShared` depend on it; the plugin symlink discipline (`Plugins/PluginsShared/SwiftExtractConfigurationShared`) mirrors the existing `SwiftJavaConfigurationShared` symlink. Effects: - `AccessLevelMode` is the single, shared enum. swift-java's `Configuration` uses it directly via `@_exported import`, retiring `JExtractMinimumAccessLevelMode` and the four-line mapping switch in `Configuration+SwiftExtract.swift` (now an identity passthrough). - `SwiftJavaConfigurationShared/Configuration.swift` guards the import with `#if canImport(SwiftExtractConfigurationShared)` so plugin builds (which inline the file alongside `AccessLevelMode.swift` via symlink rather than as a separate module) still compile. - `AccessLevelMode` gains `@nonexhaustive` (SE-0487, gated with `#if compiler(>=6.2)`) per reviewer request, so adding cases in the future is non-breaking. `Codable` conformance is added so it can replace `JExtractMinimumAccessLevelMode` in the on-disk `Configuration` JSON without changing the wire format. - The CLI's `@Option var minimumInputAccessLevelMode` and the `ExpressibleByArgument` conformance switch over to `AccessLevelMode` accordingly. --- Package.swift | 15 +++++++- .../SwiftExtractConfigurationShared | 1 + .../Configuration+SwiftExtract.swift | 14 ++++---- .../SwiftExtractConfiguration.swift | 22 +++++------- .../AccessLevelMode.swift | 34 +++++++++++++++++++ .../Configuration.swift | 11 ++++-- .../JExtractMinimumAccessLevelMode.swift | 26 -------------- .../Commands/JExtractCommand.swift | 4 +-- 8 files changed, 75 insertions(+), 52 deletions(-) create mode 120000 Plugins/PluginsShared/SwiftExtractConfigurationShared create mode 100644 Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift delete mode 100644 Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift diff --git a/Package.swift b/Package.swift index c67314238..88157ad6f 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"] @@ -276,7 +281,14 @@ let package = Package( ), .target( - name: "SwiftJavaConfigurationShared" + name: "SwiftJavaConfigurationShared", + dependencies: [ + "SwiftExtractConfigurationShared" + ] + ), + + .target( + name: "SwiftExtractConfigurationShared" ), .target( @@ -351,6 +363,7 @@ let package = Package( .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "Logging", package: "swift-log"), + "SwiftExtractConfigurationShared", ], path: "Sources/SwiftExtract", resources: [ diff --git a/Plugins/PluginsShared/SwiftExtractConfigurationShared b/Plugins/PluginsShared/SwiftExtractConfigurationShared new file mode 120000 index 000000000..263f08269 --- /dev/null +++ b/Plugins/PluginsShared/SwiftExtractConfigurationShared @@ -0,0 +1 @@ +../../Sources/SwiftExtractConfigurationShared \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift index 50bf71225..d55e59675 100644 --- a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -18,16 +18,14 @@ import SwiftJavaConfigurationShared /// Bridges swift-java's `Configuration` onto the language-neutral /// `SwiftExtractConfiguration` surface consumed by `SwiftExtract`. /// -/// Most members are satisfied directly by `Configuration`'s own properties; only -/// the two enum-typed members need a mapping from swift-java's enums onto the -/// neutral `AccessLevelMode` / `Logger.Level`. +/// Most members are satisfied directly by `Configuration`'s own properties. +/// `Configuration` shares `AccessLevelMode` with the analyzer (both pull it in +/// from `SwiftExtractConfigurationShared`), so `swiftExtractAccessLevel` is a +/// straight passthrough. Only `swiftExtractLogLevel` needs a mapping from +/// swift-java's `LogLevel` onto the neutral `Logger.Level`. extension Configuration: SwiftExtractConfiguration { public var swiftExtractAccessLevel: AccessLevelMode { - switch effectiveMinimumInputAccessLevelMode { - case .public: .public - case .package: .package - case .internal: .internal - } + effectiveMinimumInputAccessLevelMode } public var swiftExtractLogLevel: SwiftExtract.Logger.Level? { diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index 03e768421..ba3e20557 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -12,16 +12,7 @@ // //===----------------------------------------------------------------------===// -/// Minimum access level a declaration must have to be considered for extraction. -/// -/// Language-neutral counterpart to a configuration's access-level setting. The -/// concrete `Configuration` types in language layers (e.g. swift-java, or other -/// language code generators) map their own enums onto this. -public enum AccessLevelMode: String, Sendable { - case `public` - case `package` - case `internal` -} +@_exported import SwiftExtractConfigurationShared /// The configuration surface required by the language-neutral `SwiftExtract` /// analysis layer. @@ -33,9 +24,14 @@ public enum AccessLevelMode: String, Sendable { /// (e.g. Java/JNI/FFM, or other language code generators) without pulling /// target-specific config types into `SwiftExtract`. /// -/// The two enum-typed members use `swiftExtract`-prefixed names so a conforming -/// type can keep its own, differently-typed `logLevel` / -/// `effectiveMinimumInputAccessLevelMode` members without a name collision. +/// `AccessLevelMode` lives in the small `SwiftExtractConfigurationShared` +/// target so language-specific configuration shared modules (e.g. +/// `SwiftJavaConfigurationShared`) can use the same enum directly without +/// taking a dependency on SwiftSyntax. +/// +/// The enum-typed `swiftExtractLogLevel` member uses a `swiftExtract`-prefixed +/// name so a conforming type can keep its own, differently-typed `logLevel` +/// member without a name collision. public protocol SwiftExtractConfiguration { /// Name of the Swift module being analyzed. var swiftModule: String? { get } diff --git a/Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift b/Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift new file mode 100644 index 000000000..89d4325b3 --- /dev/null +++ b/Sources/SwiftExtractConfigurationShared/AccessLevelMode.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Minimum access level a declaration must have to be considered for extraction. +/// +/// Lives in the small `SwiftExtractConfigurationShared` target so the analysis +/// layer and language-specific configuration layers (e.g. swift-java's +/// `SwiftJavaConfigurationShared`) can both depend on it without dragging +/// SwiftSyntax into the latter. +#if compiler(>=6.2) +@nonexhaustive +#endif +public enum AccessLevelMode: String, Codable, Sendable { + case `public` + case `package` + case `internal` +} + +extension AccessLevelMode { + public static var `default`: Self { + .public + } +} diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 1562b4bf6..61af95a33 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -14,6 +14,13 @@ import Foundation +// In a real module build this resolves to a separate target. In plugin builds +// the file is inlined (via symlink) alongside `AccessLevelMode.swift`, so the +// module isn't a discoverable import — guard with canImport. +#if canImport(SwiftExtractConfigurationShared) +@_exported import SwiftExtractConfigurationShared +#endif + //////////////////////////////////////////////////////////////////////////////// // This file is only supposed to be edited in `Shared/` and must be symlinked // // from everywhere else! We cannot share dependencies with or between plugins // @@ -62,8 +69,8 @@ public struct Configuration: Codable { writeEmptyFiles ?? false } - public var minimumInputAccessLevelMode: JExtractMinimumAccessLevelMode? - public var effectiveMinimumInputAccessLevelMode: JExtractMinimumAccessLevelMode { + public var minimumInputAccessLevelMode: AccessLevelMode? + public var effectiveMinimumInputAccessLevelMode: AccessLevelMode { minimumInputAccessLevelMode ?? .default } diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift deleted file mode 100644 index 22fead577..000000000 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift +++ /dev/null @@ -1,26 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -/// The minimum access level which -public enum JExtractMinimumAccessLevelMode: String, Codable { - case `public` - case `package` - case `internal` -} - -extension JExtractMinimumAccessLevelMode { - public static var `default`: Self { - .public - } -} diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index f901eb731..020ae8202 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -66,7 +66,7 @@ extension SwiftJava { var writeEmptyFiles: Bool? @Option(help: "The lowest access level of Swift declarations that should be extracted, defaults to 'public'.") - var minimumInputAccessLevelMode: JExtractMinimumAccessLevelMode? + var minimumInputAccessLevelMode: AccessLevelMode? @Option( help: @@ -222,6 +222,6 @@ struct IllegalModeCombinationError: Error { } extension JExtractGenerationMode: ExpressibleByArgument {} -extension JExtractMinimumAccessLevelMode: ExpressibleByArgument {} +extension AccessLevelMode: ExpressibleByArgument {} extension JExtractMemoryManagementMode: ExpressibleByArgument {} extension JExtractAsyncFuncMode: ExpressibleByArgument {} From 7c9d99b1a0cb84a6e106c4872213ad6d068702be Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 9 Jun 2026 08:14:27 +0900 Subject: [PATCH 14/38] CodePrinting: Sendable conformance + emitSourceLocations toggle Two small additions to make CodePrinter usable from downstream targets that compare generated output against goldens: - Mark CodePrinter / PrintMode / PrinterTerminator as Sendable so the type can flow through Sendable-checked contexts (e.g. an actor-bound generator). The state is plain value-type data with no reference fields, so the conformance is unconditional. - Add an `emitSourceLocations: Bool = true` instance flag. When false, a `.sloc` terminator collapses to a plain newline instead of appending ` // function @ file:line`. Default-true preserves the existing tracing behavior; downstream targets that diff generated output against goldens can flip it off to get clean output. --- Package.swift | 5 +++++ Sources/CodePrinting/CodePrinter.swift | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 88157ad6f..eb47bb7be 100644 --- a/Package.swift +++ b/Package.swift @@ -118,6 +118,11 @@ let package = Package( targets: ["SwiftExtract"] ), + .library( + name: "CodePrinting", + targets: ["CodePrinting"] + ), + .library( name: "JExtractSwiftLib", targets: ["JExtractSwiftLib"] diff --git a/Sources/CodePrinting/CodePrinter.swift b/Sources/CodePrinting/CodePrinter.swift index d67679039..8e45b14ed 100644 --- a/Sources/CodePrinting/CodePrinter.swift +++ b/Sources/CodePrinting/CodePrinter.swift @@ -21,11 +21,18 @@ 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 (the default, + /// preserving long-standing behavior). 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 + public var indentationDepth: Int = 0 { didSet { indentationText = String(repeating: indentationPart, count: indentationDepth) @@ -49,7 +56,7 @@ public struct CodePrinter { } public var mode: PrintMode - public enum PrintMode { + public enum PrintMode: Sendable { case accumulateAll case flushToFileOnWrite } @@ -167,7 +174,11 @@ public struct CodePrinter { } if terminator == .sloc { - append(" // \(function) @ \(file):\(line)\n") + if emitSourceLocations { + append(" // \(function) @ \(file):\(line)\n") + } else { + append("\n") + } atNewline = true } else { append(terminator.rawValue) @@ -218,7 +229,7 @@ public struct CodePrinter { // ==== ----------------------------------------------------------------------- // MARK: PrinterTerminator -public enum PrinterTerminator: String { +public enum PrinterTerminator: String, Sendable { case newLine = "\n" case space = " " case commaSpace = ", " From 78edf7daadd194b9ad1cf10b8ca9a191f8d37f2c Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 9 Jun 2026 09:04:01 +0900 Subject: [PATCH 15/38] SwiftExtract: permitsUnresolvedTypeReferences + SwiftSyntheticTypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an opt-in path for downstream language code generators that treat unresolved type names symbolically rather than dropping the enclosing declaration. `SwiftExtract` defaults to a strict policy: a parameter / return / property type that the symbol table can't resolve causes the enclosing `SwiftFunctionSignature` constructor to throw, and the analyzer drops the whole declaration with 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. Other language code generators that resolve the name later — associated types in a protocol requirement before carrier substitution; a property type that names a generic parameter to be replaced during specialization; an external type bridged by simple name — can opt-in by setting `SwiftExtractConfiguration.permitsUnresolvedTypeReferences = true`. Name lookups that fail then synthesize a nominal via `SwiftSyntheticTypes.unresolvedNominal(_:)` so the enclosing declaration survives and a downstream pass can substitute or recognize the placeholder. API additions: - SwiftExtractConfiguration: `var permitsUnresolvedTypeReferences: Bool` with a default of `false` in the protocol extension. The DefaultSwiftExtractConfiguration init grows a matching parameter. - SwiftTypeLookupContext: `var permitsUnresolvedTypeReferences: Bool` (false by default), populated by SwiftAnalyzer.prepareForTranslation from the config. - SwiftSyntheticTypes.unresolvedNominal(_:moduleName:): public utility that mints a SwiftType.nominal from a bare name by parsing a throwaway `struct \(name) {}` to obtain the syntax node SwiftNominalTypeDeclaration requires. The synthetic moduleName is `__SwiftExtractSynthesized` so downstream code can route around these when needed. Default behavior (existing consumers) is unchanged: 30/30 SwiftExtract tests still pass. --- Sources/SwiftExtract/SwiftAnalyzer.swift | 2 + .../SwiftExtractConfiguration.swift | 28 +++++++- .../SwiftTypes/SwiftSyntheticTypes.swift | 65 +++++++++++++++++++ .../SwiftExtract/SwiftTypes/SwiftType.swift | 20 ++++++ .../SwiftTypes/SwiftTypeLookupContext.swift | 6 ++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index ccea5c0e4..2ac05c1c1 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -250,6 +250,8 @@ extension SwiftAnalyzer { buildConfig: self.buildConfig, ) self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) + self.lookupContext.permitsUnresolvedTypeReferences = + self.config.permitsUnresolvedTypeReferences } /// Check if any of the imported decls uses a nominal declaration that satisfies diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index ba3e20557..9d39cdbb1 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -78,6 +78,27 @@ public protocol SwiftExtractConfiguration { /// 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. + /// + /// Other language code generators that treat unresolved names *symbolically* + /// (e.g. associated types in a protocol requirement before carrier + /// substitution; a property type that names a generic parameter to be + /// replaced during specialization; an external type the user is expected + /// to bridge by simple name) can opt-in by setting this `true`. Unresolved + /// names then become synthetic nominal types via + /// `SwiftSyntheticTypes.unresolvedNominal(_:)` so downstream passes can + /// substitute or recognize them. Default: false. + var permitsUnresolvedTypeReferences: Bool { get } + /// Whether the given module name has stub declarations configured. func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool } @@ -85,6 +106,8 @@ public protocol SwiftExtractConfiguration { extension SwiftExtractConfiguration { public var availableImportModules: Set { [] } + public var permitsUnresolvedTypeReferences: Bool { false } + public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool { importedModuleStubs?.keys.contains(moduleName) ?? false } @@ -104,6 +127,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { public var extractsOperators: Bool public var extractsGenericTypeInitializers: Bool public var availableImportModules: Set + public var permitsUnresolvedTypeReferences: Bool public init( swiftModule: String? = nil, @@ -115,7 +139,8 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { swiftFilterInclude: [String]? = nil, swiftFilterExclude: [String]? = nil, importedModuleStubs: [String: [String]]? = nil, - availableImportModules: Set = [] + availableImportModules: Set = [], + permitsUnresolvedTypeReferences: Bool = false ) { self.swiftModule = swiftModule self.swiftExtractAccessLevel = accessLevel @@ -127,5 +152,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { self.swiftFilterExclude = swiftFilterExclude self.importedModuleStubs = importedModuleStubs self.availableImportModules = availableImportModules + self.permitsUnresolvedTypeReferences = permitsUnresolvedTypeReferences } } diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift b/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift new file mode 100644 index 000000000..b4b6462ed --- /dev/null +++ b/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// 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 (associated types, +// pre-substitution generic parameters, externally-bridged types) — 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 { + /// A synthetic module name used for nominals minted by + /// `unresolvedNominal(_:)`. Placed in the type's `moduleName` so downstream + /// code can recognize and route around them when needed. + public static let syntheticModuleName = "__SwiftExtractSynthesized" + + /// Build an unresolved nominal `SwiftType` for the given simple type name. + /// + /// Useful when the caller is willing to treat a name as a placeholder to be + /// substituted (or recognized symbolically) by a later pass — e.g. an + /// associated type referenced before carrier substitution, or a generic + /// parameter referenced before specialization. + public static func unresolvedNominal( + _ name: String, + moduleName: String = SwiftSyntheticTypes.syntheticModuleName + ) -> 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: moduleName, + parent: nil, + node: structSyntax + ) + return .nominal(SwiftNominalType(nominalTypeDecl: decl)) + } +} diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift index d1b17a20e..78bb8599d 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -465,6 +465,26 @@ extension SwiftType { typeDecl = try lookupContext.unqualifiedLookup(name: ident, from: name) } guard let typeDecl else { + // Lenient mode (opt-in via SwiftExtractConfiguration.permitsUnresolvedTypeReferences): + // synthesize an unresolved nominal 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.permitsUnresolvedTypeReferences { + 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) } diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift index bee0ba061..25d5eaca1 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift @@ -22,6 +22,12 @@ import SwiftSyntax 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.permitsUnresolvedTypeReferences`. + public var permitsUnresolvedTypeReferences: Bool = false + private var typeDecls: [Syntax.ID: SwiftTypeDeclaration] = [:] /// Set of typealias syntax ids currently being resolved, to break From 759a17af469096d45cec0624a4410aa8452d916d Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 9 Jun 2026 11:12:03 +0900 Subject: [PATCH 16/38] SwiftExtract: registerSpecialization hook for downstream config-driven specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a public seam so downstream language code generators can drive generic-type specialization from configuration sources other than Swift typealiases (e.g. a `specialize:` config entry that names a base type by qualified name) and have those specializations participate in deferred-constrained-extension matching alongside the analyzer's natively-registered ones. Why it matters: swift-java's analyzer registers a specialization the moment it walks a `typealias Alias = Base` decl, then `applyPendingSpecializations` drives `findMatchingSpecializations` against the deferred constrained extensions queue (`extension Box where T == ConcreteT { … }`). Downstream targets that materialize their own specializations *after* analysis (because the typealias has an attribute the analyzer doesn't recognize, or because the spec lives in JSON config) miss the window — the deferred queue runs against an empty registry and the constrained-extension methods are silently dropped. API additions: - `SwiftAnalyzer.analyze(beforeProcessingDeferredExtensions:)` — instance and static overloads. The hook fires after the per-source walk has populated the registry from in-source typealiases, but before deferred-extension processing. The default-arg overloads preserve existing call sites. - `SwiftAnalyzer.registerSpecialization(baseQualifiedName:outputName:typeArgs:)` — public API that resolves the base by qualified name through the symbol table, calls `ExtractedNominalType.specialize`, and inserts into `translator.specializations`. Returns nil when the base can't be found or isn't generic. Default behavior (existing consumers calling the no-hook overload) is unchanged: 30/30 SwiftExtract tests still pass. --- Sources/SwiftExtract/SwiftAnalyzer.swift | 135 ++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 2ac05c1c1..46b756976 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -141,6 +141,26 @@ extension SwiftAnalyzer { /// Analyze registered inputs. package func analyze() throws { + try analyze(beforeProcessingDeferredExtensions: { _ in }) + } + + /// Analyze registered inputs, 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. swift-python's + /// `@Python typealias` post-pass, or 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 + ) throws { prepareForTranslation() let visitor = SwiftAnalysisVisitor(translator: self) @@ -150,12 +170,100 @@ extension SwiftAnalyzer { 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 output 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 output name. + public var registeredSpecializationNames: Set { + Set(self.specializations.values.flatMap { $0 }.map(\.effectiveOutputName)) + } + /// Top-level convenience: run analysis on the given Swift sources and return /// the resulting `AnalysisResult`. public static func analyze( @@ -164,6 +272,31 @@ extension SwiftAnalyzer { config: (any SwiftExtractConfiguration)? = nil, sourceDependencies: SourceDependencies = SourceDependencies(), extractDecider: (any ExtractDecider)? = nil + ) throws -> AnalysisResult { + try analyze( + sources: sources, + moduleName: moduleName, + config: config, + sourceDependencies: sourceDependencies, + extractDecider: extractDecider, + beforeProcessingDeferredExtensions: { _ in } + ) + } + + /// Top-level convenience that 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. `@Python typealias`, + /// `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, + config: (any SwiftExtractConfiguration)? = nil, + sourceDependencies: SourceDependencies = SourceDependencies(), + extractDecider: (any ExtractDecider)? = nil, + beforeProcessingDeferredExtensions hook: (SwiftAnalyzer) throws -> Void ) throws -> AnalysisResult { let effectiveConfig = config ?? DefaultSwiftExtractConfiguration(swiftModule: moduleName) let translator = SwiftAnalyzer(config: effectiveConfig, moduleName: moduleName, extractDecider: extractDecider) @@ -171,7 +304,7 @@ extension SwiftAnalyzer { for source in sources { translator.add(filePath: source.path, text: source.text) } - try translator.analyze() + try translator.analyze(beforeProcessingDeferredExtensions: hook) return translator.result } From 3db5a0920b463d34f5b66613801af2df2fb02a37 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Fri, 12 Jun 2026 14:58:00 +0900 Subject: [PATCH 17/38] SwiftExtract: per-decl extraction policy belongs to each ExtractDecider Hoist the access-level filter and the operator-skip rule out of SwiftExtract and into each decider. The protocol becomes a single Bool answer instead of the prior tri-state Bool? override on top of a precomputed accessLevelPasses bit. - ExtractDecider.shouldExtract(decl:in:log:) -> Bool; deciders trace-log their own skip paths - DefaultExtractDecider: minimal access-level-only decider (analyzer fallback) - WithModifiersSyntax.passesAccessLevel(_:in:): shared helper, public overload takes ExtractedNominalType? so deciders don't need SPI access - JavaExtractDecider takes AccessLevelMode at init and now also handles the Java-specific operator skip (@JavaExport / @JavaClass-family rules unchanged) - Drop extractsOperators from SwiftExtractConfiguration + conformers; a language target that wants operators just omits the operator branch in its decider - SwiftAnalysisVisitor.shouldExtract becomes a one-line delegate; drop the operator switch from the function visitor - Wire accessLevel from config at the Java production call site (Swift2Java) and the two test helpers (TextAssertions, LoweringAssertions) --- Sources/CodePrinting/CodePrinter.swift | 3 +- .../Configuration+SwiftExtract.swift | 4 +- .../JExtractSwiftLib/JavaExtractDecider.swift | 53 +++++++++++++---- Sources/JExtractSwiftLib/Swift2Java.swift | 5 +- Sources/SwiftExtract/AnalysisResult.swift | 8 ++- .../Convenience/SwiftSyntax+Extensions.swift | 30 +++++++++- Sources/SwiftExtract/ExtractDecider.swift | 58 +++++++++++++++---- .../SwiftExtract/SwiftAnalysisVisitor.swift | 47 ++------------- .../SwiftExtractConfiguration.swift | 10 ---- .../Asserts/LoweringAssertions.swift | 10 +++- .../Asserts/TextAssertions.swift | 5 +- 11 files changed, 145 insertions(+), 88 deletions(-) diff --git a/Sources/CodePrinting/CodePrinter.swift b/Sources/CodePrinting/CodePrinter.swift index 8e45b14ed..6aa2926cc 100644 --- a/Sources/CodePrinting/CodePrinter.swift +++ b/Sources/CodePrinting/CodePrinter.swift @@ -27,8 +27,7 @@ public struct CodePrinter: Sendable { public var verbose: Bool = false /// When true, terminators of `.sloc` append a `// function @ file:line` - /// trailer to each line. Useful for debugging the generator (the default, - /// preserving long-standing behavior). Downstream targets that compare + /// 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 diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift index d55e59675..a979da6d3 100644 --- a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -41,9 +41,7 @@ extension Configuration: SwiftExtractConfiguration { } } - // swift-java targets Java, which cannot express Swift operators or - // construct open generic types directly: leave both knobs off - public var extractsOperators: Bool { false } + // swift-java targets Java, which cannot construct open generic types directly public var extractsGenericTypeInitializers: Bool { false } } diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index a7687a381..93f6097ea 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -15,24 +15,57 @@ import SwiftExtract import SwiftSyntax -/// Java-specific extraction overrides applied on top of SwiftExtract's -/// built-in access-level filter: +/// Java-specific per-decl extraction policy /// -/// - `@JavaExport` forces extraction even of non-public decls -/// - `@JavaClass` / `@JavaInterface` / `@JavaField` / `@JavaStaticField` / -/// `@JavaMethod` / `@JavaStaticMethod` / `@JavaImplementation` are Swift -/// wrappers of Java types — skip them during extraction +/// In addition to the configured access-level filter, the Java target: +/// +/// - 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 init() {} + public let accessLevel: AccessLevelMode + + public init(accessLevel: AccessLevelMode = .default) { + self.accessLevel = accessLevel + } - public func shouldExtract(decl: DeclSyntax, accessLevelPasses: Bool) -> Bool? { - let attrs = decl.asProtocol(WithAttributesSyntax.self)?.attributes + public func shouldExtract( + decl: DeclSyntax, + in parent: ExtractedNominalType?, + log: Logger + ) -> Bool { + let attrs = decl.asProtocol((any WithAttributesSyntax).self)?.attributes if attrs?.contains(where: { $0.isJavaExport }) == true { return true } if attrs?.contains(where: { $0.isSwiftJavaMacro }) == true { + log.trace("Skip '\(decl.qualifiedNameForDebug)': swift-java macro-wrapped Java type") return false } - return nil + + // 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 { + log.trace("Skip '\(decl.qualifiedNameForDebug)': not a modifier-bearing decl") + return false + } + let ok = mod.passesAccessLevel(accessLevel, in: parent) + if !ok { + log.trace("Skip '\(decl.qualifiedNameForDebug)': not at least \(accessLevel)") + } + return ok } } diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 2bf5ef037..8e2b7bc33 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -35,7 +35,10 @@ public struct SwiftToJava { fatalError("Missing '--swift-module' name.") } - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = SwiftAnalyzer( + config: config, + extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), + ) let log = translator.log if config.javaPackage == nil || config.javaPackage!.isEmpty { diff --git a/Sources/SwiftExtract/AnalysisResult.swift b/Sources/SwiftExtract/AnalysisResult.swift index 6c6f0858e..e481038dd 100644 --- a/Sources/SwiftExtract/AnalysisResult.swift +++ b/Sources/SwiftExtract/AnalysisResult.swift @@ -12,10 +12,12 @@ // //===----------------------------------------------------------------------===// +/// 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 let extractedTypes: [String: ExtractedNominalType] - public let extractedGlobalVariables: [ExtractedFunc] - public let extractedGlobalFuncs: [ExtractedFunc] + public var extractedTypes: [String: ExtractedNominalType] + public var extractedGlobalVariables: [ExtractedFunc] + public var extractedGlobalFuncs: [ExtractedFunc] public init( extractedTypes: [String: ExtractedNominalType], diff --git a/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift b/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift index 3b0194db2..4070ac0b1 100644 --- a/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift @@ -121,11 +121,35 @@ extension WithModifiersSyntax { modifier.isAtLeastInternal } } + + /// 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 passesAccessLevel( + _ mode: AccessLevelMode, + in parent: ExtractedNominalType? + ) -> Bool { + self.passesAccessLevel(mode, in: parent?.swiftNominal.syntax) + } + + /// Lower-level overload taking the parent's syntax node directly. Used by + /// the analyzer; downstream `ExtractDecider`s use the `ExtractedNominalType` + /// overload above. + package func passesAccessLevel( + _ 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. - package var ancestorDecl: DeclSyntax? { + public var ancestorDecl: DeclSyntax? { var node: Syntax = Syntax(self) while let parent = node.parent { if let decl = parent.as(DeclSyntax.self) { @@ -137,7 +161,7 @@ extension DeclSyntaxProtocol { } /// Declaration name primarily for debugging. - package var nameForDebug: String { + public var nameForDebug: String { switch DeclSyntax(self).as(DeclSyntaxEnum.self) { case .accessorDecl(let node): node.accessorSpecifier.text @@ -205,7 +229,7 @@ extension DeclSyntaxProtocol { } /// Qualified declaration name primarily for debugging. - package var qualifiedNameForDebug: String { + public var qualifiedNameForDebug: String { if let parent = ancestorDecl { parent.qualifiedNameForDebug + "." + nameForDebug } else { diff --git a/Sources/SwiftExtract/ExtractDecider.swift b/Sources/SwiftExtract/ExtractDecider.swift index 9e576660c..47dc7e9af 100644 --- a/Sources/SwiftExtract/ExtractDecider.swift +++ b/Sources/SwiftExtract/ExtractDecider.swift @@ -14,20 +14,54 @@ import SwiftSyntax -/// A pluggable extraction decision for downstream language generators +/// Per-decl extraction policy for a downstream language target /// -/// The built-in analyzer always applies its access-level filter; a supplied -/// `ExtractDecider` can override that decision on a per-decl basis to encode -/// language-specific rules. For example, the Java target uses one to honor -/// `@JavaExport` (force-include even when access-level would skip) and to -/// skip Swift wrappers of Java types (`@JavaClass`, `@JavaInterface`, …) +/// `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 +/// `DefaultExtractDecider`, which only enforces the configured access +/// level public protocol ExtractDecider { + /// Decide whether `decl` should be extracted. + /// /// - Parameters: /// - decl: the declaration being considered - /// - accessLevelPasses: whether the analyzer's built-in access-level - /// check admits the decl - /// - Returns: `true` to force-extract (even when `accessLevelPasses` - /// is `false`), `false` to skip (even when `accessLevelPasses` is - /// `true`), or `nil` to defer to the default behavior - func shouldExtract(decl: DeclSyntax, accessLevelPasses: Bool) -> Bool? + /// - parent: the nominal type containing `decl`, when applicable + /// - log: the analyzer's logger; deciders should emit a `.trace` line + /// for each skip path so users can see why a decl was dropped + func shouldExtract( + decl: DeclSyntax, + in parent: ExtractedNominalType?, + log: Logger + ) -> Bool +} + +/// Minimal `ExtractDecider` that enforces only the configured access-level +/// filter. Used by `SwiftAnalyzer` when no decider is supplied +public struct DefaultExtractDecider: ExtractDecider { + public let accessLevel: AccessLevelMode + + public init(accessLevel: AccessLevelMode) { + self.accessLevel = accessLevel + } + + public func shouldExtract( + decl: DeclSyntax, + in parent: ExtractedNominalType?, + log: Logger + ) -> Bool { + guard let mod = decl.asProtocol((any WithModifiersSyntax).self) else { + log.trace("Skip '\(decl.qualifiedNameForDebug)': not a modifier-bearing decl") + return false + } + let ok = mod.passesAccessLevel(accessLevel, in: parent) + if !ok { + log.trace("Skip '\(decl.qualifiedNameForDebug)': not at least \(accessLevel)") + } + return ok + } } diff --git a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index d7c0a43f1..2fcf12716 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -183,20 +183,6 @@ final class SwiftAnalysisVisitor { return } - switch node.name.tokenKind { - case .binaryOperator, .prefixOperator, .postfixOperator: - // Operators are extracted as ordinary `.function`s only when the target - // opts in (other language code generators may map them to language - // constructs in a post-pass). Most targets (e.g. Java) cannot express - // Swift operators and skip them. - guard config.extractsOperators else { - self.log.debug("Skip importing: '\(node.qualifiedNameForDebug)'; Operators are not supported.") - return - } - default: - break - } - self.log.debug("Import function: '\(node.qualifiedNameForDebug)'") let signature: SwiftFunctionSignature @@ -722,38 +708,17 @@ final class SwiftAnalysisVisitor { extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { /// Decide whether this declaration should be extracted /// - /// Built-in logic checks only the access level required by `config`. An - /// optional `decider` supplied by a downstream language target can override - /// the result on a per-decl basis (e.g. Java honors `@JavaExport` / - /// `@JavaClass` here) + /// Delegates entirely to the supplied `decider`, falling back to a + /// `DefaultExtractDecider` (access-level-only) when none was provided. + /// All per-decl policy lives in the decider — see `ExtractDecider` func shouldExtract( config: any SwiftExtractConfiguration, log: Logger, in parent: ExtractedNominalType?, decider: (any ExtractDecider)? ) -> Bool { - let accessLevelPasses: Bool = - switch config.swiftExtractAccessLevel { - case .public: self.isPublic(in: parent?.swiftNominal.syntax) - case .package: self.isAtLeastPackage - case .internal: self.isAtLeastInternal - } - - if let override = decider?.shouldExtract( - decl: DeclSyntax(self), - accessLevelPasses: accessLevelPasses - ) { - if !override { - log.debug("Skip import '\(self.qualifiedNameForDebug)': decider rejected") - } - return override - } - - if !accessLevelPasses { - log.debug( - "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.swiftExtractAccessLevel)" - ) - } - return accessLevelPasses + let effective: any ExtractDecider = + decider ?? DefaultExtractDecider(accessLevel: config.swiftExtractAccessLevel) + return effective.shouldExtract(decl: DeclSyntax(self), in: parent, log: log) } } diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index 9d39cdbb1..4df24040f 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -53,13 +53,6 @@ public protocol SwiftExtractConfiguration { /// Minimum access level required for a declaration to be extracted. var swiftExtractAccessLevel: AccessLevelMode { get } - /// Whether operator declarations (e.g. `static func + (…)`) should be - /// extracted as ordinary `.function`s. Most targets (e.g. Java) cannot express - /// Swift operators and leave this `false`; other language code generators that - /// map operators to language constructs set it `true` and recognize the - /// operator functions in a post-analysis pass. - var extractsOperators: Bool { get } - /// Verbosity for the analyzer's logger; `nil` falls back to `.info`. var swiftExtractLogLevel: Logger.Level? { get } @@ -124,7 +117,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { public var importedModuleStubs: [String: [String]]? public var swiftExtractAccessLevel: AccessLevelMode public var swiftExtractLogLevel: Logger.Level? - public var extractsOperators: Bool public var extractsGenericTypeInitializers: Bool public var availableImportModules: Set public var permitsUnresolvedTypeReferences: Bool @@ -133,7 +125,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { swiftModule: String? = nil, accessLevel: AccessLevelMode = .public, logLevel: Logger.Level? = nil, - extractsOperators: Bool = false, extractsGenericTypeInitializers: Bool = false, staticBuildConfigurationFile: String? = nil, swiftFilterInclude: [String]? = nil, @@ -145,7 +136,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { self.swiftModule = swiftModule self.swiftExtractAccessLevel = accessLevel self.swiftExtractLogLevel = logLevel - self.extractsOperators = extractsOperators self.extractsGenericTypeInitializers = extractsGenericTypeInitializers self.staticBuildConfigurationFile = staticBuildConfigurationFile self.swiftFilterInclude = swiftFilterInclude diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index adc338058..4d7ae4e74 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -35,7 +35,10 @@ func assertLoweredFunction( ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = SwiftAnalyzer( + config: config, + extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), + ) if let sourceFile { translator.add(filePath: "Fake.swift", text: sourceFile) @@ -121,7 +124,10 @@ func assertLoweredVariableAccessor( ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = SwiftAnalyzer( + config: config, + extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), + ) 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 baeefe26d..9c8349626 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -52,7 +52,10 @@ func assertOutput( ) throws { var config = config ?? Configuration() config.swiftModule = swiftModuleName - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = SwiftAnalyzer( + config: config, + extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), + ) translator.sourceDependencies.addJavaWrapperStubs(Array(javaClassLookupTable.keys)) for (depModule, depSource) in dependencySwiftSources { let syntax = Parser.parse(source: depSource) From bead7ace6ed440ca8649ea70296c9d9bdde4e400 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Fri, 12 Jun 2026 22:48:56 +0900 Subject: [PATCH 18/38] SwiftExtract: support InlineArray / [N of T] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new structural `SwiftType` case for Swift's fixed-size InlineArray: indirect case inlineArray(count: Int?, element: SwiftType) Why a dedicated case rather than `.nominal` + a new `SugarName`: - The count is an `Int`, not a `SwiftType`, so it doesn't fit `genericArguments: [SwiftType]`. - Other structural shapes (`tuple`, `composite`) live as top-level cases. - Forcing every consumer to make an explicit decision is the point. `count` is optional so the wildcard form `[_ of T]` (in generic contexts), non-literal counts, and zero/negative counts can be parsed and surfaced as nil — downstream gates treat these as unsupported. Counts use `IntegerLiteralExprSyntax.representedLiteralValue`, which correctly handles radix prefixes (`0x`, `0b`, `0o`) and underscore separators. Description renders the sugar form (`[N of T]`), matching how `[T]`, `[K: V]`, and `T?` are printed. JExtract (FFM + JNI) generators are updated mechanically to route `.inlineArray` through their existing `unhandledType`/ `unsupportedSwiftType` paths — no Java-side semantic support in this change. Functions referencing `InlineArray` are skipped with a diagnostic, the same way other unhandled structural types are. Tests/SwiftExtractTests/InlineArrayTypeTests.swift covers the sugar form, explicit `InlineArray`, underscore + hex counts, return positions, nested arrays, and description round-trip. --- .../FFM/CDeclLowering/CRepresentation.swift | 2 +- ...Swift2JavaGenerator+FunctionLowering.swift | 9 +- ...MSwift2JavaGenerator+JavaTranslation.swift | 4 +- ...Generator+InterfaceWrapperGeneration.swift | 10 +- ...ISwift2JavaGenerator+JavaTranslation.swift | 6 +- ...wift2JavaGenerator+NativeTranslation.swift | 8 +- Sources/SwiftExtract/SwiftAnalyzer.swift | 2 + .../SwiftExtract/SwiftTypes/SwiftType.swift | 49 +++++- .../InlineArrayTypeTests.swift | 143 ++++++++++++++++++ 9 files changed, 211 insertions(+), 22 deletions(-) create mode 100644 Tests/SwiftExtractTests/InlineArrayTypeTests.swift diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index d5956b424..727f226e3 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -73,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 31172d11b..5b577f52b 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -446,6 +446,9 @@ struct CdeclLowering { case .composite: throw LoweringError.unhandledType(type) + + case .inlineArray: + throw LoweringError.unhandledType(type) } } @@ -538,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)) } } @@ -640,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) } @@ -856,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/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index c3182c79d..0b4f52695 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -518,7 +518,7 @@ extension FFMSwift2JavaGenerator { // Otherwise, not supported yet. throw JavaTranslationError.unhandledType(swiftType) - case .composite: + case .composite, .inlineArray: throw JavaTranslationError.unhandledType(swiftType) } } @@ -816,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/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index da4a6216f..41a9029a0 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -232,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) } } @@ -259,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)) } } @@ -315,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) } } @@ -342,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)) } } @@ -477,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+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 3012fba79..b5b3ffaa9 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -576,7 +576,7 @@ extension JNISwift2JavaGenerator { case .tuple: throw JavaTranslationError.emptyTuple() - case .composite: + case .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -974,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) } } @@ -1107,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) } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 1009f4c79..fe67b5221 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -353,7 +353,7 @@ extension JNISwift2JavaGenerator { genericRequirements: genericRequirements ) - case .tuple, .composite: + case .tuple, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -678,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) } } @@ -710,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) } } @@ -815,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/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 46b756976..be46b28aa 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -407,6 +407,8 @@ extension SwiftAnalyzer { return check(ty) case .composite(let types): return types.contains(where: check) + case .inlineArray(_, let element): + return check(element) case .genericParameter: return false } diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 78bb8599d..4e6517522 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -55,6 +55,12 @@ public enum SwiftType: Equatable { /// `type1` & `type2` indirect case composite([SwiftType]) + /// `[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([]) } @@ -63,7 +69,7 @@ public enum SwiftType: Equatable { 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 } } @@ -107,7 +113,7 @@ public enum SwiftType: Equatable { return nominal.nominalTypeDecl.isReferenceType case .metatype, .function: return true - case .genericParameter, .tuple, .existential, .opaque, .composite: + case .genericParameter, .tuple, .existential, .opaque, .composite, .inlineArray: return false } } @@ -154,7 +160,7 @@ 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 } } @@ -187,6 +193,9 @@ 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)]" } } } @@ -295,9 +304,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. @@ -595,6 +631,11 @@ 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) + ) } } diff --git a/Tests/SwiftExtractTests/InlineArrayTypeTests.swift b/Tests/SwiftExtractTests/InlineArrayTypeTests.swift new file mode 100644 index 000000000..7a24d8391 --- /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 SwiftAnalyzer.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 SwiftAnalyzer.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 SwiftAnalyzer.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 SwiftAnalyzer.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 SwiftAnalyzer.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 SwiftAnalyzer.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]") + } +} From ed5fbf5c1b3bf2abef6a93233c380d0cb9106d1a Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 15 Jun 2026 10:43:31 +0900 Subject: [PATCH 19/38] SwiftExtract: add makeSwiftJavaAnalyzer --- .../JExtractSwiftLib/JavaExtractDecider.swift | 8 ++++++ Sources/JExtractSwiftLib/Swift2Java.swift | 5 +--- .../Asserts/LoweringAssertions.swift | 10 ++----- .../Asserts/TextAssertions.swift | 5 +--- .../FFMNestedTypesTests.swift | 2 +- .../FuncCallbackImportTests.swift | 6 ++--- .../FunctionDescriptorImportTests.swift | 4 +-- .../JExtractFileFilterTests.swift | 2 +- .../JExtractSwiftTests/JNI/JNIEnumTests.swift | 2 +- .../MethodImportTests.swift | 20 +++++++------- .../SpecializationTests.swift | 8 +++--- .../TypealiasResolutionTests.swift | 26 +++++++++---------- 12 files changed, 47 insertions(+), 51 deletions(-) diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index 93f6097ea..7c5cfc308 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -13,8 +13,16 @@ //===----------------------------------------------------------------------===// import SwiftExtract +import SwiftJavaConfigurationShared import SwiftSyntax +public func makeSwiftJavaAnalyzer(config: Configuration) -> SwiftAnalyzer { + SwiftAnalyzer( + config: config, + extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), + ) +} + /// Java-specific per-decl extraction policy /// /// In addition to the configured access-level filter, the Java target: diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 8e2b7bc33..feff73c5f 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -35,10 +35,7 @@ public struct SwiftToJava { fatalError("Missing '--swift-module' name.") } - let translator = SwiftAnalyzer( - config: config, - extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), - ) + let translator = makeSwiftJavaAnalyzer(config: config) let log = translator.log if config.javaPackage == nil || config.javaPackage!.isEmpty { diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index 4d7ae4e74..645a29133 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -35,10 +35,7 @@ func assertLoweredFunction( ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let translator = SwiftAnalyzer( - config: config, - extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), - ) + let translator = makeSwiftJavaAnalyzer(config: config) if let sourceFile { translator.add(filePath: "Fake.swift", text: sourceFile) @@ -124,10 +121,7 @@ func assertLoweredVariableAccessor( ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let translator = SwiftAnalyzer( - config: config, - extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), - ) + 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 9c8349626..f00a4bec4 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -52,10 +52,7 @@ func assertOutput( ) throws { var config = config ?? Configuration() config.swiftModule = swiftModuleName - let translator = SwiftAnalyzer( - config: config, - extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), - ) + let translator = makeSwiftJavaAnalyzer(config: config) translator.sourceDependencies.addJavaWrapperStubs(Array(javaClassLookupTable.keys)) for (depModule, depSource) in dependencySwiftSources { let syntax = Parser.parse(source: depSource) diff --git a/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift index f8b17c893..67f7c54ee 100644 --- a/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift +++ b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift @@ -34,7 +34,7 @@ final class FFMNestedTypesTests { func test_nested_in_extension() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 32be4c646..7aa13994b 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -42,7 +42,7 @@ final class FuncCallbackImportTests { func func_callMeFunc_callback() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) @@ -132,7 +132,7 @@ final class FuncCallbackImportTests { func func_callMeMoreFunc_callback() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) @@ -247,7 +247,7 @@ final class FuncCallbackImportTests { func func_withBuffer_body() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index 6cdacab12..b37025313 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -238,7 +238,7 @@ extension FunctionDescriptorTests { ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = logLevel try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) @@ -272,7 +272,7 @@ extension FunctionDescriptorTests { ) throws { var config = Configuration() config.swiftModule = swiftModuleName - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = logLevel try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) diff --git a/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift b/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift index 077db64ce..6b12465ce 100644 --- a/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift +++ b/Tests/JExtractSwiftTests/JExtractFileFilterTests.swift @@ -335,7 +335,7 @@ struct JExtractFileFilterTests { config.swiftModule = "__FakeModule" config.swiftFilterInclude = include config.swiftFilterExclude = exclude - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) translator.log.logLevel = .error try translator.analyze(path: "Fake.swift", text: Self.nestedTypeSource) return translator diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift index 9743dbfaa..ffff38b8b 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -355,7 +355,7 @@ struct JNIEnumTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + 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/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index fce7ed3a7..e4c866e14 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -72,7 +72,7 @@ final class MethodImportTests { func method_helloWorld() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -114,7 +114,7 @@ final class MethodImportTests { func func_globalTakeInt() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -163,7 +163,7 @@ final class MethodImportTests { func func_globalTakeIntLongString() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -209,7 +209,7 @@ final class MethodImportTests { func func_globalReturnClass() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -255,7 +255,7 @@ final class MethodImportTests { func func_globalSwapRawBufferPointer() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -304,7 +304,7 @@ final class MethodImportTests { func method_class_helloMemberFunction() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -349,7 +349,7 @@ final class MethodImportTests { func method_class_makeInt() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -400,7 +400,7 @@ final class MethodImportTests { func class_constructor() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .info try st.analyze(path: "Fake.swift", text: class_interfaceFile) @@ -454,7 +454,7 @@ final class MethodImportTests { func struct_constructor() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .info @@ -509,7 +509,7 @@ final class MethodImportTests { func func_globalReturnAny() throws { var config = Configuration() config.swiftModule = "__FakeModule" - let st = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let st = makeSwiftJavaAnalyzer(config: config) st.log.logLevel = .error try st.analyze(path: "Fake.swift", text: class_interfaceFile) diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index ef4797713..2c4085efe 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -76,7 +76,7 @@ struct SpecializationTests { func multipleSpecializationsProduceDistinctTypes() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) // Both specialized types should be registered @@ -133,7 +133,7 @@ struct SpecializationTests { func specializationEntriesContainAll() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) let baseBox = try #require(translator.extractedTypes["Box"]) @@ -193,7 +193,7 @@ struct SpecializationTests { // Verify observeTheFish does NOT appear inside ToolBox's class body var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) let toolBox = try #require(translator.extractedTypes["ToolBox"]) let methodNames = toolBox.methods.map(\.name) @@ -338,7 +338,7 @@ struct SpecializationTests { func specializeNonGenericTypeThrows() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze( path: "/fake/Fake.swiftinterface", text: """ diff --git a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift index a849b2474..b0e9d5e56 100644 --- a/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift +++ b/Tests/JExtractSwiftTests/TypealiasResolutionTests.swift @@ -49,7 +49,7 @@ struct TypealiasResolutionTests { func primitiveAliasResolvesStructMembers() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) let user = try #require(translator.extractedTypes["TypealiasUser"]) @@ -63,7 +63,7 @@ struct TypealiasResolutionTests { func primitiveAliasResolvesFreeFunc() throws { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: primitiveAliasInput) #expect( @@ -89,7 +89,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let holder = try #require(translator.extractedTypes["Holder"]) @@ -113,7 +113,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "unwrapOrZero" }) @@ -144,7 +144,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "describe" }) @@ -176,7 +176,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let holder = try #require(translator.extractedTypes["Holder"]) @@ -201,7 +201,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + 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 @@ -260,7 +260,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "passA" }) @@ -285,7 +285,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "unwrap" }) @@ -310,7 +310,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "first" }) @@ -344,7 +344,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "add" }) @@ -376,7 +376,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let player = try #require(translator.extractedTypes["Player"]) @@ -401,7 +401,7 @@ struct TypealiasResolutionTests { var config = Configuration() config.swiftModule = "SwiftModule" - let translator = SwiftAnalyzer(config: config, extractDecider: JavaExtractDecider()) + let translator = makeSwiftJavaAnalyzer(config: config) try translator.analyze(path: "/fake/Fake.swift", text: input) let fn = try #require(translator.extractedGlobalFuncs.first { $0.name == "openIntBag" }) From 6f790e5f3139ff67c59e61cc382b5284dac72944 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 15 Jun 2026 15:34:17 +0900 Subject: [PATCH 20/38] SwiftExtract: drop language-specific notes from SwiftAnalyzer doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the parenthetical "(such as honoring Java's `@JavaExport`…)" clause from the type-level doc — that example belongs on `ExtractDecider` (where it already appears) and the type-level doc reads cleaner as a one-liner about analysis output. - Trim two analyze() overload docs that named specific downstream configuration sources by example. The hook description is enough on its own; the analyzer is meant to be language-neutral and the doc shouldn't pin to particular downstream callers. --- Sources/SwiftExtract/SwiftAnalyzer.swift | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index be46b28aa..c9eef342a 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -20,10 +20,6 @@ import SwiftSyntax /// Drives the analysis of Swift source code into an `AnalysisResult` that /// downstream language generators can consume. -/// -/// The analysis output is language-neutral; language-specific extraction rules -/// (such as honoring Java's `@JavaExport` or skipping `@JavaClass`-wrapped -/// types) are layered in via an optional `ExtractDecider` public final class SwiftAnalyzer { static let SWIFT_INTERFACE_SUFFIX = ".swiftinterface" @@ -150,10 +146,9 @@ extension SwiftAnalyzer { /// 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. swift-python's - /// `@Python typealias` post-pass, or a `specialize:` config entry that - /// names a base type by qualified name) can use the hook to call - /// `registerSpecialization(_:outputName:typeArgs:)` so their + /// 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 @@ -287,9 +282,9 @@ extension SwiftAnalyzer { /// 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. `@Python typealias`, - /// `specialize:` config entries) so deferred constrained extensions - /// match against those specializations before they're dropped. + /// 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, From 6db83d9e6e99c47e3db76639693b5dead4a3ca19 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 15 Jun 2026 15:36:35 +0900 Subject: [PATCH 21/38] SwiftExtract: make extractDecider non-optional on SwiftAnalyzer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every language target needs to pick a per-decl extraction policy (access-level-only baseline vs. attribute-aware), so silently falling back to `DefaultExtractDecider` from the `SwiftAnalysisVisitor` was the wrong shape — it hides the choice and lets a downstream caller forget to supply one without compile-time pushback. - `SwiftAnalyzer.extractDecider` and the three constructor / static `analyze` entry points all take `any ExtractDecider` (no default). - `SwiftAnalysisVisitor.shouldExtract`'s `decider:` parameter is no longer optional; the internal fallback to `DefaultExtractDecider` is gone. - `SwiftExtractTests` adds a `Support/TestAnalyze.swift` helper (`analyze(sources:moduleName:config:sourceDependencies:)`) so the 21 existing call sites stay terse — they all want the access-level-only baseline anyway. Test-side migration is purely a rename (`SwiftAnalyzer.analyze` → `analyze`). --- .../SwiftExtract/SwiftAnalysisVisitor.swift | 13 +++--- Sources/SwiftExtract/SwiftAnalyzer.swift | 13 +++--- .../AnalysisResultTests.swift | 30 +++++++------- .../InlineArrayTypeTests.swift | 12 +++--- .../Support/TestAnalyze.swift | 40 +++++++++++++++++++ 5 files changed, 73 insertions(+), 35 deletions(-) create mode 100644 Tests/SwiftExtractTests/Support/TestAnalyze.swift diff --git a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index 2fcf12716..983f4e2c7 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -706,19 +706,16 @@ final class SwiftAnalysisVisitor { } extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { - /// Decide whether this declaration should be extracted + /// Decide whether this declaration should be extracted. /// - /// Delegates entirely to the supplied `decider`, falling back to a - /// `DefaultExtractDecider` (access-level-only) when none was provided. - /// All per-decl policy lives in the decider — see `ExtractDecider` + /// Delegates entirely to the supplied `decider`; all per-decl policy + /// lives there — see `ExtractDecider`. func shouldExtract( config: any SwiftExtractConfiguration, log: Logger, in parent: ExtractedNominalType?, - decider: (any ExtractDecider)? + decider: any ExtractDecider ) -> Bool { - let effective: any ExtractDecider = - decider ?? DefaultExtractDecider(accessLevel: config.swiftExtractAccessLevel) - return effective.shouldExtract(decl: DeclSyntax(self), in: parent, log: log) + decider.shouldExtract(decl: DeclSyntax(self), in: parent, log: log) } } diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index c9eef342a..4a5497c21 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -66,14 +66,15 @@ public final class SwiftAnalyzer { lookupContext?.symbolTable } - /// Optional language-specific extraction decider that can override the - /// built-in access-level filter on a per-decl basis - package let extractDecider: (any ExtractDecider)? + /// Language-specific per-decl extraction policy. Every language target + /// must supply one — pass `DefaultExtractDecider` for the + /// access-level-only baseline. + package let extractDecider: any ExtractDecider public init( config: any SwiftExtractConfiguration, moduleName: String? = nil, - extractDecider: (any ExtractDecider)? = 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 @@ -266,7 +267,7 @@ extension SwiftAnalyzer { moduleName: String, config: (any SwiftExtractConfiguration)? = nil, sourceDependencies: SourceDependencies = SourceDependencies(), - extractDecider: (any ExtractDecider)? = nil + extractDecider: any ExtractDecider ) throws -> AnalysisResult { try analyze( sources: sources, @@ -290,7 +291,7 @@ extension SwiftAnalyzer { moduleName: String, config: (any SwiftExtractConfiguration)? = nil, sourceDependencies: SourceDependencies = SourceDependencies(), - extractDecider: (any ExtractDecider)? = nil, + extractDecider: any ExtractDecider, beforeProcessingDeferredExtensions hook: (SwiftAnalyzer) throws -> Void ) throws -> AnalysisResult { let effectiveConfig = config ?? DefaultSwiftExtractConfiguration(swiftModule: moduleName) diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index 64ff1d3c1..c1d1c51a4 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -26,7 +26,7 @@ struct AnalysisResultSuite { // MARK: Top-level types @Test func topLevelTypesAreRecorded() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -65,7 +65,7 @@ struct AnalysisResultSuite { // MARK: Methods on a type @Test func methodsAreRecordedOnEnclosingType() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -91,7 +91,7 @@ struct AnalysisResultSuite { // MARK: Properties (variables) — getter/setter pair @Test func storedPropertyProducesGetterAndSetter() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -113,7 +113,7 @@ struct AnalysisResultSuite { } @Test func readOnlyPropertyHasOnlyGetter() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -138,7 +138,7 @@ struct AnalysisResultSuite { // MARK: Global functions and variables @Test func globalFunctionLandsInImportedGlobalFuncs() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -157,7 +157,7 @@ struct AnalysisResultSuite { } @Test func globalVariableProducesGetterSetterPair() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -178,7 +178,7 @@ struct AnalysisResultSuite { // MARK: Effect specifiers (throws / async) @Test func effectSpecifiersAreCapturedOnFunctionSignatures() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -214,7 +214,7 @@ struct AnalysisResultSuite { // MARK: Access-level filtering @Test func internalDeclarationsAreNotImportedByDefault() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -246,7 +246,7 @@ struct AnalysisResultSuite { var config = DefaultSwiftExtractConfiguration() config.swiftFilterExclude = ["Skip*"] - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -276,7 +276,7 @@ struct AnalysisResultSuite { // MARK: Generic typealias produces a specialization @Test func genericTypealiasProducesSpecializationEntry() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -302,7 +302,7 @@ struct AnalysisResultSuite { // MARK: Empty input @Test func emptyModuleProducesEmptyResult() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ("/fake/Source.swift", "// nothing here") ], @@ -321,7 +321,7 @@ struct AnalysisResultSuite { /// extracted: the open generic isn't directly constructible, so swift-java /// (and any caller leaving the knob at its default `false`) drops them. @Test func unspecializedGenericInitializersAreSkippedByDefault() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -349,7 +349,7 @@ struct AnalysisResultSuite { var config = DefaultSwiftExtractConfiguration() config.extractsGenericTypeInitializers = true - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -375,7 +375,7 @@ struct AnalysisResultSuite { /// build configuration doesn't know about — the type guarded behind them /// must not appear in the analysis result. @Test func canImportGuardedDeclsAreSkippedWhenModuleNotAvailable() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", @@ -404,7 +404,7 @@ struct AnalysisResultSuite { var config = DefaultSwiftExtractConfiguration() config.availableImportModules = ["MadeUpModule"] - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ( "/fake/Source.swift", diff --git a/Tests/SwiftExtractTests/InlineArrayTypeTests.swift b/Tests/SwiftExtractTests/InlineArrayTypeTests.swift index 7a24d8391..f605c8cf9 100644 --- a/Tests/SwiftExtractTests/InlineArrayTypeTests.swift +++ b/Tests/SwiftExtractTests/InlineArrayTypeTests.swift @@ -25,7 +25,7 @@ struct InlineArrayTypeSuite { // MARK: Parsing the sugar form `[N of T]` @Test func sugarFormIsParsedAsInlineArray() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ("/fake/Source.swift", "public func take(_ a: [3 of Int]) {}") ], @@ -48,7 +48,7 @@ struct InlineArrayTypeSuite { // MARK: Underscore digit separator and radix prefixes @Test func underscoreSeparatedCountIsParsed() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ("/fake/Source.swift", "public func take(_ a: [1_024 of Double]) {}") ], @@ -64,7 +64,7 @@ struct InlineArrayTypeSuite { } @Test func hexCountIsParsed() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ("/fake/Source.swift", "public func take(_ a: [0xA of UInt8]) {}") ], @@ -84,7 +84,7 @@ struct InlineArrayTypeSuite { // MARK: Returns and result types @Test func returnTypeIsParsedAsInlineArray() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ("/fake/Source.swift", "public func get() -> [4 of Float] { fatalError() }") ], @@ -104,7 +104,7 @@ struct InlineArrayTypeSuite { // MARK: Nested inline arrays @Test func nestedInlineArrayIsParsed() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ("/fake/Source.swift", "public func take(_ a: [3 of [4 of Int]]) {}") ], @@ -129,7 +129,7 @@ struct InlineArrayTypeSuite { // MARK: Description (printed form) @Test func descriptionUsesSugarForm() throws { - let result = try SwiftAnalyzer.analyze( + let result = try analyze( sources: [ ("/fake/Source.swift", "public func take(_ a: [3 of Int]) {}") ], diff --git a/Tests/SwiftExtractTests/Support/TestAnalyze.swift b/Tests/SwiftExtractTests/Support/TestAnalyze.swift new file mode 100644 index 000000000..8b62bc72c --- /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 `DefaultExtractDecider` +/// 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, + config: effectiveConfig, + sourceDependencies: sourceDependencies, + extractDecider: DefaultExtractDecider(accessLevel: effectiveConfig.swiftExtractAccessLevel) + ) +} From 4eda915c32cb0a89a3e481759ec251a81b5b0a6a Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 15 Jun 2026 15:38:02 +0900 Subject: [PATCH 22/38] SwiftExtract: rename swiftExtractAccessLevel to effectiveMinimumInputAccessLevelMode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The protocol requirement and swift-java's bridge property were two names for the same value. Aligning on `effectiveMinimumInputAccessLevelMode` (which already exists on `SwiftJavaConfigurationShared.Configuration`) lets `Configuration` conform to `SwiftExtractConfiguration` automatically, dropping the six-line passthrough property in `Configuration+SwiftExtract.swift`. - `SwiftExtractConfiguration.swiftExtractAccessLevel` → `effectiveMinimumInputAccessLevelMode` (protocol requirement + `DefaultSwiftExtractConfiguration` storage). - Drop the swift-java passthrough; the conformance now satisfies via the property already on `Configuration`. - Update the two call sites (`JavaExtractDecider`, `Tests/.../TestAnalyze.swift`). --- .../Configuration+SwiftExtract.swift | 13 +++++-------- Sources/JExtractSwiftLib/JavaExtractDecider.swift | 2 +- .../SwiftExtract/SwiftExtractConfiguration.swift | 6 +++--- Tests/SwiftExtractTests/Support/TestAnalyze.swift | 2 +- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift index a979da6d3..263549673 100644 --- a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -19,15 +19,12 @@ import SwiftJavaConfigurationShared /// `SwiftExtractConfiguration` surface consumed by `SwiftExtract`. /// /// Most members are satisfied directly by `Configuration`'s own properties. -/// `Configuration` shares `AccessLevelMode` with the analyzer (both pull it in -/// from `SwiftExtractConfigurationShared`), so `swiftExtractAccessLevel` is a -/// straight passthrough. Only `swiftExtractLogLevel` needs a mapping from -/// swift-java's `LogLevel` onto the neutral `Logger.Level`. +/// `Configuration` shares `AccessLevelMode` with the analyzer (both pull it +/// in from `SwiftExtractConfigurationShared`), so +/// `effectiveMinimumInputAccessLevelMode` already conforms without a bridge. +/// Only `swiftExtractLogLevel` needs a mapping from swift-java's `LogLevel` +/// onto the neutral `Logger.Level`. extension Configuration: SwiftExtractConfiguration { - public var swiftExtractAccessLevel: AccessLevelMode { - effectiveMinimumInputAccessLevelMode - } - public var swiftExtractLogLevel: SwiftExtract.Logger.Level? { guard let logLevel else { return nil } switch logLevel { diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index 7c5cfc308..30e8b9ac5 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -19,7 +19,7 @@ import SwiftSyntax public func makeSwiftJavaAnalyzer(config: Configuration) -> SwiftAnalyzer { SwiftAnalyzer( config: config, - extractDecider: JavaExtractDecider(accessLevel: config.swiftExtractAccessLevel), + extractDecider: JavaExtractDecider(accessLevel: config.effectiveMinimumInputAccessLevelMode), ) } diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index 4df24040f..0ce020b8e 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -51,7 +51,7 @@ public protocol SwiftExtractConfiguration { var importedModuleStubs: [String: [String]]? { get } /// Minimum access level required for a declaration to be extracted. - var swiftExtractAccessLevel: AccessLevelMode { get } + var effectiveMinimumInputAccessLevelMode: AccessLevelMode { get } /// Verbosity for the analyzer's logger; `nil` falls back to `.info`. var swiftExtractLogLevel: Logger.Level? { get } @@ -115,7 +115,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { public var swiftFilterInclude: [String]? public var swiftFilterExclude: [String]? public var importedModuleStubs: [String: [String]]? - public var swiftExtractAccessLevel: AccessLevelMode + public var effectiveMinimumInputAccessLevelMode: AccessLevelMode public var swiftExtractLogLevel: Logger.Level? public var extractsGenericTypeInitializers: Bool public var availableImportModules: Set @@ -134,7 +134,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { permitsUnresolvedTypeReferences: Bool = false ) { self.swiftModule = swiftModule - self.swiftExtractAccessLevel = accessLevel + self.effectiveMinimumInputAccessLevelMode = accessLevel self.swiftExtractLogLevel = logLevel self.extractsGenericTypeInitializers = extractsGenericTypeInitializers self.staticBuildConfigurationFile = staticBuildConfigurationFile diff --git a/Tests/SwiftExtractTests/Support/TestAnalyze.swift b/Tests/SwiftExtractTests/Support/TestAnalyze.swift index 8b62bc72c..6130ec5fa 100644 --- a/Tests/SwiftExtractTests/Support/TestAnalyze.swift +++ b/Tests/SwiftExtractTests/Support/TestAnalyze.swift @@ -35,6 +35,6 @@ func analyze( moduleName: moduleName, config: effectiveConfig, sourceDependencies: sourceDependencies, - extractDecider: DefaultExtractDecider(accessLevel: effectiveConfig.swiftExtractAccessLevel) + extractDecider: DefaultExtractDecider(accessLevel: effectiveConfig.effectiveMinimumInputAccessLevelMode) ) } From fee125a95fc620b10504651490ea78341fe319a5 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 15 Jun 2026 15:40:06 +0900 Subject: [PATCH 23/38] CodePrinting: add CodePrinter.inlineCommentStyle (.slashSlash | .hash) Hash-comment languages reuse `CodePrinter` for their generated output but need source-location trailers, `printSeparator` banners, and echo-mode (`outputDirectory == "-"`) headers to start with `#` instead of `//`. Add a public `InlineCommentStyle` enum plus a `CodePrinter.inlineCommentStyle` instance flag. - Default is `.slashSlash`, so existing Swift / Java output is byte-identical. - The three internal hard-coded `//` fragments now use `inlineCommentStyle.rawValue`. - `PrinterTerminator.sloc`'s rawValue placeholder is irrelevant to output (the print path special-cases the trailer assembly), left unchanged. - Add a `CodePrintingTests` target with four tests covering the default `//`, `.hash` flip on `.sloc`, `.hash` flip on `printSeparator`, and `emitSourceLocations = false` interaction. --- Package.swift | 7 ++ Sources/CodePrinting/CodePrinter.swift | 34 ++++++++-- .../InlineCommentStyleTests.swift | 67 +++++++++++++++++++ 3 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 Tests/CodePrintingTests/InlineCommentStyleTests.swift diff --git a/Package.swift b/Package.swift index eb47bb7be..18fda7eef 100644 --- a/Package.swift +++ b/Package.swift @@ -496,6 +496,13 @@ let package = Package( ] ), + .testTarget( + name: "CodePrintingTests", + dependencies: [ + "CodePrinting" + ] + ), + .testTarget( name: "SwiftRuntimeFunctionsTests", dependencies: [ diff --git a/Sources/CodePrinting/CodePrinter.swift b/Sources/CodePrinting/CodePrinter.swift index 6aa2926cc..139e272c0 100644 --- a/Sources/CodePrinting/CodePrinter.swift +++ b/Sources/CodePrinting/CodePrinter.swift @@ -32,6 +32,12 @@ public struct CodePrinter: Sendable { /// 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) @@ -174,7 +180,7 @@ public struct CodePrinter: Sendable { if terminator == .sloc { if emitSourceLocations { - append(" // \(function) @ \(file):\(line)\n") + append(" \(inlineCommentStyle.rawValue) \(function) @ \(file):\(line)\n") } else { append("\n") } @@ -191,11 +197,12 @@ public struct CodePrinter: Sendable { public mutating func printSeparator(_ text: String) { assert(!text.contains(where: \.isNewline)) + let lead = inlineCommentStyle.rawValue print( """ - // ==== -------------------------------------------------- - // \(text) + \(lead) ==== -------------------------------------------------- + \(lead) \(text) """ ) @@ -225,6 +232,18 @@ public struct CodePrinter: Sendable { } } +// ==== ----------------------------------------------------------------------- +// 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 @@ -295,19 +314,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/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")) + } +} From cf0681003ba96eb7cd91ce9c6eb54d854af91c72 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 15 Jun 2026 15:42:04 +0900 Subject: [PATCH 24/38] SwiftExtract: rename translator -> analyzer, imported -> extracted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 386a749e rename pass renamed types (`SwiftAnalyzer`, `ExtractedNominalType`, `extractedTypes`, …) but left the in-code identifiers and a few doc strings on the old vocabulary (`translator`, `imported`/`importedType`/`alreadyImported`). Sweep them now so the module reads consistently — the type and its in-code identifier finally agree. - `SwiftAnalysisVisitor.translator` field & `init(translator:)` parameter → `analyzer` / `init(analyzer:)`. - Every `translator.` access in the visitor and analyzer becomes `analyzer.`. - `Logger(label: "translator")` → `Logger(label: "analyzer")`. - `let imported = ExtractedFunc(…)` and friends → `let extracted`. - `importedType` / `importedNominal` / `alreadyImported` / `importedCase` locals → `extractedType` / `extractedNominal` / `alreadyExtracted` / `extractedCase`. - Companion doc-comment cleanups ("imported nominal type representation" → "extracted …", "imported representation" → "extracted …", "Record imported method" → "Record extracted …"). Module-import vocabulary (`importedModules`, `importedModuleStubs`, `importingModules`, `importedModule`) is left alone — those refer to literal Swift `import` statements, not the analyzer's extraction step. --- Sources/SwiftExtract/ExtractedDecls.swift | 2 +- .../SwiftExtract/SwiftAnalysisVisitor.swift | 118 +++++++++--------- Sources/SwiftExtract/SwiftAnalyzer.swift | 40 +++--- 3 files changed, 80 insertions(+), 80 deletions(-) diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index af5011124..7cdae0ae9 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -317,7 +317,7 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible { } /// If this function/method is member of a class/struct/protocol, - /// this will contain that declaration's imported name. + /// 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 diff --git a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index 983f4e2c7..8b6fd4973 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -19,16 +19,16 @@ import SwiftParser import SwiftSyntax final class SwiftAnalysisVisitor { - let translator: SwiftAnalyzer + let analyzer: SwiftAnalyzer var config: any SwiftExtractConfiguration { - self.translator.config + self.analyzer.config } - init(translator: SwiftAnalyzer) { - 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 { @@ -89,7 +89,7 @@ final class SwiftAnalysisVisitor { in parent: ExtractedNominalType?, sourceFilePath: String, ) { - guard let extractedNominalType = translator.extractedNominalType(node, parent: parent) else { + guard let extractedNominalType = analyzer.extractedNominalType(node, parent: parent) else { return } @@ -120,7 +120,7 @@ final class SwiftAnalysisVisitor { // 'extension' in a nominal type is invalid. Ignore return } - guard let extractedNominalType = translator.extractedNominalType(node.extendedType) else { + guard let extractedNominalType = analyzer.extractedNominalType(node.extendedType) else { return } @@ -135,7 +135,7 @@ final class SwiftAnalysisVisitor { // The extension is unconstrained: add to the base type (visible through all specializations) 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: extractedNominalType, sourceFilePath: sourceFilePath) @@ -179,7 +179,7 @@ final class SwiftAnalysisVisitor { in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -190,30 +190,30 @@ final class SwiftAnalysisVisitor { 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 = ExtractedFunc( - 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.extractedGlobalFuncs.append(imported) + analyzer.extractedGlobalFuncs.append(extracted) } } @@ -231,25 +231,25 @@ final class SwiftAnalysisVisitor { 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 = ExtractedFunc( - module: translator.swiftModuleName, + module: analyzer.swiftModuleName, swiftDecl: node, name: caseName, apiKind: .enumCase, functionSignature: signature, ) - let importedCase = ExtractedEnumCase( + let extractedCase = ExtractedEnumCase( name: caseName, parameters: parameters ?? [], swiftDecl: node, @@ -257,12 +257,12 @@ final class SwiftAnalysisVisitor { 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)" ) ) } @@ -273,7 +273,7 @@ final class SwiftAnalysisVisitor { in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -306,7 +306,7 @@ final class SwiftAnalysisVisitor { } 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)" ) ) } @@ -320,7 +320,7 @@ final class SwiftAnalysisVisitor { self.log.info("Initializer must be within a current type; \(node)") return } - guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -338,32 +338,32 @@ final class SwiftAnalysisVisitor { 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 = ExtractedFunc( - 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: ExtractedNominalType?, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext, decider: translator.extractDecider) else { + guard node.shouldExtract(config: config, log: log, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -394,7 +394,7 @@ final class SwiftAnalysisVisitor { } 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)" ) ) } @@ -405,7 +405,7 @@ final class SwiftAnalysisVisitor { 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): @@ -438,22 +438,22 @@ final class SwiftAnalysisVisitor { 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 = ExtractedFunc( - module: translator.swiftModuleName, + let extracted = ExtractedFunc( + module: analyzer.swiftModuleName, swiftDecl: node, name: name, apiKind: kind, @@ -461,12 +461,12 @@ final class SwiftAnalysisVisitor { ) 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.extractedGlobalVariables.append(imported) + analyzer.extractedGlobalVariables.append(extracted) } } @@ -474,31 +474,31 @@ final class SwiftAnalysisVisitor { enumDecl node: EnumDeclSyntax, in parent: ExtractedNominalType?, ) { - guard let imported = translator.extractedNominalType(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) } } } @@ -527,7 +527,7 @@ final class SwiftAnalysisVisitor { guard !genericArgs.isEmpty else { return } // Resolve the base type through the symbol table - guard let baseType = translator.extractedNominalType(rhsType) else { + guard let baseType = analyzer.extractedNominalType(rhsType) else { log.debug("Could not resolve base type for specialization: \(rhsType.trimmedDescription)") return } @@ -565,7 +565,7 @@ final class SwiftAnalysisVisitor { log.warning("Failed to specialize \(baseType.baseTypeName) as \(outputName): \(error)") return } - translator.specializations[baseType, default: []].insert(specialized) + analyzer.specializations[baseType, default: []].insert(specialized) log.info("Registered specialization: \(outputName) = \(rhsDescription)") } @@ -573,13 +573,13 @@ final class SwiftAnalysisVisitor { // MARK: Specialization support /// Apply specializations to a type if matching entries exist - func applySpecialization(to importedType: ExtractedNominalType) { - 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.extractedTypes[specialized.effectiveOutputName] = specialized + analyzer.extractedTypes[specialized.effectiveOutputName] = specialized log.info("Applied specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } @@ -587,19 +587,19 @@ final class SwiftAnalysisVisitor { /// 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.extractedTypes[specialized.effectiveOutputName] != nil { + if analyzer.extractedTypes[specialized.effectiveOutputName] != nil { continue } - translator.extractedTypes[specialized.effectiveOutputName] = specialized + analyzer.extractedTypes[specialized.effectiveOutputName] = specialized log.info("Applied pending specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") } } // Process constrained extensions that were deferred for deferred in deferredConstrainedExtensions { - guard let baseType = translator.extractedNominalType(deferred.node.extendedType) else { + guard let baseType = analyzer.extractedNominalType(deferred.node.extendedType) else { continue } let matchingSpecializations = findMatchingSpecializations( @@ -664,7 +664,7 @@ final class SwiftAnalysisVisitor { extendedType: ExtractedNominalType, whereConstraints: [ParsedWhereConstraint], ) -> [ExtractedNominalType] { - guard let specializations = translator.specializations[extendedType] else { + guard let specializations = analyzer.specializations[extendedType] else { return [] } return specializations.filter { specialized in @@ -689,10 +689,10 @@ final class SwiftAnalysisVisitor { guard let concreteName = specialized.genericArguments[typeParam] else { return false } - guard let concreteType = translator.extractedTypes[concreteName] else { + guard let concreteType = analyzer.extractedTypes[concreteName] else { return false } - guard concreteType.conformsTo(proto, in: translator.extractedTypes) else { + guard concreteType.conformsTo(proto, in: analyzer.extractedTypes) else { return false } } diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 4a5497c21..3e83dba97 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -53,7 +53,7 @@ public final class SwiftAnalyzer { package var extractedGlobalFuncs: [ExtractedFunc] = [] - /// A mapping from Swift type names (e.g., A.B) over to the imported nominal + /// A mapping from Swift type names (e.g., A.B) over to the extracted nominal /// type representation. package var extractedTypes: [String: ExtractedNominalType] = [:] @@ -79,7 +79,7 @@ public final class SwiftAnalyzer { guard let swiftModule = moduleName ?? config.swiftModule else { fatalError("Missing 'swiftModule' name.") // FIXME: can we make it required in config? but we shared config for many cases } - self.log = Logger(label: "translator", logLevel: config.swiftExtractLogLevel ?? .info) + self.log = Logger(label: "analyzer", logLevel: config.swiftExtractLogLevel ?? .info) self.config = config self.swiftModuleName = swiftModule self.extractDecider = extractDecider @@ -159,7 +159,7 @@ extension SwiftAnalyzer { ) throws { prepareForTranslation() - let visitor = SwiftAnalysisVisitor(translator: self) + let visitor = SwiftAnalysisVisitor(analyzer: self) for input in self.inputs { log.trace("Analyzing \(input.path)") @@ -295,13 +295,13 @@ extension SwiftAnalyzer { beforeProcessingDeferredExtensions hook: (SwiftAnalyzer) throws -> Void ) throws -> AnalysisResult { let effectiveConfig = config ?? DefaultSwiftExtractConfiguration(swiftModule: moduleName) - let translator = SwiftAnalyzer(config: effectiveConfig, moduleName: moduleName, extractDecider: extractDecider) - translator.sourceDependencies = sourceDependencies + let analyzer = SwiftAnalyzer(config: effectiveConfig, moduleName: moduleName, extractDecider: extractDecider) + analyzer.sourceDependencies = sourceDependencies for source in sources { - translator.add(filePath: source.path, text: source.text) + analyzer.add(filePath: source.path, text: source.text) } - try translator.analyze(beforeProcessingDeferredExtensions: hook) - return translator.result + try analyzer.analyze(beforeProcessingDeferredExtensions: hook) + return analyzer.result } private func visitFoundationDeclsIfNeeded(with visitor: SwiftAnalysisVisitor) { @@ -383,7 +383,7 @@ extension SwiftAnalyzer { self.config.permitsUnresolvedTypeReferences } - /// Check if any of the imported decls uses a nominal declaration that satisfies + /// 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 { @@ -426,14 +426,14 @@ extension SwiftAnalyzer { if self.extractedGlobalVariables.contains(where: check) { return true } - for importedType in self.extractedTypes.values { - if importedType.initializers.contains(where: check) { + for extractedType in self.extractedTypes.values { + if extractedType.initializers.contains(where: check) { return true } - if importedType.methods.contains(where: check) { + if extractedType.methods.contains(where: check) { return true } - if importedType.variables.contains(where: check) { + if extractedType.variables.contains(where: check) { return true } } @@ -444,7 +444,7 @@ extension SwiftAnalyzer { // ==== ---------------------------------------------------------------------------------------------------------------- // MARK: Type translation extension SwiftAnalyzer { - /// Try to resolve the given nominal declaration node into its imported representation. + /// Try to resolve the given nominal declaration node into its extracted representation. func extractedNominalType( _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, parent: ExtractedNominalType?, @@ -459,7 +459,7 @@ extension SwiftAnalyzer { return self.extractedNominalType(nominal) } - /// Try to resolve the given nominal type node into its imported representation. + /// Try to resolve the given nominal type node into its extracted representation. func extractedNominalType( _ typeNode: TypeSyntax ) -> ExtractedNominalType? { @@ -492,14 +492,14 @@ extension SwiftAnalyzer { return nil } - if let alreadyImported = extractedTypes[fullName] { - return alreadyImported + if let alreadyExtracted = extractedTypes[fullName] { + return alreadyExtracted } - let importedNominal = try? ExtractedNominalType(swiftNominal: nominal, lookupContext: lookupContext) + let extractedNominal = try? ExtractedNominalType(swiftNominal: nominal, lookupContext: lookupContext) - extractedTypes[fullName] = importedNominal - return importedNominal + extractedTypes[fullName] = extractedNominal + return extractedNominal } } From ea82caa3f90f2c38b86e3f121a38dae083f04c77 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 15 Jun 2026 16:36:02 +0900 Subject: [PATCH 25/38] SwiftExtract: drop extractsGenericTypeInitializers, fold into JavaExtractDecider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'per-decl extraction policy belongs to each ExtractDecider' refactor left this Bool on SwiftExtractConfiguration as a side-channel that the visitor read *after* the decider had already approved the decl: guard node.shouldExtract(... decider: analyzer.extractDecider) else { return } if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization && !config.extractsGenericTypeInitializers { return } That contradicts the invariant the refactor was about. Move the rule into JavaExtractDecider — the decider already receives 'parent: ExtractedNominalType?' which exposes 'swiftNominal.isGeneric' and 'isSpecialization', so the check fits naturally there: if let parent, decl.is(InitializerDeclSyntax.self), parent.swiftNominal.isGeneric, !parent.isSpecialization { log.trace("Skip ...: initializer of an unspecialized generic type") return false } Placed before the @JavaExport force-include path so an explicit export on a generic init still gets dropped — matches today's flag-based behavior, where the visitor's gate runs after the decider's @JavaExport short-circuit and drops it anyway. Other targets that DO want generic-base initializers (swift-python's specialization pipeline clones them onto each typealias) get them by default now: their decider doesn't add the skip rule, so the visitor sees the init and records it. Removed: - SwiftAnalysisVisitor.swift gate at lines 327-329 - SwiftExtractConfiguration.extractsGenericTypeInitializers protocol member + DefaultSwiftExtractConfiguration stored property + init param - JExtractSwiftLib/Configuration+SwiftExtract override Tests: - AnalysisResultTests: rewrote 'unspecializedGenericInitializersAreSkippedByDefault' + 'extractsGenericTypeInitializersKeepsBaseInitializers' into a single 'unspecializedGenericInitializersFlowThroughByDefault' that asserts the neutral analyzer + DefaultAccessLevelExtractDecider keeps base inits. - SpecializationTests: added 'javaDeciderDropsBaseGenericInitializers' locking in JavaExtractDecider's new rule. --- .../Configuration+SwiftExtract.swift | 3 -- .../JExtractSwiftLib/JavaExtractDecider.swift | 18 ++++++++- .../SwiftExtract/SwiftAnalysisVisitor.swift | 17 +++----- .../SwiftExtractConfiguration.swift | 11 ----- .../SpecializationTests.swift | 15 +++++++ .../AnalysisResultTests.swift | 40 ++++--------------- 6 files changed, 44 insertions(+), 60 deletions(-) diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift index 263549673..a5b4c1be0 100644 --- a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -37,9 +37,6 @@ extension Configuration: SwiftExtractConfiguration { case .critical: return .critical } } - - // swift-java targets Java, which cannot construct open generic types directly - public var extractsGenericTypeInitializers: Bool { false } } extension LogLevel { diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index 30e8b9ac5..1955c4e06 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -27,6 +27,10 @@ public func makeSwiftJavaAnalyzer(config: Configuration) -> SwiftAnalyzer { /// /// In addition to the configured access-level filter, the Java target: /// +/// - Skips initializers of unspecialized generic types — Java has no way to +/// construct an open generic, so `init` on a ``-parameterized type is +/// only useful once a specific `T` is fixed. swift-java doesn't yet model +/// such specializations, so the base type's initializers are dropped /// - Force-extracts decls annotated `@JavaExport` even if they would /// otherwise be filtered by access level /// - Skips Swift wrappers of Java types (`@JavaClass`, `@JavaInterface`, @@ -46,6 +50,19 @@ public struct JavaExtractDecider: ExtractDecider { in parent: ExtractedNominalType?, log: Logger ) -> Bool { + // Initializers of an unspecialized generic type can't be constructed from + // Java — drop them regardless of attribute or access level. Runs before + // the `@JavaExport` force-include path because an explicit export still + // doesn't help: there's no concrete type to instantiate. + 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 @@ -67,7 +84,6 @@ public struct JavaExtractDecider: ExtractDecider { } guard let mod = decl.asProtocol((any WithModifiersSyntax).self) else { - log.trace("Skip '\(decl.qualifiedNameForDebug)': not a modifier-bearing decl") return false } let ok = mod.passesAccessLevel(accessLevel, in: parent) diff --git a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index 8b6fd4973..f8bbf4468 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -324,13 +324,6 @@ final class SwiftAnalysisVisitor { return } - if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization - && !config.extractsGenericTypeInitializers - { - log.debug("Skip Importing generic type initializer \(node.kind) '\(node.qualifiedNameForDebug)'") - return - } - self.log.debug("Import initializer: \(node.kind) '\(node.qualifiedNameForDebug)'") let signature: SwiftFunctionSignature @@ -579,8 +572,8 @@ final class SwiftAnalysisVisitor { } for specialized in specializations { - analyzer.extractedTypes[specialized.effectiveOutputName] = specialized - log.info("Applied specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") + analyzer.extractedTypes[specialized.effectiveTypeName] = specialized + log.info("Applied specialization: \(specialized.effectiveTypeName) -> \(specialized.effectiveSwiftTypeName)") } } @@ -589,11 +582,11 @@ final class SwiftAnalysisVisitor { func applyPendingSpecializations() { for (_, specializations) in analyzer.specializations { for specialized in specializations { - if analyzer.extractedTypes[specialized.effectiveOutputName] != nil { + if analyzer.extractedTypes[specialized.effectiveTypeName] != nil { continue } - analyzer.extractedTypes[specialized.effectiveOutputName] = specialized - log.info("Applied pending specialization: \(specialized.effectiveOutputName) -> \(specialized.effectiveSwiftTypeName)") + analyzer.extractedTypes[specialized.effectiveTypeName] = specialized + log.info("Applied pending specialization: \(specialized.effectiveTypeName) -> \(specialized.effectiveSwiftTypeName)") } } diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index 0ce020b8e..634ede026 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -56,14 +56,6 @@ public protocol SwiftExtractConfiguration { /// Verbosity for the analyzer's logger; `nil` falls back to `.info`. var swiftExtractLogLevel: Logger.Level? { get } - /// Whether to extract initializers of *generic* nominal types even when they - /// are not (yet) specialized. swift-java skips these by default (an open - /// generic isn't directly constructible); other language code generators that - /// specialize generics in a post-analysis pass set this `true` so the base - /// type's initializers are available to clone onto the specialization. - /// Default: false. - var extractsGenericTypeInitializers: Bool { get } - /// Module names that should be treated as importable when resolving /// `#if canImport()` conditions, in addition to whatever the build /// configuration already knows. Lets a target opt-in to extracting code @@ -117,7 +109,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { public var importedModuleStubs: [String: [String]]? public var effectiveMinimumInputAccessLevelMode: AccessLevelMode public var swiftExtractLogLevel: Logger.Level? - public var extractsGenericTypeInitializers: Bool public var availableImportModules: Set public var permitsUnresolvedTypeReferences: Bool @@ -125,7 +116,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { swiftModule: String? = nil, accessLevel: AccessLevelMode = .public, logLevel: Logger.Level? = nil, - extractsGenericTypeInitializers: Bool = false, staticBuildConfigurationFile: String? = nil, swiftFilterInclude: [String]? = nil, swiftFilterExclude: [String]? = nil, @@ -136,7 +126,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { self.swiftModule = swiftModule self.effectiveMinimumInputAccessLevelMode = accessLevel self.swiftExtractLogLevel = logLevel - self.extractsGenericTypeInitializers = extractsGenericTypeInitializers self.staticBuildConfigurationFile = staticBuildConfigurationFile self.swiftFilterInclude = swiftFilterInclude self.swiftFilterExclude = swiftFilterExclude diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index 2c4085efe..04b9c6823 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -129,6 +129,21 @@ struct SpecializationTests { #expect(toolCountFunc.parentType?.description == "ToolBox") } + /// Java cannot construct an open generic, so `JavaExtractDecider` drops + /// initializers on an unspecialized base type. + @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() diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index c1d1c51a4..703e27ef6 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -317,10 +317,13 @@ struct AnalysisResultSuite { // ==== ----------------------------------------------------------------------- // MARK: Configuration knobs - /// By default, initializers on an unspecialized generic type are NOT - /// extracted: the open generic isn't directly constructible, so swift-java - /// (and any caller leaving the knob at its default `false`) drops them. - @Test func unspecializedGenericInitializersAreSkippedByDefault() throws { + /// `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: [ ( @@ -336,35 +339,6 @@ struct AnalysisResultSuite { moduleName: "Aquarium" ) - let tank = try #require(result.extractedTypes["Tank"]) - #expect(tank.swiftNominal.isGeneric) - #expect(!tank.isSpecialization) - #expect(tank.initializers.isEmpty) - } - - /// Opting into `extractsGenericTypeInitializers` keeps the base type's - /// initializers in the analysis result so post-analysis specializers can - /// clone them onto a concrete `Tank`. - @Test func extractsGenericTypeInitializersKeepsBaseInitializers() throws { - var config = DefaultSwiftExtractConfiguration() - config.extractsGenericTypeInitializers = true - - let result = try analyze( - sources: [ - ( - "/fake/Source.swift", - """ - public struct Tank { - public init() {} - public init(capacity: Int) {} - } - """ - ) - ], - moduleName: "Aquarium", - config: config - ) - let tank = try #require(result.extractedTypes["Tank"]) #expect(tank.swiftNominal.isGeneric) #expect(!tank.isSpecialization) From 2c0e6952a93ab72439959832d91159ff0296555a Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 15 Jun 2026 16:48:46 +0900 Subject: [PATCH 26/38] SwiftExtract: rename effectiveOutputName -> effectiveTypeName, DefaultExtractDecider -> DefaultAccessLevelExtractDecider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'output' qualifier on ExtractedNominalType.effectiveOutputName / effectiveOutputSimpleName conflated two roles: the Swift-side registration key in the analyzer's type table (which is purely language-neutral — 'FishBox' for a 'typealias FishBox = Box', 'Box' for the base) and the downstream language-output-facing class name (which lives on the code generator). Rename the neutral one to .effectiveTypeName, drop the now-redundant .effectiveOutputSimpleName in favor of computing the simple name on-site (specializedTypeName ?? swiftNominal.name) where it's actually used. JExtractSwiftLib's Java-facing aliases get explicit doc comments and the .effectiveJavaSimpleName / .effectiveJavaName accessors are spelled out instead of bouncing off the (now removed) neutral helpers. Rename DefaultExtractDecider -> DefaultAccessLevelExtractDecider so the role is in the name: it's the access-level-only baseline, not a catch-all default. SwiftAnalyzer's doc comment, TestAnalyze, and the ExtractDecider doc comment all updated to match. Trim a couple of doc comments that just paraphrased the @Test description or the function name. --- .../ExtractedDecls+JavaNaming.swift | 21 ++++++++++++++----- .../JExtractSwiftLib/JavaExtractDecider.swift | 9 ++------ Sources/SwiftExtract/ExtractDecider.swift | 9 ++++---- Sources/SwiftExtract/ExtractedDecls.swift | 13 ++++++------ Sources/SwiftExtract/SwiftAnalyzer.swift | 12 +++++------ .../SpecializationTests.swift | 2 -- .../Support/TestAnalyze.swift | 4 ++-- 7 files changed, 36 insertions(+), 34 deletions(-) diff --git a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift index d8de44734..68965ce14 100644 --- a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift +++ b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift @@ -21,13 +21,24 @@ package typealias JavaClassName = String package typealias JavaFullyQualifiedClassName = String package typealias JavaPackageName = String -// ==== ----------------------------------------------------------------------- -// MARK: Java-facing name aliases for ExtractedNominalType - 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 } - package var effectiveJavaName: String { effectiveOutputName } - package var effectiveJavaSimpleName: String { effectiveOutputSimpleName } + + /// 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 } } diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index 1955c4e06..ae2fac57a 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -27,10 +27,7 @@ public func makeSwiftJavaAnalyzer(config: Configuration) -> SwiftAnalyzer { /// /// In addition to the configured access-level filter, the Java target: /// -/// - Skips initializers of unspecialized generic types — Java has no way to -/// construct an open generic, so `init` on a ``-parameterized type is -/// only useful once a specific `T` is fixed. swift-java doesn't yet model -/// such specializations, so the base type's initializers are dropped +/// - 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`, @@ -51,9 +48,7 @@ public struct JavaExtractDecider: ExtractDecider { log: Logger ) -> Bool { // Initializers of an unspecialized generic type can't be constructed from - // Java — drop them regardless of attribute or access level. Runs before - // the `@JavaExport` force-include path because an explicit export still - // doesn't help: there's no concrete type to instantiate. + // Java — drop them regardless of attribute or access level. if let parent, decl.is(InitializerDeclSyntax.self), parent.swiftNominal.isGeneric, diff --git a/Sources/SwiftExtract/ExtractDecider.swift b/Sources/SwiftExtract/ExtractDecider.swift index 47dc7e9af..cbee50fbf 100644 --- a/Sources/SwiftExtract/ExtractDecider.swift +++ b/Sources/SwiftExtract/ExtractDecider.swift @@ -14,7 +14,7 @@ import SwiftSyntax -/// Per-decl extraction policy for a downstream language target +/// 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 @@ -23,8 +23,8 @@ import SwiftSyntax /// (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 -/// `DefaultExtractDecider`, which only enforces the configured access -/// level +/// `DefaultAccessLevelExtractDecider`, which only enforces the configured +/// access level public protocol ExtractDecider { /// Decide whether `decl` should be extracted. /// @@ -42,7 +42,7 @@ public protocol ExtractDecider { /// Minimal `ExtractDecider` that enforces only the configured access-level /// filter. Used by `SwiftAnalyzer` when no decider is supplied -public struct DefaultExtractDecider: ExtractDecider { +public struct DefaultAccessLevelExtractDecider: ExtractDecider { public let accessLevel: AccessLevelMode public init(accessLevel: AccessLevelMode) { @@ -55,7 +55,6 @@ public struct DefaultExtractDecider: ExtractDecider { log: Logger ) -> Bool { guard let mod = decl.asProtocol((any WithModifiersSyntax).self) else { - log.trace("Skip '\(decl.qualifiedNameForDebug)': not a modifier-bearing decl") return false } let ok = mod.passesAccessLevel(accessLevel, in: parent) diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index 7cdae0ae9..0407f2b3d 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -128,16 +128,15 @@ public final class ExtractedNominalType: ExtractedSwiftDecl { return swiftNominal.qualifiedTypeName } - /// The effective output-facing name — "FishBox" for specialized, "Box" for base - public var effectiveOutputName: String { + /// 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 simple output-facing class name (no qualification) for file naming purposes - public var effectiveOutputSimpleName: String { - specializedTypeName ?? swiftNominal.name - } - /// The Swift type for thunk generation — "Box" for specialized, "Box" for base /// Computed from baseTypeName + genericArguments public var effectiveSwiftTypeName: String { diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 3e83dba97..09aefcff1 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -67,7 +67,7 @@ public final class SwiftAnalyzer { } /// Language-specific per-decl extraction policy. Every language target - /// must supply one — pass `DefaultExtractDecider` for the + /// must supply one — pass `DefaultAccessLevelExtractDecider` for the /// access-level-only baseline. package let extractDecider: any ExtractDecider @@ -250,14 +250,14 @@ extension SwiftAnalyzer { return base } - /// The set of effective output names of every specialization currently - /// registered with the analyzer (across all base types). Useful for - /// callers driving `registerSpecialization` from a hook to skip names + /// 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 output name. + /// with the same effective name. public var registeredSpecializationNames: Set { - Set(self.specializations.values.flatMap { $0 }.map(\.effectiveOutputName)) + Set(self.specializations.values.flatMap { $0 }.map(\.effectiveTypeName)) } /// Top-level convenience: run analysis on the given Swift sources and return diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift index 04b9c6823..bfdff6d02 100644 --- a/Tests/JExtractSwiftTests/SpecializationTests.swift +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -129,8 +129,6 @@ struct SpecializationTests { #expect(toolCountFunc.parentType?.description == "ToolBox") } - /// Java cannot construct an open generic, so `JavaExtractDecider` drops - /// initializers on an unspecialized base type. @Test("JavaExtractDecider drops initializers on the unspecialized generic base") func javaDeciderDropsBaseGenericInitializers() throws { var config = Configuration() diff --git a/Tests/SwiftExtractTests/Support/TestAnalyze.swift b/Tests/SwiftExtractTests/Support/TestAnalyze.swift index 6130ec5fa..115eb0c9e 100644 --- a/Tests/SwiftExtractTests/Support/TestAnalyze.swift +++ b/Tests/SwiftExtractTests/Support/TestAnalyze.swift @@ -18,7 +18,7 @@ import SwiftExtract // MARK: Test analysis helpers /// Drive `SwiftAnalyzer.analyze` from the test suite without spelling out an -/// `extractDecider:` argument every time. Builds a `DefaultExtractDecider` +/// `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 @@ -35,6 +35,6 @@ func analyze( moduleName: moduleName, config: effectiveConfig, sourceDependencies: sourceDependencies, - extractDecider: DefaultExtractDecider(accessLevel: effectiveConfig.effectiveMinimumInputAccessLevelMode) + extractDecider: DefaultAccessLevelExtractDecider(accessLevel: effectiveConfig.effectiveMinimumInputAccessLevelMode) ) } From 8bc45247d7a7aad01a21ee27bdb5f02a159b40bc Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 15 Jun 2026 17:20:48 +0900 Subject: [PATCH 27/38] SwiftExtract: replace __SwiftExtractSynthesized magic module with isUnresolvedTypePlaceholder flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'is this a synthetic placeholder?' bit was being smuggled through the type's moduleName as the sentinel string '__SwiftExtractSynthesized'. Two problems with that: (1) the marker is a string sentinel embedded in semantic data, so anything that prints, logs, or compares moduleName has to know to special-case it; (2) it conflates two unrelated dimensions — 'what module declared this type' (real Swift semantics) vs. 'did SwiftExtract synthesize this stand-in because the symbol table couldn't resolve the name' (analyzer bookkeeping). Replace with an explicit Bool on SwiftNominalTypeDeclaration: /// 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… public let isUnresolvedTypePlaceholder: Bool The doc comment carries the long-form motivation (the canonical associated-type-in-protocol-requirement example, plus pre-specialization generic parameters and externally-bridged simple-name types). The SwiftType-level computed property is a thin pass-through: extension SwiftType { public var isUnresolvedTypePlaceholder: Bool { asNominalTypeDeclaration?.isUnresolvedTypePlaceholder ?? false } } SwiftSyntheticTypes.unresolvedNominal stamps the synthetic decl with isUnresolvedTypePlaceholder: true and an empty moduleName — the honest answer to 'what module declared this?'. The moduleName: parameter on unresolvedNominal goes away (no caller passed a non-default value). The public static syntheticModuleName constant goes away with it. Tests/SwiftExtractTests/SwiftSyntheticTypesTests.swift covers the round-trip: unresolvedNominal('Element') has isUnresolvedTypePlaceholder == true and renders as 'Element' (no module-name leak); a real source-derived nominal has isUnresolvedTypePlaceholder == false. --- .../SwiftExtractConfiguration.swift | 15 +++--- .../SwiftNominalTypeDeclaration.swift | 30 +++++++++++ .../SwiftTypes/SwiftSyntheticTypes.swift | 30 +++++------ .../SwiftExtract/SwiftTypes/SwiftType.swift | 12 ++++- .../SwiftSyntheticTypesTests.swift | 52 +++++++++++++++++++ 5 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 Tests/SwiftExtractTests/SwiftSyntheticTypesTests.swift diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index 634ede026..858a96109 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -74,14 +74,13 @@ public protocol SwiftExtractConfiguration { /// Java/JNI, where the generator can't render code referencing an /// unresolved Swift type. /// - /// Other language code generators that treat unresolved names *symbolically* - /// (e.g. associated types in a protocol requirement before carrier - /// substitution; a property type that names a generic parameter to be - /// replaced during specialization; an external type the user is expected - /// to bridge by simple name) can opt-in by setting this `true`. Unresolved - /// names then become synthetic nominal types via - /// `SwiftSyntheticTypes.unresolvedNominal(_:)` so downstream passes can - /// substitute or recognize them. Default: false. + /// 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 permitsUnresolvedTypeReferences: Bool { get } /// Whether the given module name has stub declarations configured. diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift index b6f6399e8..45cd45696 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -75,6 +75,34 @@ public class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { /// The generic parameters of this nominal type. 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[. public private(set) lazy var knownTypeKind: SwiftKnownTypeDeclKind? = { @@ -89,9 +117,11 @@ public class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { 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) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift b/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift index b4b6462ed..90fd7991d 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftSyntheticTypes.swift @@ -20,27 +20,20 @@ import SwiftSyntax // // `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 (associated types, -// pre-substitution generic parameters, externally-bridged types) — 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 +// 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 { - /// A synthetic module name used for nominals minted by - /// `unresolvedNominal(_:)`. Placed in the type's `moduleName` so downstream - /// code can recognize and route around them when needed. - public static let syntheticModuleName = "__SwiftExtractSynthesized" - /// Build an unresolved nominal `SwiftType` for the given simple type name. - /// - /// Useful when the caller is willing to treat a name as a placeholder to be - /// substituted (or recognized symbolically) by a later pass — e.g. an - /// associated type referenced before carrier substitution, or a generic - /// parameter referenced before specialization. + /// 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, - moduleName: String = SwiftSyntheticTypes.syntheticModuleName + _ name: String ) -> SwiftType { let source = Parser.parse(source: "struct \(name) {}") // Fall back gracefully if `name` isn't a simple identifier (the parsed @@ -56,9 +49,10 @@ public enum SwiftSyntheticTypes { let decl = SwiftNominalTypeDeclaration( name: name, sourceFilePath: "", - moduleName: moduleName, + moduleName: "", parent: nil, - node: structSyntax + node: structSyntax, + isUnresolvedTypePlaceholder: true ) return .nominal(SwiftNominalType(nominalTypeDecl: decl)) } diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 4e6517522..9ce66d14f 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -77,6 +77,14 @@ public enum SwiftType: Equatable { 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. public var isVoid: Bool { switch self { @@ -502,8 +510,8 @@ extension SwiftType { } guard let typeDecl else { // Lenient mode (opt-in via SwiftExtractConfiguration.permitsUnresolvedTypeReferences): - // synthesize an unresolved nominal so a downstream pass can substitute - // or recognize it. Generic-argument names are kept (so e.g. + // 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.permitsUnresolvedTypeReferences { 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) + } +} From 0a9d178fbf18fbffad5b38a8d8f4cc4b73a375ad Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 15:59:15 +0900 Subject: [PATCH 28/38] CodePrinting: CodePrinter init takes optional inlineCommentStyle Lets a downstream caller spell its choice at construction time instead of constructing then mutating: CodePrinter(inlineCommentStyle: .hash) Default stays .slashSlash so existing call sites are unchanged. --- Sources/CodePrinting/CodePrinter.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/CodePrinting/CodePrinter.swift b/Sources/CodePrinting/CodePrinter.swift index 139e272c0..4d91c884b 100644 --- a/Sources/CodePrinting/CodePrinter.swift +++ b/Sources/CodePrinting/CodePrinter.swift @@ -65,8 +65,12 @@ public struct CodePrinter: 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) { From 9d08e3d76720e52ca8c4a9cd5282835f5a947bdd Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 15:59:38 +0900 Subject: [PATCH 29/38] SwiftExtract: move LogLevel into SwiftExtractConfigurationShared, drop bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LogLevel was duplicated as Logger.Level in SwiftExtract and as a parallel LogLevel in SwiftJavaConfigurationShared, glued together by two switch statements in JExtractSwiftLib/Configuration+SwiftExtract.swift. Same-shape enum, two case-by-case bridges — exactly the conflation AccessLevelMode already retired by living in SwiftExtractConfigurationShared. Apply the same pattern to LogLevel. - Sources/SwiftExtractConfigurationShared/LogLevel.swift (new) holds the canonical enum with Codable, ExpressibleByStringLiteral, Comparable conformances. The plugin symlink at Plugins/PluginsShared/SwiftExtractConfigurationShared/ picks it up automatically alongside AccessLevelMode.swift. - SwiftExtract/Logger.swift drops the nested Logger.Level enum and uses LogLevel via @_exported import — the analyzer's API surface is now Logger(label:logLevel: LogLevel). - SwiftJavaConfigurationShared/Configuration.swift drops its own LogLevel enum + Codable extensions; the @_exported import of SwiftExtractConfigurationShared brings the shared one in. - SwiftExtractConfiguration's protocol requirement renames from swiftExtractLogLevel: Logger.Level? to logLevel: LogLevel? — the swiftExtract-prefix existed to dodge a name collision against Configuration.logLevel that no longer exists (both sides reference the same type now), so Configuration: SwiftExtractConfiguration is satisfied by the stored property without any bridge code. - JExtractSwiftLib/Configuration+SwiftExtract.swift collapses to an empty conformance declaration (`extension Configuration: SwiftExtractConfiguration {}`). The two switch statements (LogLevel <-> Logger.Level) are gone. - SwiftJavaTool: CommonOptions.logLevel and the SwiftJavaBaseAsyncParsableCommand protocol's logLevel both type-annotate LogLevel directly; the override-from-CLI line collapses from `config.logLevel = LogLevel(command.logLevel)` to `config.logLevel = command.logLevel`. - Logger+ArgumentParser.swift's ExpressibleByArgument conformance moves from Logger.Level to LogLevel. - Tests: FunctionDescriptorImportTests' two helper-default annotations rename Logger.Level -> LogLevel. Net -118 lines (one duplicate enum + two switch bridges deleted), and the Configuration -> SwiftExtractConfiguration conformance is now an empty declaration instead of two case-by-case mappings. --- .../Configuration+SwiftExtract.swift | 47 ++--------- .../Logger+ArgumentParser.swift | 2 +- Sources/SwiftExtract/Logger.swift | 48 +---------- Sources/SwiftExtract/SwiftAnalyzer.swift | 2 +- .../SwiftExtractConfiguration.swift | 20 ++--- .../LogLevel.swift | 84 +++++++++++++++++++ .../Configuration.swift | 46 ---------- Sources/SwiftJavaTool/CommonOptions.swift | 2 +- .../SwiftJavaBaseAsyncParsableCommand.swift | 6 +- .../FunctionDescriptorImportTests.swift | 4 +- 10 files changed, 111 insertions(+), 150 deletions(-) create mode 100644 Sources/SwiftExtractConfigurationShared/LogLevel.swift diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift index a5b4c1be0..331fb0aa1 100644 --- a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -15,42 +15,11 @@ import SwiftExtract import SwiftJavaConfigurationShared -/// Bridges swift-java's `Configuration` onto the language-neutral -/// `SwiftExtractConfiguration` surface consumed by `SwiftExtract`. -/// -/// Most members are satisfied directly by `Configuration`'s own properties. -/// `Configuration` shares `AccessLevelMode` with the analyzer (both pull it -/// in from `SwiftExtractConfigurationShared`), so -/// `effectiveMinimumInputAccessLevelMode` already conforms without a bridge. -/// Only `swiftExtractLogLevel` needs a mapping from swift-java's `LogLevel` -/// onto the neutral `Logger.Level`. -extension Configuration: SwiftExtractConfiguration { - public var swiftExtractLogLevel: SwiftExtract.Logger.Level? { - guard let logLevel else { return nil } - switch logLevel { - case .trace: return .trace - case .debug: return .debug - case .info: return .info - case .notice: return .notice - case .warning: return .warning - case .error: return .error - case .critical: return .critical - } - } -} - -extension LogLevel { - /// Bridges from the analysis layer's neutral `Logger.Level` (used by the CLI's - /// `--log-level` option) onto swift-java's own `LogLevel`. - public init(_ level: SwiftExtract.Logger.Level) { - switch level { - case .trace: self = .trace - case .debug: self = .debug - case .info: self = .info - case .notice: self = .notice - case .warning: self = .warning - case .error: self = .error - case .critical: self = .critical - } - } -} +/// `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 `permitsUnresolvedTypeReferences`, so this +/// conformance is empty. +extension Configuration: SwiftExtractConfiguration {} diff --git a/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift b/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift index d6e6a828f..5b60fd324 100644 --- a/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift +++ b/Sources/JExtractSwiftLib/Logger+ArgumentParser.swift @@ -16,7 +16,7 @@ import ArgumentParser import SwiftExtract import SwiftJavaConfigurationShared -extension Logger.Level: ExpressibleByArgument { +extension LogLevel: ExpressibleByArgument { public var defaultValueDescription: String { "log level" } diff --git a/Sources/SwiftExtract/Logger.swift b/Sources/SwiftExtract/Logger.swift index 5a89412db..c81974019 100644 --- a/Sources/SwiftExtract/Logger.swift +++ b/Sources/SwiftExtract/Logger.swift @@ -13,14 +13,15 @@ //===----------------------------------------------------------------------===// import Foundation +@_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 } @@ -110,46 +111,3 @@ public struct Logger { print("[trace][\(file):\(line)](\(function)) \(message()) \(metadataString)") } } - -extension Logger { - /// Log verbosity levels for the analysis layer's lightweight logger. - /// - /// Language-neutral; language-specific configuration modules map their own - /// log-level enums onto this via `SwiftExtractConfiguration`. - public enum Level: String, Codable, Hashable, Sendable { - case trace - case debug - case info - case notice - case warning - case error - case critical - } -} - -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/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 09aefcff1..ff06e4494 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -79,7 +79,7 @@ public final class SwiftAnalyzer { 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.swiftExtractLogLevel ?? .info) + self.log = Logger(label: "analyzer", logLevel: config.logLevel ?? .info) self.config = config self.swiftModuleName = swiftModule self.extractDecider = extractDecider diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index 858a96109..b8453bc2c 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -24,14 +24,10 @@ /// (e.g. Java/JNI/FFM, or other language code generators) without pulling /// target-specific config types into `SwiftExtract`. /// -/// `AccessLevelMode` lives in the small `SwiftExtractConfigurationShared` -/// target so language-specific configuration shared modules (e.g. -/// `SwiftJavaConfigurationShared`) can use the same enum directly without -/// taking a dependency on SwiftSyntax. -/// -/// The enum-typed `swiftExtractLogLevel` member uses a `swiftExtract`-prefixed -/// name so a conforming type can keep its own, differently-typed `logLevel` -/// member without a name collision. +/// `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 } @@ -54,7 +50,7 @@ public protocol SwiftExtractConfiguration { var effectiveMinimumInputAccessLevelMode: AccessLevelMode { get } /// Verbosity for the analyzer's logger; `nil` falls back to `.info`. - var swiftExtractLogLevel: Logger.Level? { get } + var logLevel: LogLevel? { get } /// Module names that should be treated as importable when resolving /// `#if canImport()` conditions, in addition to whatever the build @@ -107,14 +103,14 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { public var swiftFilterExclude: [String]? public var importedModuleStubs: [String: [String]]? public var effectiveMinimumInputAccessLevelMode: AccessLevelMode - public var swiftExtractLogLevel: Logger.Level? + public var logLevel: LogLevel? public var availableImportModules: Set public var permitsUnresolvedTypeReferences: Bool public init( swiftModule: String? = nil, accessLevel: AccessLevelMode = .public, - logLevel: Logger.Level? = nil, + logLevel: LogLevel? = nil, staticBuildConfigurationFile: String? = nil, swiftFilterInclude: [String]? = nil, swiftFilterExclude: [String]? = nil, @@ -124,7 +120,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { ) { self.swiftModule = swiftModule self.effectiveMinimumInputAccessLevelMode = accessLevel - self.swiftExtractLogLevel = logLevel + self.logLevel = logLevel self.staticBuildConfigurationFile = staticBuildConfigurationFile self.swiftFilterInclude = swiftFilterInclude self.swiftFilterExclude = swiftFilterExclude 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 61af95a33..50d4a6f13 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -667,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/SwiftJavaTool/CommonOptions.swift b/Sources/SwiftJavaTool/CommonOptions.swift index 552cfea74..0be3c210c 100644 --- a/Sources/SwiftJavaTool/CommonOptions.swift +++ b/Sources/SwiftJavaTool/CommonOptions.swift @@ -64,7 +64,7 @@ extension SwiftJava { var inputSwift: String? = nil @Option(name: .shortAndLong, help: "Configure the level of logs that should be printed") - var logLevel: SwiftExtract.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 95eaf7d0a..b5550cd9d 100644 --- a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -31,7 +31,7 @@ protocol SwiftJavaBaseAsyncParsableCommand: AsyncParsableCommand { var log: Logging.Logger { get } - var logLevel: SwiftExtract.Logger.Level { get set } + var logLevel: LogLevel { get set } /// Must be implemented with an `@OptionGroup` in Command implementations var commonOptions: SwiftJava.CommonOptions { get set } @@ -104,7 +104,7 @@ extension SwiftJavaBaseAsyncParsableCommand { .init(label: "swift-java") } - var logLevel: SwiftExtract.Logger.Level { + var logLevel: LogLevel { get { self.commonOptions.logLevel } @@ -183,7 +183,7 @@ extension SwiftJavaBaseAsyncParsableCommand { config = Configuration() } // override configuration with options from command line - config.logLevel = LogLevel(command.logLevel) + config.logLevel = command.logLevel return config } } diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index b37025313..7bdefe2ea 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -233,7 +233,7 @@ 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() @@ -267,7 +267,7 @@ 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() From 6908ff4ff5d1e29af0b5d389661224b595f09311 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 16:03:47 +0900 Subject: [PATCH 30/38] SwiftExtract: drop @_spi(Testing) from SwiftNominalTypeDeclaration.syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JExtractSwiftLib needs to read the underlying syntax attributes to recognize `@JavaClass`/`@JavaInterface`/`@JavaExport` etc. on a SwiftNominalTypeDeclaration, which forced a `@_spi(Testing) import SwiftExtract` in JExtractSwiftLib/SwiftSyntax+Java.swift. The SPI gating wasn't really protecting anything — once a downstream language target wants to inspect attributes on a nominal, the test-only label becomes a lie. - NominalTypeDeclSyntaxNode typealias and SwiftNominalTypeDeclaration.syntax are now plain `public` (also fixed the typo `/////` -> `///` on the typealias's leading doc comment). - The synthesizing init() keeps its @_spi(Testing) — building a fake nominal from outside SwiftExtract genuinely is a test-only need. - JExtractSwiftLib/SwiftSyntax+Java.swift: `@_spi(Testing) import SwiftExtract` -> `import SwiftExtract`. --- Sources/JExtractSwiftLib/SwiftSyntax+Java.swift | 2 +- .../SwiftTypes/SwiftNominalTypeDeclaration.swift | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift index 74acf3fff..cc749e237 100644 --- a/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift +++ b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -@_spi(Testing) import SwiftExtract +import SwiftExtract import SwiftSyntax extension AttributeListSyntax.Element { diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift index 45cd45696..f43e92321 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -14,8 +14,7 @@ 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 @@ -63,7 +62,7 @@ public class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { } /// The syntax node this declaration is derived from. - @_spi(Testing) public let syntax: NominalTypeDeclSyntaxNode + public let syntax: NominalTypeDeclSyntaxNode /// The kind of nominal type. public let kind: Kind From 9ebadddc1e14001997a4f7feaf3620eec02cdaa9 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 16:06:09 +0900 Subject: [PATCH 31/38] JExtractSwiftLib: rename isSwiftJavaMacro -> isJavaKitMacro, factor known names into a constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The attribute these wrappers come from belong to JavaKit (the Java -> Swift wrapping layer), so 'JavaKit' captures the relationship better than the more generic 'SwiftJava'. While renaming, lift the inline list of attribute names into a top-level constant so the names have one source of truth and the predicate body becomes a single `contains` call instead of a multi-case switch. - New top-level `KnownJavaKitMacroNames: [String]` lists the seven attribute names (`JavaClass`, `JavaInterface`, `JavaField`, `JavaStaticField`, `JavaMethod`, `JavaStaticMethod`, `JavaImplementation`). - `AttributeListSyntax.Element.isSwiftJavaMacro` -> `isJavaKitMacro`, body collapses to `KnownJavaKitMacroNames.contains(attrName)`. - `SwiftNominalType.isSwiftJavaWrapper` updates its only call site (`\.isSwiftJavaMacro` -> `\.isJavaKitMacro`); the property name stays as-is — it describes the type's role (a Swift wrapper for a Java class), not the macro name. - `JavaExtractDecider`'s `isSwiftJavaMacro` callsite updated. --- .../JExtractSwiftLib/JavaExtractDecider.swift | 2 +- .../JExtractSwiftLib/SwiftSyntax+Java.swift | 36 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index ae2fac57a..93e77c984 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -62,7 +62,7 @@ public struct JavaExtractDecider: ExtractDecider { if attrs?.contains(where: { $0.isJavaExport }) == true { return true } - if attrs?.contains(where: { $0.isSwiftJavaMacro }) == true { + if attrs?.contains(where: { $0.isJavaKitMacro }) == true { log.trace("Skip '\(decl.qualifiedNameForDebug)': swift-java macro-wrapped Java type") return false } diff --git a/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift index cc749e237..c035bae01 100644 --- a/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift +++ b/Sources/JExtractSwiftLib/SwiftSyntax+Java.swift @@ -15,23 +15,35 @@ 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 has `SwiftJava` wrapping attributes (types that wrap Java classes). - /// These are skipped during jextract because they represent Java->Swift wrappers. - /// Note: `@JavaExport` is NOT included here — it forces export of Swift types to Java. - package var isSwiftJavaMacro: Bool { + /// 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 } - switch attrName { - case "JavaClass", "JavaInterface", "JavaField", "JavaStaticField", "JavaMethod", "JavaStaticMethod", - "JavaImplementation": - return true - default: - return false - } + return KnownJavaKitMacroNames.contains(attrName) } /// Whether this is a `@JavaExport` attribute (used on typealiases for specialization, @@ -48,6 +60,6 @@ extension SwiftNominalType { /// macros (`@JavaClass`, `@JavaInterface`, …) — meaning the type represents /// a Java class wrapped for Swift, not a Swift type to be re-exported public var isSwiftJavaWrapper: Bool { - nominalTypeDecl.syntax.attributes.contains(where: \.isSwiftJavaMacro) + nominalTypeDecl.syntax.attributes.contains(where: \.isJavaKitMacro) } } From 714020f06bbd2f25d42403858aff467357776d49 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 16:08:41 +0900 Subject: [PATCH 32/38] SwiftExtract: rename WithModifiersSyntax.passesAccessLevel -> isAtLeast The call site reads as English now: mod.isAtLeast(accessLevel, in: parent) Two overloads (the public ExtractedNominalType-parent variant and the package NominalTypeDeclSyntaxNode-parent variant) both renamed; both ExtractDecider implementations (DefaultAccessLevelExtractDecider, JavaExtractDecider) updated. --- Sources/JExtractSwiftLib/JavaExtractDecider.swift | 2 +- .../SwiftExtract/Convenience/SwiftSyntax+Extensions.swift | 6 +++--- Sources/SwiftExtract/ExtractDecider.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index 93e77c984..9f1f34500 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -81,7 +81,7 @@ public struct JavaExtractDecider: ExtractDecider { guard let mod = decl.asProtocol((any WithModifiersSyntax).self) else { return false } - let ok = mod.passesAccessLevel(accessLevel, in: parent) + let ok = mod.isAtLeast(accessLevel, in: parent) if !ok { log.trace("Skip '\(decl.qualifiedNameForDebug)': not at least \(accessLevel)") } diff --git a/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift b/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift index 4070ac0b1..e54c1c3eb 100644 --- a/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift @@ -125,17 +125,17 @@ extension WithModifiersSyntax { /// 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 passesAccessLevel( + public func isAtLeast( _ mode: AccessLevelMode, in parent: ExtractedNominalType? ) -> Bool { - self.passesAccessLevel(mode, in: parent?.swiftNominal.syntax) + self.isAtLeast(mode, in: parent?.swiftNominal.syntax) } /// Lower-level overload taking the parent's syntax node directly. Used by /// the analyzer; downstream `ExtractDecider`s use the `ExtractedNominalType` /// overload above. - package func passesAccessLevel( + package func isAtLeast( _ mode: AccessLevelMode, in parent: NominalTypeDeclSyntaxNode? ) -> Bool { diff --git a/Sources/SwiftExtract/ExtractDecider.swift b/Sources/SwiftExtract/ExtractDecider.swift index cbee50fbf..ba96778d0 100644 --- a/Sources/SwiftExtract/ExtractDecider.swift +++ b/Sources/SwiftExtract/ExtractDecider.swift @@ -57,7 +57,7 @@ public struct DefaultAccessLevelExtractDecider: ExtractDecider { guard let mod = decl.asProtocol((any WithModifiersSyntax).self) else { return false } - let ok = mod.passesAccessLevel(accessLevel, in: parent) + let ok = mod.isAtLeast(accessLevel, in: parent) if !ok { log.trace("Skip '\(decl.qualifiedNameForDebug)': not at least \(accessLevel)") } From 98ea1997e402df9c81868891d9bcb05545b32203 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 16:29:16 +0900 Subject: [PATCH 33/38] SwiftExtract: ExtractDecider owns its own logger (drops log: param) The protocol was passing the analyzer's logger into every shouldExtract call. That hid two things: (1) the decider couldn't be constructed and asked outside the analyzer's context (the caller had no logger to give it), and (2) the decider's trace messages all came out under the analyzer's logger label, masking which decider produced them. Make each decider own its logger: - DefaultAccessLevelExtractDecider's init takes `logLevel: LogLevel = .info` and stores a Logger labelled `DefaultAccessLevelExtractDecider`. - JavaExtractDecider's init takes the same; `makeSwiftJavaAnalyzer` threads `config.logLevel ?? .info` in. - ExtractDecider.shouldExtract drops `log: Logger` parameter; the visitor's wrapper drops it too. - All six call sites (`SwiftAnalysisVisitor` x4, `SwiftAnalyzer` x2) drop `log: log`. Doc comment updated to spell out the new contract: 'implementations should emit a .trace line on every skip path (using their own logger)'. --- Sources/JExtractSwiftLib/JavaExtractDecider.swift | 12 ++++++++---- Sources/SwiftExtract/ExtractDecider.swift | 15 ++++++++------- Sources/SwiftExtract/SwiftAnalysisVisitor.swift | 11 +++++------ Sources/SwiftExtract/SwiftAnalyzer.swift | 4 ++-- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index 9f1f34500..34e2e164a 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -19,7 +19,10 @@ import SwiftSyntax public func makeSwiftJavaAnalyzer(config: Configuration) -> SwiftAnalyzer { SwiftAnalyzer( config: config, - extractDecider: JavaExtractDecider(accessLevel: config.effectiveMinimumInputAccessLevelMode), + extractDecider: JavaExtractDecider( + accessLevel: config.effectiveMinimumInputAccessLevelMode, + logLevel: config.logLevel ?? .info + ), ) } @@ -37,15 +40,16 @@ public func makeSwiftJavaAnalyzer(config: Configuration) -> SwiftAnalyzer { /// 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) { + 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?, - log: Logger + in parent: ExtractedNominalType? ) -> Bool { // Initializers of an unspecialized generic type can't be constructed from // Java — drop them regardless of attribute or access level. diff --git a/Sources/SwiftExtract/ExtractDecider.swift b/Sources/SwiftExtract/ExtractDecider.swift index ba96778d0..2e3a02302 100644 --- a/Sources/SwiftExtract/ExtractDecider.swift +++ b/Sources/SwiftExtract/ExtractDecider.swift @@ -28,15 +28,15 @@ import SwiftSyntax 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 - /// - log: the analyzer's logger; deciders should emit a `.trace` line - /// for each skip path so users can see why a decl was dropped func shouldExtract( decl: DeclSyntax, - in parent: ExtractedNominalType?, - log: Logger + in parent: ExtractedNominalType? ) -> Bool } @@ -44,15 +44,16 @@ public protocol ExtractDecider { /// filter. Used by `SwiftAnalyzer` when no decider is supplied public struct DefaultAccessLevelExtractDecider: ExtractDecider { public let accessLevel: AccessLevelMode + let log: Logger - public init(accessLevel: AccessLevelMode) { + 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?, - log: Logger + in parent: ExtractedNominalType? ) -> Bool { guard let mod = decl.asProtocol((any WithModifiersSyntax).self) else { return false diff --git a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index f8bbf4468..3c9e79d1a 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -179,7 +179,7 @@ final class SwiftAnalysisVisitor { in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext, decider: analyzer.extractDecider) else { + guard node.shouldExtract(config: config, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -273,7 +273,7 @@ final class SwiftAnalysisVisitor { in typeContext: ExtractedNominalType?, sourceFilePath: String, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext, decider: analyzer.extractDecider) else { + guard node.shouldExtract(config: config, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -320,7 +320,7 @@ final class SwiftAnalysisVisitor { self.log.info("Initializer must be within a current type; \(node)") return } - guard node.shouldExtract(config: config, log: log, in: typeContext, decider: analyzer.extractDecider) else { + guard node.shouldExtract(config: config, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -356,7 +356,7 @@ final class SwiftAnalysisVisitor { subscriptDecl node: SubscriptDeclSyntax, in typeContext: ExtractedNominalType?, ) { - guard node.shouldExtract(config: config, log: log, in: typeContext, decider: analyzer.extractDecider) else { + guard node.shouldExtract(config: config, in: typeContext, decider: analyzer.extractDecider) else { return } @@ -705,10 +705,9 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn /// lives there — see `ExtractDecider`. func shouldExtract( config: any SwiftExtractConfiguration, - log: Logger, in parent: ExtractedNominalType?, decider: any ExtractDecider ) -> Bool { - decider.shouldExtract(decl: DeclSyntax(self), in: parent, log: log) + decider.shouldExtract(decl: DeclSyntax(self), in: parent) } } diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index ff06e4494..0e5e18b78 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -449,7 +449,7 @@ extension SwiftAnalyzer { _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, parent: ExtractedNominalType?, ) -> ExtractedNominalType? { - if !nominalNode.shouldExtract(config: config, log: log, in: parent, decider: extractDecider) { + if !nominalNode.shouldExtract(config: config, in: parent, decider: extractDecider) { return nil } @@ -477,7 +477,7 @@ extension SwiftAnalyzer { return nil } - guard swiftNominalDecl.syntax.shouldExtract(config: config, log: log, in: nil as ExtractedNominalType?, decider: extractDecider) else { + guard swiftNominalDecl.syntax.shouldExtract(config: config, in: nil as ExtractedNominalType?, decider: extractDecider) else { return nil } From 53fa1d36fbc57b81bd589aaeeedd8d2da3aa5d94 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 16:40:36 +0900 Subject: [PATCH 34/38] SwiftExtract: type extractedTypes via SwiftTypeName typealias The String dictionary key on extractedTypes hid what the key actually is. SwiftExtract already exposes the typealias `SwiftTypeName = String` in SourceDependencies.swift; use it on the four [String: ExtractedNominalType] occurrences so the role of the key reads from the type signature. - SwiftAnalyzer.extractedTypes - AnalysisResult.extractedTypes (+ initializer) - ExtractedNominalType.conformsTo(_:in:)'s parameter - JNISwift2JavaGenerator+JavaTranslation's local extractedTypes ref --- .../JNI/JNISwift2JavaGenerator+JavaTranslation.swift | 2 +- Sources/SwiftExtract/AnalysisResult.swift | 4 ++-- Sources/SwiftExtract/ExtractedDecls.swift | 2 +- Sources/SwiftExtract/SwiftAnalyzer.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index b5b3ffaa9..00c606d66 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -94,7 +94,7 @@ extension JNISwift2JavaGenerator { let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger var javaIdentifiers: JavaIdentifierFactory - let extractedTypes: [String: ExtractedNominalType] + let extractedTypes: [SwiftTypeName: ExtractedNominalType] func translate(enumCase: ExtractedEnumCase) throws -> TranslatedEnumCase { let methodName = "" // TODO: Used for closures, replace with better name? diff --git a/Sources/SwiftExtract/AnalysisResult.swift b/Sources/SwiftExtract/AnalysisResult.swift index e481038dd..4b39506de 100644 --- a/Sources/SwiftExtract/AnalysisResult.swift +++ b/Sources/SwiftExtract/AnalysisResult.swift @@ -15,12 +15,12 @@ /// 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: [String: ExtractedNominalType] + public var extractedTypes: [SwiftTypeName: ExtractedNominalType] public var extractedGlobalVariables: [ExtractedFunc] public var extractedGlobalFuncs: [ExtractedFunc] public init( - extractedTypes: [String: ExtractedNominalType], + extractedTypes: [SwiftTypeName: ExtractedNominalType], extractedGlobalVariables: [ExtractedFunc], extractedGlobalFuncs: [ExtractedFunc] ) { diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index 0407f2b3d..cdfe8185a 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -199,7 +199,7 @@ public final class ExtractedNominalType: ExtractedSwiftDecl { } /// Checks if this type, or any of types it inherits from, conforms to the passed in protocol. - public func conformsTo(_ protocolName: String, in extractedTypes: [String: ExtractedNominalType]) -> Bool { + public func conformsTo(_ protocolName: String, in extractedTypes: [SwiftTypeName: ExtractedNominalType]) -> Bool { var visited: Set = [] var queue: [ExtractedNominalType] = [self] while let current = queue.popLast() { diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 0e5e18b78..b90f2509f 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -55,7 +55,7 @@ public final class SwiftAnalyzer { /// A mapping from Swift type names (e.g., A.B) over to the extracted nominal /// type representation. - package var extractedTypes: [String: ExtractedNominalType] = [:] + 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] = [:] From 3299380ec341e7f447b4c0359b01af125ab2571b Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 16:41:51 +0900 Subject: [PATCH 35/38] SwiftExtract: collapse analyze() overloads, default hook to no-op, fix param order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The analyze API carried two overloads on both the instance and static sides — one with the hook required, one taking no hook and trampolining to the first with an empty closure. Default the hook to `{ _ in }` and drop the trampolining overloads. Also fix the static analyze's parameter order: the rule is 'defaulted parameters follow non-defaulted', and `extractDecider:` (required) was sandwiched between `config:` and `sourceDependencies:` (both defaulted). Move `extractDecider:` up so all three required parameters lead. TestAnalyze's call site reorders to match (kwargs were already used so no signature change leaks beyond the file). --- Sources/SwiftExtract/SwiftAnalyzer.swift | 43 +++++-------------- .../Support/TestAnalyze.swift | 4 +- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index b90f2509f..84e30dddd 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -136,15 +136,11 @@ extension SwiftAnalyzer { try self.analyze() } - /// Analyze registered inputs. - package func analyze() throws { - try analyze(beforeProcessingDeferredExtensions: { _ in }) - } - - /// Analyze registered inputs, 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. + /// 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:` @@ -155,7 +151,7 @@ extension SwiftAnalyzer { /// 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 + beforeProcessingDeferredExtensions hook: (SwiftAnalyzer) throws -> Void = { _ in } ) throws { prepareForTranslation() @@ -261,26 +257,9 @@ extension SwiftAnalyzer { } /// Top-level convenience: run analysis on the given Swift sources and return - /// the resulting `AnalysisResult`. - public static func analyze( - sources: [(path: String, text: String)], - moduleName: String, - config: (any SwiftExtractConfiguration)? = nil, - sourceDependencies: SourceDependencies = SourceDependencies(), - extractDecider: any ExtractDecider - ) throws -> AnalysisResult { - try analyze( - sources: sources, - moduleName: moduleName, - config: config, - sourceDependencies: sourceDependencies, - extractDecider: extractDecider, - beforeProcessingDeferredExtensions: { _ in } - ) - } - - /// Top-level convenience that accepts a hook fired after the per-source - /// walk and before deferred-constrained-extension processing. See + /// 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) @@ -289,10 +268,10 @@ extension SwiftAnalyzer { public static func analyze( sources: [(path: String, text: String)], moduleName: String, + extractDecider: any ExtractDecider, config: (any SwiftExtractConfiguration)? = nil, sourceDependencies: SourceDependencies = SourceDependencies(), - extractDecider: any ExtractDecider, - beforeProcessingDeferredExtensions hook: (SwiftAnalyzer) throws -> Void + beforeProcessingDeferredExtensions hook: (SwiftAnalyzer) throws -> Void = { _ in } ) throws -> AnalysisResult { let effectiveConfig = config ?? DefaultSwiftExtractConfiguration(swiftModule: moduleName) let analyzer = SwiftAnalyzer(config: effectiveConfig, moduleName: moduleName, extractDecider: extractDecider) diff --git a/Tests/SwiftExtractTests/Support/TestAnalyze.swift b/Tests/SwiftExtractTests/Support/TestAnalyze.swift index 115eb0c9e..86dcb0949 100644 --- a/Tests/SwiftExtractTests/Support/TestAnalyze.swift +++ b/Tests/SwiftExtractTests/Support/TestAnalyze.swift @@ -33,8 +33,8 @@ func analyze( return try SwiftAnalyzer.analyze( sources: sources, moduleName: moduleName, + extractDecider: DefaultAccessLevelExtractDecider(accessLevel: effectiveConfig.effectiveMinimumInputAccessLevelMode), config: effectiveConfig, - sourceDependencies: sourceDependencies, - extractDecider: DefaultAccessLevelExtractDecider(accessLevel: effectiveConfig.effectiveMinimumInputAccessLevelMode) + sourceDependencies: sourceDependencies ) } From deea4248a0190bc7549fcaa6a8652b50f29b0972 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 16:43:19 +0900 Subject: [PATCH 36/38] SwiftExtract: move Foundation overlay logic into SwiftAnalyzer+Foundation.swift `visitFoundationDeclsIfNeeded` is a self-contained ~65-line block that overlays Foundation/FoundationEssentials known types when the analyzed code references any of them. It belongs next to the rest of the analyzer's Foundation knowledge but doesn't need to live in the main `SwiftAnalyzer.swift` file. Move it into a sibling `SwiftAnalyzer+Foundation.swift` (still an extension on `SwiftAnalyzer`). Visibility goes from `private` to `internal` (no modifier) since its caller `analyze(...)` lives in the original file. The `isUsing` helper it depends on stays where it is and remains internal. --- .../SwiftAnalyzer+Foundation.swift | 83 +++++++++++++++++++ Sources/SwiftExtract/SwiftAnalyzer.swift | 66 --------------- 2 files changed, 83 insertions(+), 66 deletions(-) create mode 100644 Sources/SwiftExtract/SwiftAnalyzer+Foundation.swift 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 index 84e30dddd..48c18974a 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -283,72 +283,6 @@ extension SwiftAnalyzer { return analyzer.result } - private func visitFoundationDeclsIfNeeded(with visitor: SwiftAnalysisVisitor) { - // Each entry pairs a Foundation/FoundationEssentials counterpart so the - // user-code reference can match either. Entries within the same group are - // visited together when any one of the candidates is referenced — so using - // 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, From 439a625a5586429f316fb12122991707f820c3de Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 16:45:16 +0900 Subject: [PATCH 37/38] SwiftExtract: rename permitsUnresolvedTypeReferences -> allowUnresolvedTypeReferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads more naturally as a config flag — `allow` is the verb you ask when toggling a permission, `permits` is the verb you'd describe one in the third person. Renames the protocol requirement, both default and stored implementations on `SwiftExtractConfiguration`/`Default*`, the corresponding mirror on `SwiftTypeLookupContext`, and the two read sites in `SwiftType.init` and `SwiftAnalyzer.prepareForTranslation`. --- .../JExtractSwiftLib/Configuration+SwiftExtract.swift | 2 +- Sources/SwiftExtract/SwiftAnalyzer.swift | 4 ++-- Sources/SwiftExtract/SwiftExtractConfiguration.swift | 10 +++++----- Sources/SwiftExtract/SwiftTypes/SwiftType.swift | 4 ++-- .../SwiftTypes/SwiftTypeLookupContext.swift | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift index 331fb0aa1..1435a62ab 100644 --- a/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift +++ b/Sources/JExtractSwiftLib/Configuration+SwiftExtract.swift @@ -20,6 +20,6 @@ import SwiftJavaConfigurationShared /// `logLevel`, …) — `LogLevel` and `AccessLevelMode` are the same enums on /// both sides, both pulled in from `SwiftExtractConfigurationShared`. The /// protocol extension on `SwiftExtractConfiguration` defaults -/// `availableImportModules` and `permitsUnresolvedTypeReferences`, so this +/// `availableImportModules` and `allowUnresolvedTypeReferences`, so this /// conformance is empty. extension Configuration: SwiftExtractConfiguration {} diff --git a/Sources/SwiftExtract/SwiftAnalyzer.swift b/Sources/SwiftExtract/SwiftAnalyzer.swift index 48c18974a..c50e9401d 100644 --- a/Sources/SwiftExtract/SwiftAnalyzer.swift +++ b/Sources/SwiftExtract/SwiftAnalyzer.swift @@ -292,8 +292,8 @@ extension SwiftAnalyzer { buildConfig: self.buildConfig, ) self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) - self.lookupContext.permitsUnresolvedTypeReferences = - self.config.permitsUnresolvedTypeReferences + self.lookupContext.allowUnresolvedTypeReferences = + self.config.allowUnresolvedTypeReferences } /// Check if any of the extracted decls uses a nominal declaration that satisfies diff --git a/Sources/SwiftExtract/SwiftExtractConfiguration.swift b/Sources/SwiftExtract/SwiftExtractConfiguration.swift index b8453bc2c..6ff68c9d6 100644 --- a/Sources/SwiftExtract/SwiftExtractConfiguration.swift +++ b/Sources/SwiftExtract/SwiftExtractConfiguration.swift @@ -77,7 +77,7 @@ public protocol SwiftExtractConfiguration { /// or recognize them. See /// `SwiftNominalTypeDeclaration.isUnresolvedTypePlaceholder` for the long /// form. Default: false. - var permitsUnresolvedTypeReferences: Bool { get } + var allowUnresolvedTypeReferences: Bool { get } /// Whether the given module name has stub declarations configured. func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool @@ -86,7 +86,7 @@ public protocol SwiftExtractConfiguration { extension SwiftExtractConfiguration { public var availableImportModules: Set { [] } - public var permitsUnresolvedTypeReferences: Bool { false } + public var allowUnresolvedTypeReferences: Bool { false } public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool { importedModuleStubs?.keys.contains(moduleName) ?? false @@ -105,7 +105,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { public var effectiveMinimumInputAccessLevelMode: AccessLevelMode public var logLevel: LogLevel? public var availableImportModules: Set - public var permitsUnresolvedTypeReferences: Bool + public var allowUnresolvedTypeReferences: Bool public init( swiftModule: String? = nil, @@ -116,7 +116,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { swiftFilterExclude: [String]? = nil, importedModuleStubs: [String: [String]]? = nil, availableImportModules: Set = [], - permitsUnresolvedTypeReferences: Bool = false + allowUnresolvedTypeReferences: Bool = false ) { self.swiftModule = swiftModule self.effectiveMinimumInputAccessLevelMode = accessLevel @@ -126,6 +126,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration { self.swiftFilterExclude = swiftFilterExclude self.importedModuleStubs = importedModuleStubs self.availableImportModules = availableImportModules - self.permitsUnresolvedTypeReferences = permitsUnresolvedTypeReferences + self.allowUnresolvedTypeReferences = allowUnresolvedTypeReferences } } diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 9ce66d14f..8671cb73d 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -509,12 +509,12 @@ extension SwiftType { typeDecl = try lookupContext.unqualifiedLookup(name: ident, from: name) } guard let typeDecl else { - // Lenient mode (opt-in via SwiftExtractConfiguration.permitsUnresolvedTypeReferences): + // 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.permitsUnresolvedTypeReferences { + if lookupContext.allowUnresolvedTypeReferences { self = SwiftSyntheticTypes.unresolvedNominal(name.text) if !genericArguments.isEmpty, case .nominal(let synth) = self diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift index 25d5eaca1..31d66f86f 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift @@ -25,8 +25,8 @@ public class SwiftTypeLookupContext { /// When true, name lookups that fail to resolve fall back to a synthetic /// unresolved nominal (via `SwiftSyntheticTypes.unresolvedNominal(_:)`) /// instead of throwing. See - /// `SwiftExtractConfiguration.permitsUnresolvedTypeReferences`. - public var permitsUnresolvedTypeReferences: Bool = false + /// `SwiftExtractConfiguration.allowUnresolvedTypeReferences`. + public var allowUnresolvedTypeReferences: Bool = false private var typeDecls: [Syntax.ID: SwiftTypeDeclaration] = [:] From 1a3b3c8559b65c8be48f00ec4037ca1390ea3321 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 16 Jun 2026 16:48:16 +0900 Subject: [PATCH 38/38] SwiftExtract: drop @_spi(Testing) from SwiftNominalTypeDeclaration.init Companion to 376062f4 (which dropped the SPI on the .syntax stored property and on the typealias). The init's SPI gate only restricted external callers; all four in-module call sites (ExtractedDecls, SwiftSyntheticTypes, SwiftTypeLookupContext, SwiftParsedModuleSymbolTableBuilder) bypass it because they're in the same module. The bookkeeping bit `isUnresolvedTypePlaceholder` on the init means external code that wants to mint synthetic nominals can do so directly (matching the API direction we've been moving in). Consistent with the rest of the type's surface being plain `public`. --- .../SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift index f43e92321..31a076648 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -110,7 +110,7 @@ public class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { /// Create a nominal type declaration from the syntax node for a nominal type /// declaration. - @_spi(Testing) public init( + public init( name: String, sourceFilePath: String, moduleName: String,