Skip to content

Commit b849ab5

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. 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.
1 parent e395b3b commit b849ab5

4 files changed

Lines changed: 78 additions & 4 deletions

File tree

Sources/SwiftExtract/SwiftAnalysisVisitor.swift

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

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

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: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,32 @@ 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); other language code generators that
73+
/// specialize generics in a post-analysis pass set this `true` so the base
74+
/// type's initializers are available to clone onto the specialization.
75+
/// Default: false.
76+
var extractsGenericTypeInitializers: Bool { get }
77+
78+
/// Module names that should be treated as importable when resolving
79+
/// `#if canImport(<module>)` conditions, in addition to whatever the build
80+
/// configuration already knows. Lets a target opt-in to extracting code
81+
/// guarded behind `#if canImport(MyModule)` (e.g. another language code
82+
/// generator can declare its runtime module importable here). Default: empty.
83+
var availableImportModules: Set<String> { get }
84+
7085
/// Whether the given module name has stub declarations configured.
7186
func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool
7287
}
7388

7489
extension SwiftExtractConfiguration {
7590
public var extractsOperators: Bool { false }
7691

92+
public var extractsGenericTypeInitializers: Bool { false }
93+
94+
public var availableImportModules: Set<String> { [] }
95+
7796
public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool {
7897
importedModuleStubs?.keys.contains(moduleName) ?? false
7998
}
@@ -91,6 +110,7 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration {
91110
public var swiftExtractAccessLevel: AccessLevelMode
92111
public var swiftExtractLogLevel: Logger.Level?
93112
public var extractsOperators: Bool
113+
public var availableImportModules: Set<String>
94114

95115
public init(
96116
swiftModule: String? = nil,
@@ -100,7 +120,8 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration {
100120
staticBuildConfigurationFile: String? = nil,
101121
swiftFilterInclude: [String]? = nil,
102122
swiftFilterExclude: [String]? = nil,
103-
importedModuleStubs: [String: [String]]? = nil
123+
importedModuleStubs: [String: [String]]? = nil,
124+
availableImportModules: Set<String> = []
104125
) {
105126
self.swiftModule = swiftModule
106127
self.swiftExtractAccessLevel = accessLevel
@@ -110,5 +131,6 @@ public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration {
110131
self.swiftFilterInclude = swiftFilterInclude
111132
self.swiftFilterExclude = swiftFilterExclude
112133
self.importedModuleStubs = importedModuleStubs
134+
self.availableImportModules = availableImportModules
113135
}
114136
}

Sources/SwiftExtract/SwiftExtractDefaultBuildConfiguration.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,41 @@ 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. another language code generator can
109+
/// declare its own runtime module importable for extraction).
110+
package struct ImportOverlayBuildConfiguration<Base: BuildConfiguration>: BuildConfiguration {
111+
package var base: Base
112+
package var availableImportModules: Set<String>
113+
114+
package init(base: Base, availableImportModules: Set<String>) {
115+
self.base = base
116+
self.availableImportModules = availableImportModules
117+
}
118+
119+
package func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool {
120+
// `importPath` is the dotted module path; the leading component is the module.
121+
if let moduleName = importPath.first?.1, availableImportModules.contains(moduleName) {
122+
return true
123+
}
124+
return try base.canImport(importPath: importPath, version: version)
125+
}
126+
127+
package func isCustomConditionSet(name: String) throws -> Bool { try base.isCustomConditionSet(name: name) }
128+
package func hasFeature(name: String) throws -> Bool { try base.hasFeature(name: name) }
129+
package func hasAttribute(name: String) throws -> Bool { try base.hasAttribute(name: name) }
130+
package func isActiveTargetOS(name: String) throws -> Bool { try base.isActiveTargetOS(name: name) }
131+
package func isActiveTargetArchitecture(name: String) throws -> Bool { try base.isActiveTargetArchitecture(name: name) }
132+
package func isActiveTargetEnvironment(name: String) throws -> Bool { try base.isActiveTargetEnvironment(name: name) }
133+
package func isActiveTargetRuntime(name: String) throws -> Bool { try base.isActiveTargetRuntime(name: name) }
134+
package func isActiveTargetPointerAuthentication(name: String) throws -> Bool { try base.isActiveTargetPointerAuthentication(name: name) }
135+
package func isActiveTargetObjectFormat(name: String) throws -> Bool { try base.isActiveTargetObjectFormat(name: name) }
136+
package var targetPointerBitWidth: Int { base.targetPointerBitWidth }
137+
package var targetAtomicBitWidths: [Int] { base.targetAtomicBitWidths }
138+
package var endianness: Endianness { base.endianness }
139+
package var languageVersion: VersionTuple { base.languageVersion }
140+
package var compilerVersion: VersionTuple { base.compilerVersion }
141+
}

0 commit comments

Comments
 (0)