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
37 changes: 30 additions & 7 deletions Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,28 @@ public struct SwiftFunctionType: Equatable {
public var resultType: SwiftType
public var isEscaping: Bool = false

public var effectSpecifiers: [SwiftEffectSpecifier] = []

public var thrownTypedError: SwiftType? = nil

public var isAsync: Bool { effectSpecifiers.contains(.async) }
public var isThrowing: Bool { effectSpecifiers.contains(.throws) }
public var isTypedThrowing: Bool { thrownTypedError != nil }

public init(
convention: Convention,
parameters: [SwiftParameter],
resultType: SwiftType,
isEscaping: Bool = false
isEscaping: Bool = false,
effectSpecifiers: [SwiftEffectSpecifier] = [],
thrownTypedError: SwiftType? = nil
) {
self.convention = convention
self.parameters = parameters
self.resultType = resultType
self.isEscaping = isEscaping
self.effectSpecifiers = effectSpecifiers
self.thrownTypedError = thrownTypedError
}
}

Expand All @@ -47,7 +59,14 @@ extension SwiftFunctionType: CustomStringConvertible {
case .swift: ""
}
let escapingPrefix = isEscaping ? "@escaping " : ""
return "\(escapingPrefix)\(conventionPrefix)(\(parameterString)) -> \(resultType.description)"
let throwsString =
switch (isThrowing, thrownTypedError) {
case (true, .some(let errorType)): " throws(\(errorType.description))"
case (true, .none): " throws"
case (false, _): ""
}
let effectsSuffix = (isAsync ? " async" : "") + throwsString
return "\(escapingPrefix)\(conventionPrefix)(\(parameterString))\(effectsSuffix) -> \(resultType.description)"
}
}

Expand All @@ -70,12 +89,16 @@ extension SwiftFunctionType {

self.resultType = try SwiftType(node.returnClause.type, lookupContext: lookupContext)

// check for effect specifiers
if let throwsClause = node.effectSpecifiers?.throwsClause {
throw SwiftFunctionTranslationError.throws(throwsClause)
var effectSpecifiers: [SwiftEffectSpecifier] = []
if node.effectSpecifiers?.asyncSpecifier != nil {
effectSpecifiers.append(.async)
}
if let asyncSpecifier = node.effectSpecifiers?.asyncSpecifier {
throw SwiftFunctionTranslationError.async(asyncSpecifier)
if let throwsClause = node.effectSpecifiers?.throwsClause {
effectSpecifiers.append(.throws)
if let errorTypeNode = throwsClause.type {
self.thrownTypedError = try? SwiftType(errorTypeNode, lookupContext: lookupContext)
}
}
self.effectSpecifiers = effectSpecifiers
}
}
129 changes: 129 additions & 0 deletions Tests/SwiftExtractTests/FunctionTypeEffectSpecifierTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//===----------------------------------------------------------------------===//
//
// 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

@Suite("Function type effect specifiers")
struct FunctionTypeEffectSpecifierSuite {

private func closureParameterType(_ source: String) throws -> SwiftFunctionType {
let result = try analyze(
sources: [("/fake/Source.swift", source)],
moduleName: "Test"
)

let fn = try #require(result.extractedGlobalFuncs.first { $0.name == "take" })
guard case .function(let fnType) = fn.functionSignature.parameters[0].type else {
throw TestError("expected .function parameter, got \(fn.functionSignature.parameters[0].type)")
}
return fnType
}

@Test
func asyncClosureRecordsAsync() throws {
let fnType = try closureParameterType("public func take(_ cb: () async -> Void) {}")

#expect(fnType.effectSpecifiers == [.async])
#expect(fnType.isAsync)
#expect(!fnType.isThrowing)
}

@Test
func throwingClosureRecordsThrows() throws {
let fnType = try closureParameterType("public func take(_ cb: () throws -> Void) {}")

#expect(fnType.effectSpecifiers == [.throws])
#expect(!fnType.isAsync)
#expect(fnType.isThrowing)
#expect(!fnType.isTypedThrowing)
#expect(fnType.thrownTypedError == nil)
}

@Test
func typedThrowsClosureRecordsThrownErrorType() throws {
let fnType = try closureParameterType(
"""
public struct FishTankError: Error {}
public func take(_ cb: () throws(FishTankError) -> Void) {}
"""
)

#expect(fnType.effectSpecifiers == [.throws])
#expect(fnType.isThrowing)
#expect(fnType.isTypedThrowing)
#expect(fnType.thrownTypedError?.description == "FishTankError")
}

@Test
func unresolvableThrownErrorTypeStillRecordsThrows() throws {
let fnType = try closureParameterType("public func take(_ cb: () throws(NoSuchError) -> Void) {}")

#expect(fnType.effectSpecifiers == [.throws])
#expect(fnType.isThrowing)
#expect(fnType.thrownTypedError == nil)
}

@Test
func asyncThrowingClosureRecordsBoth() throws {
let fnType = try closureParameterType("public func take(_ cb: () async throws -> Void) {}")

#expect(fnType.effectSpecifiers == [.async, .throws])
#expect(fnType.isAsync)
#expect(fnType.isThrowing)
}

@Test
func descriptionRendersEffectSpecifiers() throws {
let fnType = try closureParameterType(
"public func take(_ cb: @escaping (Int) async throws -> Void) {}"
)

#expect(fnType.description == "@escaping (Int) async throws -> Void")
}

@Test
func descriptionRendersThrownErrorType() throws {
let fnType = try closureParameterType(
"""
public struct FishTankError: Error {}
public func take(_ cb: @escaping (Int) async throws(FishTankError) -> Void) {}
"""
)

#expect(fnType.description == "@escaping (Int) async throws(FishTankError) -> Void")
}

@Test
func effectsOnReturnedClosureAreRecorded() throws {
let result = try analyze(
sources: [("/fake/Source.swift", "public func get() -> () async -> Void { fatalError() }")],
moduleName: "Test"
)

let fn = try #require(result.extractedGlobalFuncs.first { $0.name == "get" })
guard case .function(let fnType) = fn.functionSignature.result.type else {
Issue.record("expected .function result, got \(fn.functionSignature.result.type)")
return
}
#expect(fnType.effectSpecifiers == [.async])
}
}

private struct TestError: Error, CustomStringConvertible {
let description: String
init(_ description: String) {
self.description = description
}
}
Loading