Skip to content

Commit 09cee5b

Browse files
committed
SwiftExtract: configurable importable modules + generic-type initializer extraction
Two opt-in, language-neutral knobs on `SwiftExtractConfiguration` (both default to the prior behavior, so the Java path is unchanged): - `availableImportModules: Set<String>` — module names treated as importable when resolving `#if canImport(<module>)`. The analyzer wraps its build configuration in an `ImportOverlayBuildConfiguration` so declarations guarded behind `#if canImport(MyModule)` are extracted (e.g. swift-python guards extractable code with `#if canImport(SwiftPython)`, which the static build config doesn't otherwise know). - `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); targets that specialize generics in a post-analysis pass need the base type's initializers available to clone onto the specialization.
1 parent e7eeea7 commit 09cee5b

4 files changed

Lines changed: 76 additions & 4 deletions

File tree

Sources/SwiftExtract/SwiftAnalysisVisitor.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,9 @@ final class SwiftAnalysisVisitor {
337337
return
338338
}
339339

340-
if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization {
340+
if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization
341+
&& !config.extractsGenericTypeInitializers
342+
{
341343
log.debug("Skip Importing generic type initializer \(node.kind) '\(node.qualifiedNameForDebug)'")
342344
return
343345
}

Sources/SwiftExtract/SwiftAnalyzer.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,27 @@ public final class SwiftAnalyzer {
9191
do {
9292
let data = try Data(contentsOf: URL(fileURLWithPath: staticBuildConfigPath))
9393
let decoder = JSONDecoder()
94-
self.buildConfig = try decoder.decode(StaticBuildConfiguration.self, from: data)
94+
let staticConfig = try decoder.decode(StaticBuildConfiguration.self, from: data)
95+
self.buildConfig = SwiftAnalyzer.overlayingAvailableModules(staticConfig, config.availableImportModules)
9596
self.log.info("Using custom static build configuration from: \(staticBuildConfigPath)")
9697
} catch {
9798
fatalError("Failed to load static build configuration from '\(staticBuildConfigPath)': \(error)")
9899
}
99100
} else {
100-
self.buildConfig = .swiftExtractDefault
101+
self.buildConfig = SwiftAnalyzer.overlayingAvailableModules(.swiftExtractDefault, config.availableImportModules)
101102
}
102103
}
104+
105+
/// Overlay the configured extra importable modules onto a base build config
106+
/// (returns the base unchanged when none are configured).
107+
private static func overlayingAvailableModules<Base: BuildConfiguration>(
108+
_ base: Base,
109+
_ availableImportModules: Set<String>
110+
) -> any BuildConfiguration {
111+
availableImportModules.isEmpty
112+
? base
113+
: ImportOverlayBuildConfiguration(base: base, availableImportModules: availableImportModules)
114+
}
103115
}
104116

105117
// ===== --------------------------------------------------------------------------------------------------------------

Sources/SwiftExtract/SwiftExtractConfiguration.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,31 @@ public protocol SwiftExtractConfiguration {
6767
/// Verbosity for the analyzer's logger; `nil` falls back to `.info`.
6868
var swiftExtractLogLevel: Logger.Level? { get }
6969

70+
/// Whether to extract initializers of *generic* nominal types even when they
71+
/// are not (yet) specialized. swift-java skips these by default (an open
72+
/// generic isn't directly constructible); targets that specialize generics in
73+
/// a post-analysis pass (e.g. swift-python) set this `true` so the base type's
74+
/// initializers are available to clone onto the specialization. Default: false.
75+
var extractsGenericTypeInitializers: Bool { get }
76+
77+
/// Module names that should be treated as importable when resolving
78+
/// `#if canImport(<module>)` conditions, in addition to whatever the build
79+
/// configuration already knows. Lets a target opt-in to extracting code
80+
/// guarded behind `#if canImport(MyModule)` (e.g. swift-python guards
81+
/// extractable declarations with `#if canImport(SwiftPython)`). Default: empty.
82+
var availableImportModules: Set<String> { get }
83+
7084
/// Whether the given module name has stub declarations configured.
7185
func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool
7286
}
7387

