Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 161 additions & 1 deletion Sources/SwiftExtract/AnalysisResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,21 @@ public struct AnalysisResult {
public var extractedGlobalVariables: [ExtractedFunc]
public var extractedGlobalFuncs: [ExtractedFunc]

/// Extensions the analyzed module declares on nominal types from other modules.
///
/// Separate from `extractedTypes`, since we're not declaring types here at all, just extensions.
public var crossModuleExtensions: CrossModuleExtensions

public init(
extractedTypes: [SwiftTypeName: ExtractedNominalType],
extractedGlobalVariables: [ExtractedFunc],
extractedGlobalFuncs: [ExtractedFunc]
extractedGlobalFuncs: [ExtractedFunc],
crossModuleExtensions: CrossModuleExtensions = CrossModuleExtensions()
) {
self.extractedTypes = extractedTypes
self.extractedGlobalVariables = extractedGlobalVariables
self.extractedGlobalFuncs = extractedGlobalFuncs
self.crossModuleExtensions = crossModuleExtensions
}

/// Expands variadic functions into distinct overloads.
Expand All @@ -41,3 +48,156 @@ public struct AnalysisResult {
}
}
}

// ==== -----------------------------------------------------------------------
// MARK: Conformance queries

/// A conformance the analysis observed, either stated directly or inherited
public struct ObservedConformance {
/// How this conformance was derived
public enum Derivation: Equatable {
/// An inheritance clause named the protocol directly
case stated
/// Reached from a stated conformance by inheritance.
case inherited(via: [SwiftNominalIdentity])
}

/// The conforming nominal
public let type: SwiftNominalIdentity

/// The protocol it conforms to
public let protocolType: SwiftNominalIdentity

public let derivation: Derivation

/// Requirements guarding the *stated* conformance this was derived from.
/// Empty when the conformance is unconditional.
public let requirements: [SwiftGenericRequirement]

/// True when the guarding `where` clause held a requirement the analyzer could not
/// represent, so `requirements` is an incomplete picture.
///
/// This can happen when a not-extracted type is used in a requirement.
public let hasUnrepresentableRequirements: Bool

public init(
type: SwiftNominalIdentity,
protocolType: SwiftNominalIdentity,
derivation: Derivation,
requirements: [SwiftGenericRequirement],
hasUnrepresentableRequirements: Bool = false
) {
self.type = type
self.protocolType = protocolType
self.derivation = derivation
self.requirements = requirements
self.hasUnrepresentableRequirements = hasUnrepresentableRequirements
}

/// Whether this conformance applies only under a `where` clause
public var isConditional: Bool {
!requirements.isEmpty || hasUnrepresentableRequirements
}
}

extension AnalysisResult {

/// Every nominal the analysis observed to conform to `protocolType`.
public func typesConforming(to protocolType: SwiftNominalIdentity) -> [ObservedConformance] {
var results: [ObservedConformance] = []
var refinementCache: [SwiftNominalIdentity: [SwiftNominalIdentity]?] = [:]

/// The protocols walked from `stated` up to and including `protocolType`, or nil when
/// `stated` does not refine it. Empty when `stated` *is* `protocolType`
func refinementPath(from stated: SwiftNominalIdentity) -> [SwiftNominalIdentity]? {
if let cached = refinementCache[stated] { return cached }
// Seed the cache before recursing so a cyclic inheritance clause in malformed
// input cannot spin forever
refinementCache[stated] = .some(nil)
var path: [SwiftNominalIdentity]? = nil
if stated == protocolType {
path = []
} else {
for parent in inheritedProtocolIdentities(of: stated) {
if let rest = refinementPath(from: parent) {
path = [parent] + rest
break
}
}
}
refinementCache[stated] = .some(path)
return path
}

func consider(
conformer: SwiftNominalIdentity,
stated: SwiftNominalIdentity,
requirements: [SwiftGenericRequirement],
hasUnrepresentableRequirements: Bool
) {
guard let path = refinementPath(from: stated) else { return }
// `path` ends at `protocolType`. The reportable chain starts at what source
// actually named and stops short of the protocol being asked about
let via = Array(([stated] + path).dropLast())
results.append(
ObservedConformance(
type: conformer,
protocolType: protocolType,
derivation: path.isEmpty ? .stated : .inherited(via: via),
requirements: requirements,
hasUnrepresentableRequirements: hasUnrepresentableRequirements
)
)
}

// Types this module declares, from their own inheritance clauses
for name in extractedTypes.keys.sorted() {
guard let type = extractedTypes[name], !type.swiftNominal.isProtocolKind else { continue }
let conformer = type.identity
for stated in protocolIdentities(in: type.inheritedTypes) {
consider(
conformer: conformer,
stated: stated,
requirements: [],
hasUnrepresentableRequirements: false
)
}
}

// Other module's types, from the extensions that gave them conformances
for record in crossModuleExtensions.all {
for stated in protocolIdentities(in: record.addedConformances) {
consider(
conformer: record.extendedType,
stated: stated,
requirements: record.requirements,
hasUnrepresentableRequirements: record.hasUnrepresentableRequirements
)
}
}

return results
}

/// The protocols `protocolType` refines, as far as this analysis can see them.
///
/// Resolved through `extractedTypes`.
private func inheritedProtocolIdentities(of protocolType: SwiftNominalIdentity) -> [SwiftNominalIdentity] {
guard let declared = extractedTypes[protocolType.qualifiedName] else { return [] }
return protocolIdentities(in: declared.inheritedTypes)
}

private func protocolIdentities(in types: [SwiftType]) -> [SwiftNominalIdentity] {
types.compactMap { type in
guard let decl = type.asNominalTypeDeclaration, decl.isProtocolKind else { return nil }
return decl.identity
}
}
}

