Skip to content

Initial SwiftExtract version - a reusable Swift analysis module#774

Merged
ktoso merged 39 commits into
swiftlang:mainfrom
ktoso:wip-swift-extract
Jun 16, 2026
Merged

Initial SwiftExtract version - a reusable Swift analysis module#774
ktoso merged 39 commits into
swiftlang:mainfrom
ktoso:wip-swift-extract

Conversation

@ktoso

@ktoso ktoso commented May 27, 2026

Copy link
Copy Markdown
Collaborator

This is an initial PR to extract a lot of the analysis code into an reusable SwiftExtract module. The module has no source compatibility guarantees at this point. The intent is to facilitate other language source generators that can reuse the same analysis core which is "the tricky bit".

This fully rebases swift-java onto this shared infrastructure, without loss of features; and even gains a few along the way, like being better prepared for lazy specialization with typealiases.

This is a massive "move stuff around" so reviewing everyting might be hard but the long term benefit will be massive.

@ktoso ktoso changed the title Separate out SwiftExcract, a reusable Swift analysis module [WIP] Introduce SwiftExcract, a reusable Swift analysis module May 27, 2026
@ktoso ktoso force-pushed the wip-swift-extract branch from b5fb44d to a434174 Compare May 27, 2026 07:11
@ktoso ktoso changed the title [WIP] Introduce SwiftExcract, a reusable Swift analysis module [WIP] Introduce SwiftExtract, a reusable Swift analysis module May 27, 2026
@ktoso ktoso changed the title [WIP] Introduce SwiftExtract, a reusable Swift analysis module [WIP] Work towards SwiftExtract - a reusable Swift analysis module May 27, 2026
@ktoso ktoso changed the title [WIP] Work towards SwiftExtract - a reusable Swift analysis module [WIP] Work towards SwiftExtract, a reusable Swift analysis module May 27, 2026
@ktoso ktoso force-pushed the wip-swift-extract branch 4 times, most recently from ac9071e to e23ae2c Compare June 3, 2026 13:00
ktoso added 6 commits June 15, 2026 13:18
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.
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.
@ktoso ktoso force-pushed the wip-swift-extract branch from 155632d to b618038 Compare June 15, 2026 04:54
ktoso added 14 commits June 15, 2026 15:43
…zer 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.
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.
…Modules

`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<Fish>` 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`.
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.
… 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.
…hared

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.
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.
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.
…n specs

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<Args…>` 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.
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)
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<N, T>`, underscore + hex counts, return
positions, nested arrays, and description round-trip.
- 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.
ktoso added 4 commits June 15, 2026 15:43
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`).
…AccessLevelMode

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`).
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.
The 386a749 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.<member>` access in the visitor and analyzer
  becomes `analyzer.<member>`.
- `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.
@ktoso ktoso force-pushed the wip-swift-extract branch from b618038 to cf06810 Compare June 15, 2026 06:56
ktoso added 3 commits June 15, 2026 16:36
…ractDecider

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.
…tExtractDecider -> DefaultAccessLevelExtractDecider

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<Fish>',
'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.
…nresolvedTypePlaceholder flag

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.
@ktoso ktoso force-pushed the wip-swift-extract branch from ea5364b to 8bc4524 Compare June 15, 2026 09:32
@ktoso ktoso changed the title [WIP] Work towards SwiftExtract, a reusable Swift analysis module Initial SwiftExtract version - a reusable Swift analysis module Jun 15, 2026
@ktoso ktoso marked this pull request as ready for review June 16, 2026 08:07
ktoso added 11 commits June 16, 2026 17:07
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.
…p bridge

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.
…ntax

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`.
…nown names into a constant

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.
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.
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)'.
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
…x param order

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).
…tion.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.
…edTypeReferences

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`.
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`.
@ktoso ktoso force-pushed the wip-swift-extract branch from 10a933d to 1a3b3c8 Compare June 16, 2026 08:25
@ktoso

ktoso commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

I deeply reviewed all the files and validated against a few consumers, I think this is good enough first stab at the split.

@ktoso ktoso merged commit 65af850 into swiftlang:main Jun 16, 2026
41 of 42 checks passed
@ktoso ktoso deleted the wip-swift-extract branch June 16, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant