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
7 changes: 2 additions & 5 deletions Sources/SafeDICore/Generators/DependencyTreeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public actor DependencyTreeGenerator {
// Create mock-root ScopeGenerators using the production Scope tree.
var seen = Set<TypeDescription>()
return try await withThrowingTaskGroup(
of: GeneratedRoot?.self,
of: GeneratedRoot.self,
returning: [GeneratedRoot].self,
) { taskGroup in
for instantiable in typeDescriptionToFulfillingInstantiableMap.values
Expand Down Expand Up @@ -134,7 +134,6 @@ public actor DependencyTreeGenerator {
mockConditionalCompilation: mockConditionalCompilation,
)),
)
guard !code.isEmpty else { return nil }
return GeneratedRoot(
typeDescription: instantiable.concreteInstantiable,
sourceFilePath: instantiable.sourceFilePath,
Expand All @@ -144,9 +143,7 @@ public actor DependencyTreeGenerator {
}
var generatedRoots = [GeneratedRoot]()
for try await generatedRoot in taskGroup {
if let generatedRoot {
generatedRoots.append(generatedRoot)
}
generatedRoots.append(generatedRoot)
}
return generatedRoots
}
Expand Down
12 changes: 5 additions & 7 deletions Sources/SafeDICore/Models/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,11 @@ public struct Property: Codable, Hashable, Comparable, Sendable {
name: "escaping",
trailingTrivia: .space,
))
if let attributes {
for attribute in attributes {
AttributeSyntax(
attributeName: IdentifierTypeSyntax(name: .identifier(attribute)),
trailingTrivia: .space,
)
}
for attribute in attributes {
AttributeSyntax(
attributeName: IdentifierTypeSyntax(name: .identifier(attribute)),
trailingTrivia: .space,
)
}
},
baseType: IdentifierTypeSyntax(name: .identifier(typeDescription.asSource)),
Expand Down
84 changes: 31 additions & 53 deletions Sources/SafeDICore/Models/TypeDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
/// A meta type. e.g. `Int.Type` or `Equatable.Protocol`
indirect case metatype(TypeDescription, isType: Bool)
/// A type identifier with a specifier or attributes. e.g. `inout Int` or `@autoclosure () -> Void`
indirect case attributed(TypeDescription, specifiers: [String]?, attributes: [String]?)
indirect case attributed(TypeDescription, specifiers: [String], attributes: [String])
/// An array. e.g. [Int]
indirect case array(element: TypeDescription)
/// A dictionary. e.g. [Int: String]
Expand Down Expand Up @@ -128,28 +128,13 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
case let .any(type):
return "any \(type.wrappedIfAmbiguous.asSource)"
case let .attributed(type, specifiers, attributes):
func attributesFromList(_ attributes: [String]) -> String {
attributes
.map { "@\($0)" }
.joined(separator: " ")
}
return switch (specifiers, attributes) {
case let (.some(specifiers), .none):
"\(specifiers.joined(separator: " ")) \(type.asSource)"
case let (.none, .some(attributes)):
"\(attributesFromList(attributes)) \(type.asSource)"
case let (.some(specifiers), .some(attributes)):
// This case likely represents an error.
// We are unaware of type reference that compiles with both a specifier and attributes.
// The Swift reference manual specifies that attributes come before the specifier,
// however code that puts an attribute first does not parse as AttributedTypeSyntax.
// Only code where the specifier comes before the attribute parses as an AttributedTypeSyntax.
// As a result, we construct this source with the specifier first.
// Reference manual: https://docs.swift.org/swift-book/ReferenceManual/Types.html#grammar_type
"\(specifiers.joined(separator: " ")) \(attributesFromList(attributes)) \(type.asSource)"
case (.none, .none):
type.asSource // This case represents an error.
}
// Specifiers come before attributes per the Swift reference manual:
// https://docs.swift.org/swift-book/ReferenceManual/Types.html#grammar_type
let prefix = [
specifiers.isEmpty ? nil : specifiers.joined(separator: " "),
attributes.isEmpty ? nil : attributes.map { "@\($0)" }.joined(separator: " "),
].compactMap(\.self).joined(separator: " ")
return "\(prefix) \(type.asSource)"
case let .array(element):
return "[\(element.asSource)]"
case let .dictionary(key, value):
Expand Down Expand Up @@ -251,16 +236,12 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
.void:
self
case .closure:
.attributed(self, specifiers: nil, attributes: ["escaping"])
case let .attributed(type, specifiers: specifiers, attributes: attributes):
if let attributes {
if attributes.contains(where: { $0 == "escaping" }) {
.attributed(type, specifiers: specifiers, attributes: attributes)
} else {
.attributed(type, specifiers: specifiers, attributes: ["escaping"] + attributes)
}
.attributed(self, specifiers: [], attributes: ["escaping"])
case let .attributed(type, specifiers, attributes):
if attributes.contains(where: { $0 == "escaping" }) {
.attributed(type, specifiers: specifiers, attributes: attributes)
} else {
.attributed(type, specifiers: specifiers, attributes: ["escaping"])
.attributed(type, specifiers: specifiers, attributes: ["escaping"] + attributes)
}
}
}
Expand Down Expand Up @@ -351,12 +332,9 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
case let .metatype(type, isType):
return "\(type.asIdentifier)_\(isType ? "Type" : "Protocol")"
case let .attributed(type, specifiers, attributes):
// .attributed always has at least one non-nil specifier or attribute
// (the parser sets nil-if-empty, and all programmatic constructors
// ensure at least one is present).
let prefix = [
specifiers?.joined(separator: "_"),
attributes?.joined(separator: "_"),
specifiers.isEmpty ? nil : specifiers.joined(separator: "_"),
attributes.isEmpty ? nil : attributes.joined(separator: "_"),
].compactMap(\.self).joined(separator: "_")
return "\(prefix)_\(type.asIdentifier)"
case let .array(element):
Expand All @@ -380,11 +358,11 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
public var strippingEscaping: TypeDescription {
switch self {
case let .attributed(type, specifiers, attributes):
let filtered = attributes?.filter { $0 != "escaping" }
if let filtered, !filtered.isEmpty {
let filtered = attributes.filter { $0 != "escaping" }
if !filtered.isEmpty {
return .attributed(type, specifiers: specifiers, attributes: filtered)
} else if let specifiers, !specifiers.isEmpty {
return .attributed(type, specifiers: specifiers, attributes: nil)
} else if !specifiers.isEmpty {
return .attributed(type, specifiers: specifiers, attributes: [])
} else {
return type
}
Expand Down Expand Up @@ -581,8 +559,8 @@ extension TypeSyntax {
}
return .attributed(
typeIdentifier.baseType.typeDescription,
specifiers: typeIdentifier.specifiers.textRepresentation,
attributes: attributes.isEmpty ? nil : attributes,
specifiers: typeIdentifier.specifiers.textRepresentation ?? [],
attributes: attributes,
)

} else if let typeIdentifier = ArrayTypeSyntax(self) {
Expand Down Expand Up @@ -662,19 +640,19 @@ extension ExprSyntax {
} else if let genericExpr = GenericSpecializationExprSyntax(self) {
let genericTypeVisitor = GenericArgumentVisitor(viewMode: .sourceAccurate)
genericTypeVisitor.walk(genericExpr.genericArgumentClause)
return switch genericExpr.expression.typeDescription {
case let .simple(name, _):
.simple(
name: name,
if let declReferenceExpr = DeclReferenceExprSyntax(genericExpr.expression) {
return .simple(
name: declReferenceExpr.baseName.text,
generics: genericTypeVisitor.genericArguments,
)
case let .nested(name, parentType, _):
.nested(
name: name,
parentType: parentType, generics: genericTypeVisitor.genericArguments,
} else if let memberAccessExpr = MemberAccessExprSyntax(genericExpr.expression),
let base = memberAccessExpr.base
{
return .nested(
name: memberAccessExpr.declName.baseName.text,
parentType: base.typeDescription,
generics: genericTypeVisitor.genericArguments,
)
case .any, .array, .attributed, .closure, .composition, .dictionary, .implicitlyUnwrappedOptional, .metatype, .optional, .some, .tuple, .unknown, .void:
.unknown(text: trimmedDescription)
}
} else if let tupleExpr = TupleExprSyntax(self) {
let tupleElements = tupleExpr.elements
Expand Down
4 changes: 2 additions & 2 deletions Sources/SafeDIMacros/Macros/InstantiableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1317,14 +1317,14 @@ extension TypeDescription {
)
|| self == .attributed(
.simple(name: "Instantiable"),
specifiers: nil,
specifiers: [],
attributes: ["retroactive"],
)
|| self == .nested(
name: "Instantiable",
parentType: .attributed(
.simple(name: "SafeDI"),
specifiers: nil,
specifiers: [],
attributes: ["retroactive"],
),
)
Expand Down
11 changes: 6 additions & 5 deletions Sources/SafeDITool/SafeDITool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import SafeDICore
import SwiftParser

@main
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
struct SafeDITool: AsyncParsableCommand {
// MARK: Arguments

Expand Down Expand Up @@ -507,24 +508,24 @@ struct SafeDITool: AsyncParsableCommand {
}

extension Data {
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
fileprivate func write(toPath filePath: String) throws {
try write(to: filePath.asFileURL)
}
}

extension String {
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
fileprivate func write(toPath filePath: String) throws {
try Data(utf8).write(toPath: filePath)
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
fileprivate var asFileURL: URL {
#if os(Linux)
return URL(fileURLWithPath: self)
URL(fileURLWithPath: self)
#else
guard #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) else {
return URL(fileURLWithPath: self)
}
return URL(filePath: self)
URL(filePath: self)
#endif
}
}
Expand Down
53 changes: 43 additions & 10 deletions Tests/SafeDICoreTests/TypeDescriptionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,19 @@ struct TypeDescriptionTests {
#expect(typeDescription.asSource == "(Int, Double) throws -> String")
}

@Test
func typeDescription_whenCalledOnATypeSyntaxNodeRepresentingASuppressedTypeSyntax_returnsUnknown() throws {
let content = """
var test: ~Copyable
"""

let visitor = SuppressedTypeSyntaxVisitor(viewMode: .sourceAccurate)
visitor.walk(Parser.parse(source: content))
let typeDescription = try #require(visitor.suppressedTypeIdentifier)
#expect(typeDescription.isUnknown)
#expect(typeDescription.asSource == "~Copyable")
}

@Test
func typeDescription_whenCalledOnAExprSyntaxNodeRepresentingAVoidType_findsTheType() throws {
let content = """
Expand Down Expand Up @@ -655,6 +668,18 @@ struct TypeDescriptionTests {
#expect(typeDescription.asSource == "() -> ()")
}

@Test
func typeDescription_whenCalledOnAExprSyntaxNodeRepresentingAClosureTypeWithArguments_findsTheType() throws {
let content = """
let test: Any.Type = ((Int, String) -> Bool).self
"""
let visitor = MemberAccessExprSyntaxVisitor(viewMode: .sourceAccurate)
visitor.walk(Parser.parse(source: content))
let typeDescription = try #require(visitor.typeDescription)
#expect(!typeDescription.isUnknown, "Type description is not of known type!")
#expect(typeDescription.asSource == "(Int, String) -> Bool")
}

@Test
func typeDescription_whenCalledOnAExprSyntaxNodeRepresentingAThrowingClosureType_findsTheType() throws {
let content = """
Expand Down Expand Up @@ -738,7 +763,7 @@ struct TypeDescriptionTests {
doesThrow: false,
returnType: .void(.identifier),
),
specifiers: nil,
specifiers: [],
attributes: ["autoclosure", "escaping"],
).asFunctionParameter == TypeDescription.attributed(
.closure(
Expand All @@ -747,7 +772,7 @@ struct TypeDescriptionTests {
doesThrow: false,
returnType: .void(.identifier),
),
specifiers: nil,
specifiers: [],
attributes: ["autoclosure", "escaping"],
))
}
Expand All @@ -761,7 +786,7 @@ struct TypeDescriptionTests {
doesThrow: false,
returnType: .void(.identifier),
),
specifiers: nil,
specifiers: [],
attributes: ["escaping", "autoclosure"],
).asFunctionParameter == TypeDescription.attributed(
.closure(
Expand All @@ -770,7 +795,7 @@ struct TypeDescriptionTests {
doesThrow: false,
returnType: .void(.identifier),
),
specifiers: nil,
specifiers: [],
attributes: ["escaping", "autoclosure"],
))
}
Expand All @@ -796,7 +821,7 @@ struct TypeDescriptionTests {
returnType: .void(.tuple),
),
specifiers: ["borrowing"],
attributes: nil,
attributes: [],
))
}

Expand All @@ -809,16 +834,16 @@ struct TypeDescriptionTests {
doesThrow: false,
returnType: .void(.identifier),
),
specifiers: nil,
attributes: nil,
specifiers: [],
attributes: [],
).asFunctionParameter == TypeDescription.attributed(
.closure(
arguments: [.void(.tuple)],
isAsync: false,
doesThrow: false,
returnType: .void(.identifier),
),
specifiers: nil,
specifiers: [],
attributes: ["escaping"],
))
}
Expand Down Expand Up @@ -857,13 +882,13 @@ struct TypeDescriptionTests {

@Test
func simplified_stripsAttributes() {
let type = TypeDescription.attributed(.simple(name: "Int"), specifiers: ["inout"], attributes: nil)
let type = TypeDescription.attributed(.simple(name: "Int"), specifiers: ["inout"], attributes: [])
#expect(type.simplified == .simple(name: "Int"))
}

@Test
func simplified_stripsNestedWrappers() {
let type = TypeDescription.optional(.attributed(.some(.simple(name: "Service")), specifiers: nil, attributes: ["Sendable"]))
let type = TypeDescription.optional(.attributed(.some(.simple(name: "Service")), specifiers: [], attributes: ["Sendable"]))
#expect(type.simplified == .simple(name: "Service"))
}

Expand Down Expand Up @@ -1522,6 +1547,14 @@ struct TypeDescriptionTests {
}
}

private final class SuppressedTypeSyntaxVisitor: SyntaxVisitor {
var suppressedTypeIdentifier: TypeDescription?
override func visit(_ node: SuppressedTypeSyntax) -> SyntaxVisitorContinueKind {
suppressedTypeIdentifier = TypeSyntax(node).typeDescription
return .skipChildren
}
}

private final class MemberAccessExprSyntaxVisitor: SyntaxVisitor {
var typeDescription: TypeDescription?
override func visit(_ node: MemberAccessExprSyntax) -> SyntaxVisitorContinueKind {
Expand Down
2 changes: 2 additions & 0 deletions Tests/SafeDIToolTests/Helpers/SafeDIToolTestExecution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import SafeDIRootScannerCore
import Testing
@testable import SafeDITool

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
func executeSafeDIToolTest(
swiftFileContent: [String],
additionalDirectorySwiftFileContent: [String] = [],
Expand Down Expand Up @@ -185,6 +186,7 @@ func executeSafeDIToolTest(
}
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
struct TestOutput {
let moduleInfo: SafeDITool.ModuleInfo
let moduleInfoOutputPath: String
Expand Down
Loading
Loading