extension SwiftNominalTypeDeclaration {
/// Whether this declaration is a protocol
var isProtocolKind: Bool {
kind == .protocol
}
}
157 changes: 157 additions & 0 deletions Sources/SwiftExtract/CrossModuleExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//===----------------------------------------------------------------------===//
//
// 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

/// The members a single extension contributes to the type it extends
public struct CrossModuleExtensionMembers {
public var initializers: [ExtractedFunc]
public var methods: [ExtractedFunc]
public var variables: [ExtractedFunc]

public init(
initializers: [ExtractedFunc] = [],
methods: [ExtractedFunc] = [],
variables: [ExtractedFunc] = []
) {
self.initializers = initializers
self.methods = methods
self.variables = variables
}

public var isEmpty: Bool {
initializers.isEmpty && methods.isEmpty && variables.isEmpty
}
}

// ==== -----------------------------------------------------------------------
// MARK: A cross-module extension

/// One `extension` the analyzed sources declare on a nominal type from another module
public struct CrossModuleExtension {
/// The other module's nominal being extended
public let extendedType: SwiftNominalIdentity

/// The extension declaration syntax
public let syntax: ExtensionDeclSyntax

/// Protocols this extension adds, resolved.
public let addedConformances: [SwiftType]

/// Members this extension contributes
public let members: CrossModuleExtensionMembers

/// The `where` clause requirements, resolved.
///
/// Empty for an unconditional extension, unless `hasUnrepresentableRequirements` in which case
/// this collection is missing the unrepresentable requirements.
public let requirements: [SwiftGenericRequirement]

/// True when the `where` clause held a requirement this analyzer cannot represent
/// (a layout requirement, or one whose types did not resolve).
public let hasUnrepresentableRequirements: Bool

/// The extension's own attributes, including e.g. `@available` among them.
public let attributes: AttributeListSyntax

public let sourceFilePath: String

public init(
extendedType: SwiftNominalIdentity,
syntax: ExtensionDeclSyntax,
addedConformances: [SwiftType],
members: CrossModuleExtensionMembers,
requirements: [SwiftGenericRequirement],
hasUnrepresentableRequirements: Bool = false,
attributes: AttributeListSyntax,
sourceFilePath: String
) {
self.extendedType = extendedType
self.syntax = syntax
self.addedConformances = addedConformances
self.members = members
self.requirements = requirements
self.hasUnrepresentableRequirements = hasUnrepresentableRequirements
self.attributes = attributes
self.sourceFilePath = sourceFilePath
}

/// Whether the conformances this extension adds apply only under its `where` clause
public var isConditional: Bool {
!requirements.isEmpty || hasUnrepresentableRequirements
}
}

/// An extension whose extended type could not be resolved at all.
///
/// A diagnostics channel rather than a worklist. These were previously discarded with
/// no trace, which made an unresolvable import indistinguishable from an empty extension
public struct UnresolvedExtension {
/// The extended type as written, e.g. "SomeUnknownModule.Widget"
public let extendedTypeDescription: String
public let syntax: ExtensionDeclSyntax
public let sourceFilePath: String

public init(extendedTypeDescription: String, syntax: ExtensionDeclSyntax, sourceFilePath: String) {
self.extendedTypeDescription = extendedTypeDescription
self.syntax = syntax
self.sourceFilePath = sourceFilePath
}
}

/// The cross-module extensions found by one analysis.
///
/// A module cannot declare a nominal type owned by another module, so an extension is
/// the only way the analyzed sources can add conformances or members to another module's
/// type.
public struct CrossModuleExtensions {
/// Every recorded cross-module extension, in the order encountered
public private(set) var all: [CrossModuleExtension] = []

/// Extensions whose extended type never resolved
public private(set) var unresolved: [UnresolvedExtension] = []

/// Index into `all`, keyed by extended type
private var byExtendedType: [SwiftNominalIdentity: [Int]] = [:]

public init() {}

public var isEmpty: Bool {
all.isEmpty && unresolved.isEmpty
}

public mutating func record(_ extension: CrossModuleExtension) {
byExtendedType[`extension`.extendedType, default: []].append(all.count)
all.append(`extension`)
}

public mutating func record(unresolved unresolvedExtension: UnresolvedExtension) {
unresolved.append(unresolvedExtension)
}

/// Every cross-module extension on `type`, in the order encountered
public func extensions(of type: SwiftNominalIdentity) -> [CrossModuleExtension] {
(byExtendedType[type] ?? []).map { all[$0] }
}

/// Every other module's nominal that some cross-module extension extends, in a stable order
public var extendedTypes: [SwiftNominalIdentity] {
var seen: Set<SwiftNominalIdentity> = []
var ordered: [SwiftNominalIdentity] = []
for record in all where seen.insert(record.extendedType).inserted {
ordered.append(record.extendedType)
}
return ordered
}
}
9 changes: 9 additions & 0 deletions Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ public final class ExtractedNominalType: ExtractedSwiftDecl {
return swiftNominal.qualifiedTypeName
}

/// This type's module-qualified identity, keyed on the output-facing name so a
/// specialization identifies as itself ("FishBox") rather than as its base ("Box").
///
/// `identity.qualifiedName` is therefore `effectiveTypeName`, the key this type is
/// registered under in `AnalysisResult.extractedTypes`
public var identity: SwiftNominalIdentity {
SwiftNominalIdentity(moduleName: swiftNominal.moduleName, typeName: effectiveOutputTypeName)
}

/// 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<Fish>`, the qualified base name (e.g. "Box")
Expand Down
Loading
Loading