Skip to content

Commit a07aadf

Browse files
committed
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.
1 parent d041c25 commit a07aadf

13 files changed

Lines changed: 230 additions & 31 deletions

Package.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,6 @@ let package = Package(
351351
.product(name: "SwiftSyntax", package: "swift-syntax"),
352352
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
353353
.product(name: "Logging", package: "swift-log"),
354-
"SwiftJavaConfigurationShared",
355354
],
356355
path: "Sources/SwiftExtract",
357356
resources: [
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import SwiftExtract
16+
import SwiftJavaConfigurationShared
17+
18+
/// Bridges swift-java's `Configuration` onto the language-neutral
19+
/// `SwiftExtractConfiguration` surface consumed by `SwiftExtract`.
20+
///
21+
/// Most members are satisfied directly by `Configuration`'s own properties; only
22+
/// the two enum-typed members need a mapping from swift-java's enums onto the
23+
/// neutral `AccessLevelMode` / `Logger.Level`.
24+
extension Configuration: SwiftExtractConfiguration {
25+
public var swiftExtractAccessLevel: AccessLevelMode {
26+
switch effectiveMinimumInputAccessLevelMode {
27+
case .public: .public
28+
case .package: .package
29+
case .internal: .internal
30+
}
31+
}
32+
33+
public var swiftExtractLogLevel: SwiftExtract.Logger.Level? {
34+
guard let logLevel else { return nil }
35+
switch logLevel {
36+
case .trace: return .trace
37+
case .debug: return .debug
38+
case .info: return .info
39+
case .notice: return .notice
40+
case .warning: return .warning
41+
case .error: return .error
42+
case .critical: return .critical
43+
}
44+
}
45+
}
46+
47+
extension LogLevel {
48+
/// Bridges from the analysis layer's neutral `Logger.Level` (used by the CLI's
49+
/// `--log-level` option) onto swift-java's own `LogLevel`.
50+
public init(_ level: SwiftExtract.Logger.Level) {
51+
switch level {
52+
case .trace: self = .trace
53+
case .debug: self = .debug
54+
case .info: self = .info
55+
case .notice: self = .notice
56+
case .warning: self = .warning
57+
case .error: self = .error
58+
case .critical: self = .critical
59+
}
60+
}
61+
}

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator {
8686

8787
// If we are forced to write empty files, construct the expected outputs.
8888
// It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway.
89-
if translator.config.effectiveWriteEmptyFiles {
89+
if config.effectiveWriteEmptyFiles {
9090
self.expectedOutputSwiftFileNames = Set(
9191
translator.inputs.compactMap { (input) -> String? in
9292
guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else {

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
8686

8787
// If we are forced to write empty files, construct the expected outputs.
8888
// It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway.
89-
if translator.config.effectiveWriteEmptyFiles {
89+
if config.effectiveWriteEmptyFiles {
9090
self.expectedOutputSwiftFileNames = Set(
9191
translator.inputs.compactMap { (input) -> String? in
9292
guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else {
@@ -117,7 +117,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
117117
self.expectedOutputSwiftFileNames = []
118118
}
119119

120-
if translator.config.enableJavaCallbacks ?? false {
120+
if config.enableJavaCallbacks ?? false {
121121
// We translate all the protocol wrappers
122122
// as we need them to know what protocols we can allow the user to implement themselves
123123
// in Java.

Sources/SwiftExtract/ExtractedDecls.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ public final class ExtractedNominalType: ExtractedSwiftDecl {
151151
self.swiftNominal.qualifiedName
152152
}
153153

154+
/// The attribute list on this type's declaration (e.g. `@resultBuilder`),
155+
/// for language targets that key behavior off attributes.
156+
/// Mirrors `ExtractedFunc.swiftDecl` being public for the function case.
157+
public var declAttributes: AttributeListSyntax {
158+
swiftNominal.syntax.attributes
159+
}
160+
161+
/// The declaration-group syntax for this type (protocol/struct/class/enum/
162+
/// actor), for language targets that need to inspect members or clauses the
163+
/// neutral model doesn't surface (e.g. a protocol's primary associated types).
164+
public var declGroupSyntax: any DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax {
165+
swiftNominal.syntax
166+
}
167+
154168
/// The output generic clause, e.g. "<Element>" for generic base types, "" for specialized or non-generic
155169
public var outputGenericClause: String {
156170
if isSpecialization {

Sources/SwiftExtract/Logger.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16-
import SwiftJavaConfigurationShared
1716
import SwiftSyntax
1817

1918
// Placeholder for some better logger, we could depend on swift-log
@@ -113,7 +112,19 @@ public struct Logger {
113112
}
114113

115114
extension Logger {
116-
public typealias Level = SwiftJavaConfigurationShared.LogLevel
115+
/// Log verbosity levels for the analysis layer's lightweight logger.
116+
///
117+
/// Language-neutral; language-specific configuration modules map their own
118+
/// log-level enums onto this via `SwiftExtractConfiguration`.
119+
public enum Level: String, Codable, Hashable, Sendable {
120+
case trace
121+
case debug
122+
case info
123+
case notice
124+
case warning
125+
case error
126+
case critical
127+
}
117128
}
118129

119130
extension Logger.Level {

Sources/SwiftExtract/SwiftAnalysisVisitor.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@
1515
import Foundation
1616
import Logging
1717
import SwiftIfConfig
18-
import SwiftJavaConfigurationShared
1918
import SwiftParser
2019
import SwiftSyntax
2120

2221
final class SwiftAnalysisVisitor {
2322
let translator: SwiftAnalyzer
24-
var config: Configuration {
23+
var config: any SwiftExtractConfiguration {
2524
self.translator.config
2625
}
2726

@@ -186,8 +185,14 @@ final class SwiftAnalysisVisitor {
186185

187186
switch node.name.tokenKind {
188187
case .binaryOperator, .prefixOperator, .postfixOperator:
189-
self.log.debug("Skip importing: '\(node.qualifiedNameForDebug)'; Operators are not supported.")
190-
return
188+
// Operators are extracted as ordinary `.function`s only when the target
189+
// opts in (other language code generators may map them to language
190+
// constructs in a post-pass). Most targets (e.g. Java) cannot express
191+
// Swift operators and skip them.
192+
guard config.extractsOperators else {
193+
self.log.debug("Skip importing: '\(node.qualifiedNameForDebug)'; Operators are not supported.")
194+
return
195+
}
191196
default:
192197
break
193198
}
@@ -720,13 +725,13 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn
720725
/// the result on a per-decl basis (e.g. Java honors `@JavaExport` /
721726
/// `@JavaClass` here)
722727
func shouldExtract(
723-
config: Configuration,
728+
config: any SwiftExtractConfiguration,
724729
log: Logger,
725730
in parent: ExtractedNominalType?,
726731
decider: (any ExtractDecider)?
727732
) -> Bool {
728733
let accessLevelPasses: Bool =
729-
switch config.effectiveMinimumInputAccessLevelMode {
734+
switch config.swiftExtractAccessLevel {
730735
case .public: self.isPublic(in: parent?.swiftNominal.syntax)
731736
case .package: self.isAtLeastPackage
732737
case .internal: self.isAtLeastInternal
@@ -744,7 +749,7 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn
744749

745750
if !accessLevelPasses {
746751
log.debug(
747-
"Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)"
752+
"Skip import '\(self.qualifiedNameForDebug)': not at least \(config.swiftExtractAccessLevel)"
748753
)
749754
}
750755
return accessLevelPasses

Sources/SwiftExtract/SwiftAnalyzer.swift

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import Foundation
1616
import Logging
1717
import SwiftIfConfig
18-
import SwiftJavaConfigurationShared
1918
import SwiftParser
2019
import SwiftSyntax
2120

@@ -30,7 +29,7 @@ public final class SwiftAnalyzer {
3029

3130
package var log: Logger
3231

33-
package let config: Configuration
32+
package let config: any SwiftExtractConfiguration
3433

3534
/// The build configuration used to resolve #if conditional compilation blocks.
3635
package let buildConfig: any BuildConfiguration
@@ -76,13 +75,14 @@ public final class SwiftAnalyzer {
7675
package let extractDecider: (any ExtractDecider)?
7776

7877
public init(
79-
config: Configuration,
78+
config: any SwiftExtractConfiguration,
79+
moduleName: String? = nil,
8080
extractDecider: (any ExtractDecider)? = nil
8181
) {
82-
guard let swiftModule = config.swiftModule else {
82+
guard let swiftModule = moduleName ?? config.swiftModule else {
8383
fatalError("Missing 'swiftModule' name.") // FIXME: can we make it required in config? but we shared config for many cases
8484
}
85-
self.log = Logger(label: "translator", logLevel: config.logLevel ?? .info)
85+
self.log = Logger(label: "translator", logLevel: config.swiftExtractLogLevel ?? .info)
8686
self.config = config
8787
self.swiftModuleName = swiftModule
8888
self.extractDecider = extractDecider
@@ -150,13 +150,12 @@ extension SwiftAnalyzer {
150150
public static func analyze(
151151
sources: [(path: String, text: String)],
152152
moduleName: String,
153-
config: Configuration? = nil,
153+
config: (any SwiftExtractConfiguration)? = nil,
154154
sourceDependencies: SourceDependencies = SourceDependencies(),
155155
extractDecider: (any ExtractDecider)? = nil
156156
) throws -> AnalysisResult {
157-
var effectiveConfig = config ?? Configuration()
158-
effectiveConfig.swiftModule = moduleName
159-
let translator = SwiftAnalyzer(config: effectiveConfig, extractDecider: extractDecider)
157+
let effectiveConfig = config ?? DefaultSwiftExtractConfiguration(swiftModule: moduleName)
158+
let translator = SwiftAnalyzer(config: effectiveConfig, moduleName: moduleName, extractDecider: extractDecider)
160159
translator.sourceDependencies = sourceDependencies
161160
for source in sources {
162161
translator.add(filePath: source.path, text: source.text)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// Minimum access level a declaration must have to be considered for extraction.
16+
///
17+
/// Language-neutral counterpart to a configuration's access-level setting. The
18+
/// concrete `Configuration` types in language layers (e.g. swift-java, or other
19+
/// language code generators) map their own enums onto this.
20+
public enum AccessLevelMode: String, Sendable {
21+
case `public`
22+
case `package`
23+
case `internal`
24+
}
25+
26+
/// The configuration surface required by the language-neutral `SwiftExtract`
27+
/// analysis layer.
28+
///
29+
/// `SwiftExtract` deliberately does NOT depend on any language-specific
30+
/// configuration module. Instead, each language layer makes its own
31+
/// `Configuration` type conform to this protocol, mapping its settings onto the
32+
/// neutral surface below. This keeps the analysis layer reusable across targets
33+
/// (e.g. Java/JNI/FFM, or other language code generators) without pulling
34+
/// target-specific config types into `SwiftExtract`.
35+
///
36+
/// The two enum-typed members use `swiftExtract`-prefixed names so a conforming
37+
/// type can keep its own, differently-typed `logLevel` /
38+
/// `effectiveMinimumInputAccessLevelMode` members without a name collision.
39+
public protocol SwiftExtractConfiguration {
40+
/// Name of the Swift module being analyzed.
41+
var swiftModule: String? { get }
42+
43+
/// Optional path to a JSON `StaticBuildConfiguration` used to resolve `#if`.
44+
var staticBuildConfigurationFile: String? { get }
45+
46+
/// Glob patterns selecting which Swift files/types to extract.
47+
var swiftFilterInclude: [String]? { get }
48+
49+
/// Glob patterns excluding Swift files/types from extraction.
50+
var swiftFilterExclude: [String]? { get }
51+
52+
/// Stub declarations for imported modules whose source is unavailable to the
53+
/// analyzer. Keyed by module name; values are Swift declaration strings parsed
54+
/// as if they belonged to that module.
55+
var importedModuleStubs: [String: [String]]? { get }
56+
57+
/// Minimum access level required for a declaration to be extracted.
58+
var swiftExtractAccessLevel: AccessLevelMode { get }
59+
60+
/// Whether operator declarations (e.g. `static func + (…)`) should be
61+
/// extracted as ordinary `.function`s. Most targets (e.g. Java) cannot express
62+
/// Swift operators and leave this `false`; other language code generators that
63+
/// map operators to language constructs set it `true` and recognize the
64+
/// operator functions in a post-analysis pass.
65+
var extractsOperators: Bool { get }
66+
67+
/// Verbosity for the analyzer's logger; `nil` falls back to `.info`.
68+
var swiftExtractLogLevel: Logger.Level? { get }
69+
70+
/// Whether the given module name has stub declarations configured.
71+
func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool
72+
}
73+
74+
extension SwiftExtractConfiguration {
75+
public var extractsOperators: Bool { false }
76+
77+
public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool {
78+
importedModuleStubs?.keys.contains(moduleName) ?? false
79+
}
80+
}
81+
82+
/// A minimal, self-contained `SwiftExtractConfiguration` for callers that only
83+
/// need analysis (tests, tools) and don't have a richer language-specific
84+
/// configuration to supply.
85+
public struct DefaultSwiftExtractConfiguration: SwiftExtractConfiguration {
86+
public var swiftModule: String?
87+
public var staticBuildConfigurationFile: String?
88+
public var swiftFilterInclude: [String]?
89+
public var swiftFilterExclude: [String]?
90+
public var importedModuleStubs: [String: [String]]?
91+
public var swiftExtractAccessLevel: AccessLevelMode
92+
public var swiftExtractLogLevel: Logger.Level?
93+
public var extractsOperators: Bool
94+
95+
public init(
96+
swiftModule: String? = nil,
97+
accessLevel: AccessLevelMode = .public,
98+
logLevel: Logger.Level? = nil,
99+
extractsOperators: Bool = false,
100+
staticBuildConfigurationFile: String? = nil,
101+
swiftFilterInclude: [String]? = nil,
102+
swiftFilterExclude: [String]? = nil,
103+
importedModuleStubs: [String: [String]]? = nil
104+
) {
105+
self.swiftModule = swiftModule
106+
self.swiftExtractAccessLevel = accessLevel
107+
self.swiftExtractLogLevel = logLevel
108+
self.extractsOperators = extractsOperators
109+
self.staticBuildConfigurationFile = staticBuildConfigurationFile
110+
self.swiftFilterInclude = swiftFilterInclude
111+
self.swiftFilterExclude = swiftFilterExclude
112+
self.importedModuleStubs = importedModuleStubs
113+
}
114+
}

Sources/SwiftExtract/SwiftFileFilter.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import SwiftJavaConfigurationShared
16-
1715
// ==== -----------------------------------------------------------------------
1816
// MARK: Swift filter pattern classification
1917

@@ -195,7 +193,7 @@ package func matchesTypeNameFilter(qualifiedName: String, pattern: String) -> Bo
195193
/// Only file-path patterns (containing `/`) and plain patterns (no `/` or `.`)
196194
/// are checked here. Type-name patterns are skipped — use
197195
/// `shouldExtractSwiftType` for those
198-
package func shouldExtractSwiftFile(relativePath: String, config: Configuration) -> Bool {
196+
package func shouldExtractSwiftFile(relativePath: String, config: any SwiftExtractConfiguration) -> Bool {
199197
if let includeFilters = config.swiftFilterInclude, !includeFilters.isEmpty {
200198
// Must match at least one file-level include pattern.
201199
// 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 {
246244
/// Only type-name patterns (containing `.`) and plain patterns (no `/` or `.`)
247245
/// are checked here. File-path patterns are skipped — use `shouldExtractSwiftFile`
248246
/// for those
249-
package func shouldExtractSwiftType(qualifiedName: String, config: Configuration) -> Bool {
247+
package func shouldExtractSwiftType(qualifiedName: String, config: any SwiftExtractConfiguration) -> Bool {
250248
if let includeFilters = config.swiftFilterInclude, !includeFilters.isEmpty {
251249
let typePatterns = includeFilters.filter { classifyPattern($0) != .filePath }
252250
if !typePatterns.isEmpty {

0 commit comments

Comments
 (0)