From 807fdff8902a1a235e9dd838da55ce4e01e69f8c Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 23 Jun 2026 18:12:23 +0900 Subject: [PATCH] SwiftExtract: Implement lookForGenericParameters logic This allows us getting generic parameter names from extensions, including unconstrained extensions. This way we can get Box's T when we're in an `extension Box` --- Sources/SwiftExtract/ExtractedDecls.swift | 6 + .../SwiftTypes/SwiftTypeLookupContext.swift | 30 +++- .../ExtensionGenericLookupTests.swift | 147 ++++++++++++++++++ 3 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 Tests/SwiftExtractTests/ExtensionGenericLookupTests.swift diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index f4686e74e..580bddb47 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -68,6 +68,12 @@ public final class ExtractedNominalType: ExtractedSwiftDecl { swiftNominal.genericParameters.map(\.name) } + /// Generic parameter declarations (e.g. `[Element]` for `Box`). + /// Empty for non-generic types. + public var genericParameters: [SwiftGenericParameterDeclaration] { + swiftNominal.genericParameters + } + /// Maps generic parameter -> concrete type argument. Empty for unspecialized types /// e.g. {"Element": "Fish"} for FishBox public var genericArguments: [String: String] = [:] diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift index 31d66f86f..a07ee6e78 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftTypeLookupContext.swift @@ -76,9 +76,17 @@ public class SwiftTypeLookupContext { } case .lookForGenericParameters(let extensionNode): - // TODO: Implement - _ = extensionNode - break + // Resolve the extension's extended type and consult its generic + // parameter list, so a bare `Element` reference inside an + // unconstrained `extension Box { ... }` resolves to + // `Box.Element`. + guard let extendedNominal = extendedNominal(of: extensionNode) else { + break + } + guard let genericParam = extendedNominal.genericParameters.first(where: { $0.name == name.name }) else { + break + } + return genericParam case .lookForImplicitClosureParameters: // Dollar identifier can't be a type, ignore. @@ -222,6 +230,22 @@ public class SwiftTypeLookupContext { defer { resolvingAliases.remove(id) } return try SwiftType(decl.syntax.initializer.value, lookupContext: self) } + + private func extendedNominal(of extensionNode: ExtensionDeclSyntax) -> SwiftNominalTypeDeclaration? { + func resolve(_ extendedType: TypeSyntax) -> SwiftNominalTypeDeclaration? { + if let id = extendedType.as(IdentifierTypeSyntax.self) { + return symbolTable.lookupTopLevelNominalType(id.name.text) + } + if let member = extendedType.as(MemberTypeSyntax.self) { + // Recursively resolve the parent chain: for `Outer.Inner`, find Outer + // first, then look up Inner as its nested type. + guard let parent = resolve(member.baseType) else { return nil } + return symbolTable.lookupNestedType(member.name.text, parent: parent) + } + return nil + } + return resolve(extensionNode.extendedType) + } } public enum TypeLookupError: Error { diff --git a/Tests/SwiftExtractTests/ExtensionGenericLookupTests.swift b/Tests/SwiftExtractTests/ExtensionGenericLookupTests.swift new file mode 100644 index 000000000..87f36deb1 --- /dev/null +++ b/Tests/SwiftExtractTests/ExtensionGenericLookupTests.swift @@ -0,0 +1,147 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftExtract +import Testing + +/// Verifies that bare references to a generic parameter from inside an +/// extension on the corresponding generic type resolve correctly. +@Suite("Extension generic parameter lookup") +struct ExtensionGenericLookupSuite { + + // ==== ----------------------------------------------------------------------- + // MARK: Unconstrained extension parameter and return types + + @Test func unconstrainedExtensionParamResolvesGenericParameter() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Box {} + extension Box { + public mutating func append(_ x: Element) {} + } + """ + ) + ], + moduleName: "Test" + ) + + let box = try #require(result.extractedTypes["Box"]) + let append = try #require(box.methods.first { $0.name == "append" }) + let paramType = append.functionSignature.parameters[0].type + + guard case .genericParameter(let decl) = paramType else { + Issue.record("expected .genericParameter(Element), got \(paramType)") + return + } + #expect(decl.name == "Element") + } + + @Test func unconstrainedExtensionReturnResolvesGenericParameter() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Box { + private var storage: [Element] = [] + } + extension Box { + public func first() -> Element { storage[0] } + } + """ + ) + ], + moduleName: "Test" + ) + + let box = try #require(result.extractedTypes["Box"]) + let first = try #require(box.methods.first { $0.name == "first" }) + let returnType = first.functionSignature.result.type + + guard case .genericParameter(let decl) = returnType else { + Issue.record("expected .genericParameter(Element), got \(returnType)") + return + } + #expect(decl.name == "Element") + } + + // ==== ----------------------------------------------------------------------- + // MARK: Constrained extensions still resolve the generic parameter + + @Test func constrainedExtensionParamResolvesGenericParameter() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Box { + public init() {} + } + public typealias IntBox = Box + extension Box where Element == Int { + public mutating func replace(_ x: Element) {} + } + """ + ) + ], + moduleName: "Test" + ) + + // The constrained extension folds into the IntBox specialization. + let intBox = try #require(result.extractedTypes["IntBox"]) + let replace = try #require(intBox.methods.first { $0.name == "replace" }) + let paramType = replace.functionSignature.parameters[0].type + + guard case .genericParameter(let decl) = paramType else { + Issue.record("expected .genericParameter(Element), got \(paramType)") + return + } + #expect(decl.name == "Element") + } + + // ==== ----------------------------------------------------------------------- + // MARK: ExtractedNominalType.genericParameters typed accessor + + @Test func extractedNominalExposesGenericParameters() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public struct Tag {} + public struct Box { + public init() {} + } + public struct Pair {} + """ + ) + ], + moduleName: "Test" + ) + + let tag = try #require(result.extractedTypes["Tag"]) + #expect(tag.genericParameters.isEmpty) + + let box = try #require(result.extractedTypes["Box"]) + #expect(box.genericParameters.count == 1) + #expect(box.genericParameters[0].name == "Element") + #expect(box.genericParameterNames == ["Element"]) + + let pair = try #require(result.extractedTypes["Pair"]) + #expect(pair.genericParameters.map(\.name) == ["Key", "Value"]) + } +}