7488
extension SwiftExtractConfiguration {
7589
public var extractsOperators: Bool { false }
7690

91+
public var extractsGenericTypeInitializers: Bool { false }
92+
93+
public var availableImportModules: Set<String> { [] }
94+
7795
public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool {
7896
importedModuleStubs?.keys.contains(moduleName) ?? false
7997
}
@@ -91,6 +109,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration {
91109
public var swiftExtractAccessLevel: AccessLevelMode
92110
public var swiftExtractLogLevel: Logger.Level?
93111
public var extractsOperators: Bool
112+
public var availableImportModules: Set<String>
94113

95114
public init(
96115
swiftModule: String? = nil,
@@ -100,7 +119,8 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration {
100119
staticBuildConfigurationFile: String? = nil,
101120
swiftFilterInclude: [String]? = nil,
102121
swiftFilterExclude: [String]? = nil,
103-
importedModuleStubs: [String: [String]]? = nil
122+
importedModuleStubs: [String: [String]]? = nil,
123+
availableImportModules: Set<String> = []
104124
) {
105125
self.swiftModule = swiftModule
106126
self.swiftExtractAccessLevel = accessLevel
@@ -110,5 +130,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration {
110130
self.swiftFilterInclude = swiftFilterInclude
111131
self.swiftFilterExclude = swiftFilterExclude
112132
self.importedModuleStubs = importedModuleStubs
133+
self.availableImportModules = availableImportModules
113134
}
114135
}

Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,40 @@ extension BuildConfiguration where Self == SwiftExtractDefaultBuildConfiguration
101101
.shared
102102
}
103103
}
104+
105+
/// Wraps any `BuildConfiguration` and additionally reports a configured set of
106+
/// module names as importable. Used so a target can extract declarations guarded
107+
/// behind `#if canImport(<module>)` for modules the static build configuration
108+
/// does not otherwise know about (e.g. swift-python's `#if canImport(SwiftPython)`).
109+
package struct ImportOverlayBuildConfiguration<Base: BuildConfiguration>: BuildConfiguration {
110+
package var base: Base
111+
package var availableImportModules: Set<String>
112+
113+
package init(base: Base, availableImportModules: Set<String>) {
114+
self.base = base
115+
self.availableImportModules = availableImportModules
116+
}
117+
118+
package func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool {
119+
// `importPath` is the dotted module path; the leading component is the module.
120+
if let moduleName = importPath.first?.1, availableImportModules.contains(moduleName) {
121+
return true
122+
}
123+
return try base.canImport(importPath: importPath, version: version)
124+
}
125+
126+
package func isCustomConditionSet(name: String) throws -> Bool { try base.isCustomConditionSet(name: name) }
127+
package func hasFeature(name: String) throws -> Bool { try base.hasFeature(name: name) }
128+
package func hasAttribute(name: String) throws -> Bool { try base.hasAttribute(name: name) }
129+
package func isActiveTargetOS(name: String) throws -> Bool { try base.isActiveTargetOS(name: name) }
130+
package func isActiveTargetArchitecture(name: String) throws -> Bool { try base.isActiveTargetArchitecture(name: name) }
131+
package func isActiveTargetEnvironment(name: String) throws -> Bool { try base.isActiveTargetEnvironment(name: name) }
132+
package func isActiveTargetRuntime(name: String) throws -> Bool { try base.isActiveTargetRuntime(name: name) }
133+
package func isActiveTargetPointerAuthentication(name: String) throws -> Bool { try base.isActiveTargetPointerAuthentication(name: name) }
134+
package func isActiveTargetObjectFormat(name: String) throws -> Bool { try base.isActiveTargetObjectFormat(name: name) }
135+
package var targetPointerBitWidth: Int { base.targetPointerBitWidth }
136+
package var targetAtomicBitWidths: [Int] { base.targetAtomicBitWidths }
137+
package var endianness: Endianness { base.endianness }
138+
package var languageVersion: VersionTuple { base.languageVersion }
139+
package var compilerVersion: VersionTuple { base.compilerVersion }
140+
}

0 commit comments

Comments
 (0)