diff --git a/Sources/ManagedModelMacros/ModelMacro/ModelMemberAttributes.swift b/Sources/ManagedModelMacros/ModelMacro/ModelMemberAttributes.swift index b3e9faa..cd29b15 100644 --- a/Sources/ManagedModelMacros/ModelMacro/ModelMemberAttributes.swift +++ b/Sources/ManagedModelMacros/ModelMacro/ModelMemberAttributes.swift @@ -51,7 +51,13 @@ extension ModelMacro: MemberAttributeMacro { // @attached(memberAttribute) // property.declaredValueType is set var lastname : String */ - let addAtObjC = property.isKnownRelationshipPropertyType + let isRelationship: Bool + if case .relationship(_) = property.type { + isRelationship = true + } else { + isRelationship = false + } + let addAtObjC = isRelationship || (property.valueType?.canBeRepresentedInObjectiveC ?? false) // We'd like @objc, but we don't know which ones to attach it to? diff --git a/Sources/ManagedModelMacros/Utilities/AttributeTypes.swift b/Sources/ManagedModelMacros/Utilities/AttributeTypes.swift index 4fc1833..aeae102 100644 --- a/Sources/ManagedModelMacros/Utilities/AttributeTypes.swift +++ b/Sources/ManagedModelMacros/Utilities/AttributeTypes.swift @@ -8,7 +8,7 @@ import SwiftSyntax // This is a little fishy as the user might shadow those types, // but I suppose an acceptable tradeoff. -private let attributeTypes : Set = [ +private let swiftTypes: Set = [ // Swift "String", "Int", "Int8", "Int16", "Int32", "Int64", @@ -20,7 +20,8 @@ private let attributeTypes : Set = [ "Swift.UInt", "Swift.UInt8", "Swift.UInt16", "Swift.UInt32", "Swift.UInt64", "Swift.Float", "Swift.Double", "Swift.Bool", - +] +private let foundationTypes: Set = [ // Foundation "Data", "Foundation.Data", "Date", "Foundation.Date", @@ -33,6 +34,7 @@ private let attributeTypes : Set = [ "NSURL", "Foundation.NSURL", "NSData", "Foundation.NSData" ] +private let attributeTypes : Set = swiftTypes.union(foundationTypes) private let toOneRelationshipTypes : Set = [ // CoreData @@ -42,6 +44,8 @@ private let toOneRelationshipTypes : Set = [ ] private let toManyRelationshipTypes : Set = [ // Foundation + "Array", "Foundation.Array", + "NSArray", "Foundation.NSArray", "Set", "Foundation.Set", "NSSet", "Foundation.NSSet", "NSOrderedSet", "Foundation.NSOrderedSet" @@ -52,28 +56,36 @@ extension TypeSyntax { /// Whether the type can be represented in Objective-C. /// A *very* basic implementation. var canBeRepresentedInObjectiveC : Bool { - // TODO: Naive shortcut - if let id = self.as(IdentifierTypeSyntax.self) { - return id.isKnownAttributePropertyType - || id.isKnownRelationshipPropertyType - } - if let opt = self.as(OptionalTypeSyntax.self) { + if let array = opt.wrappedType.as(ArrayTypeSyntax.self) { + return array.element.canBeRepresentedInObjectiveC + } if let id = opt.wrappedType.as(IdentifierTypeSyntax.self) { - return id.isKnownAttributePropertyType - || id.isKnownRelationshipPropertyType + if id.isKnownRelationshipPropertyType { + let element = id.genericArgumentClause?.arguments.first?.argument + return element?.isKnownAttributePropertyType ?? false + } + return id.isKnownFoundationPropertyType } // E.g. this is not representable: `String??`, this is `String?`. + // But Double? or Int? is not representable // I.e. nesting of Optional's are not representable. return false } + if let array = self.as(ArrayTypeSyntax.self) { // This *is* representable: `[String]`, // even this `[ [ 10, 20 ], [ 30, 40 ] ]` return array.element.canBeRepresentedInObjectiveC } - - return false + + if let id = self.as(IdentifierTypeSyntax.self), + id.isKnownFoundationGenericPropertyType { + let arg = id.genericArgumentClause?.arguments.first?.argument + return arg?.isKnownAttributePropertyType ?? false + } + + return self.isKnownAttributePropertyType } /** @@ -137,7 +149,23 @@ extension IdentifierTypeSyntax { return false } } + + var isKnownFoundationPropertyType: Bool { + let name = name.trimmed.text + return foundationTypes.contains(name) + } + var isKnownFoundationGenericPropertyType: Bool { + let name = name.trimmed.text + guard toManyRelationshipTypes.contains(name) else { + return false + } + if let generic = genericArgumentClause { + return generic.arguments.count == 1 + } + return false + } + var isKnownRelationshipPropertyType : Bool { isKnownRelationshipPropertyType(checkOptional: true) } diff --git a/Tests/ManagedModelTests/ObjCMarkedPropertiesTests.swift b/Tests/ManagedModelTests/ObjCMarkedPropertiesTests.swift new file mode 100644 index 0000000..78c8f38 --- /dev/null +++ b/Tests/ManagedModelTests/ObjCMarkedPropertiesTests.swift @@ -0,0 +1,76 @@ +// +// ObjCMarkedPropertiesTests.swift +// ManagedModels +// +// Created by Adam Kopeć on 12/02/2025. +// +import XCTest +import Foundation +import CoreData +@testable import ManagedModels + +final class ObjCMarkedPropertiesTests: XCTestCase { + func getAllObjCPropertyNames() -> [String] { + let classType: AnyClass = Fixtures.AdvancedCodablePropertiesSchema.AdvancedStoredAccess.self + + var count: UInt32 = 0 + var properties = [String]() + class_copyPropertyList(classType, &count)?.withMemoryRebound(to: objc_property_t.self, capacity: Int(count), { pointer in + var ptr = pointer + for _ in 0.. String { + let classType: AnyClass = Fixtures.AdvancedCodablePropertiesSchema.AdvancedStoredAccess.self + + let property = class_getProperty(classType, propertyName) + XCTAssertNotNil(property, "Property \(propertyName) not found") + guard let property else { return "" } + let attributes = property_getAttributes(property) + let attributesString = String(cString: attributes!) + + return attributesString + } + + func testPropertiesMarkedObjC() { + let tokenAttributes = getObjCAttributes(propertyName: "token") + XCTAssertTrue(tokenAttributes.contains("T@\"NSString\""), "Property token is not marked as @objc (\(tokenAttributes))") + + let expiresAttributes = getObjCAttributes(propertyName: "expires") + XCTAssertTrue(expiresAttributes.contains("T@\"NSDate\""), "Property expires is not marked as @objc (\(expiresAttributes))") + + let integerAttributes = getObjCAttributes(propertyName: "integer") + XCTAssertTrue(!integerAttributes.isEmpty, "Property integer is not marked as @objc (\(integerAttributes))") + + let arrayAttributes = getObjCAttributes(propertyName: "array") + XCTAssertTrue(arrayAttributes.contains("T@\"NSArray\""), "Property array is not marked as @objc (\(arrayAttributes))") + + let array2Attributes = getObjCAttributes(propertyName: "array2") + XCTAssertTrue(arrayAttributes.contains("T@\"NSArray\""), "Property array2 is not marked as @objc (\(array2Attributes))") + + let numArrayAttributes = getObjCAttributes(propertyName: "numArray") + XCTAssertTrue(numArrayAttributes.contains("T@\"NSArray\""), "Property numArray is not marked as @objc (\(numArrayAttributes))") + + let optionalArrayAttributes = getObjCAttributes(propertyName: "optionalArray") + XCTAssertTrue(optionalArrayAttributes.contains("T@\"NSArray\""), "Property optionalArray is not marked as @objc (\(optionalArrayAttributes))") + + let optionalArray2Attributes = getObjCAttributes(propertyName: "optionalArray2") + XCTAssertTrue(optionalArray2Attributes.contains("T@\"NSArray\""), "Property optionalArray2 is not marked as @objc (\(optionalArray2Attributes))") + + let optionalNumArrayAttributes = getObjCAttributes(propertyName: "optionalNumArray") + XCTAssertTrue(optionalNumArrayAttributes.contains("T@\"NSArray\""), "Property optionalNumArray is not marked as @objc (\(optionalNumArrayAttributes))") + + let optionalNumArray2Attributes = getObjCAttributes(propertyName: "optionalNumArray2") + XCTAssertTrue(optionalNumArray2Attributes.contains("T@\"NSArray\""), "Property optionalNumArray2 is not marked as @objc (\(optionalNumArray2Attributes))") + + let objcSetAttributes = getObjCAttributes(propertyName: "objcSet") + XCTAssertTrue(objcSetAttributes.contains("T@\"NSSet\""), "Property objcSet is not marked as @objc (\(objcSetAttributes))") + } +} diff --git a/Tests/ManagedModelTests/Schemas/AdvancedCodablePropertiesSchema.swift b/Tests/ManagedModelTests/Schemas/AdvancedCodablePropertiesSchema.swift new file mode 100644 index 0000000..c4873f8 --- /dev/null +++ b/Tests/ManagedModelTests/Schemas/AdvancedCodablePropertiesSchema.swift @@ -0,0 +1,50 @@ +// +// AdvancedCodablePropertiesSchema.swift +// ManagedModels +// +// Created by Adam Kopeć on 04/02/2025. +// + +import ManagedModels + +extension Fixtures { + // https://github.com/Data-swift/ManagedModels/issues/36 + + enum AdvancedCodablePropertiesSchema: VersionedSchema { + static var models : [ any PersistentModel.Type ] = [ + AdvancedStoredAccess.self + ] + + public static let versionIdentifier = Schema.Version(0, 1, 0) + + @Model + final class AdvancedStoredAccess: NSManagedObject { + var token : String + var expires : Date + var integer : Int + var distance: Int? + var avgSpeed: Double? + var sip : AccessSIP + var numArray: [Int] + var array : [String] + var array2 : Array + var optionalNumArray : [Int]? + var optionalNumArray2: Array? + var optionalArray : [String]? + var optionalArray2 : Array? + var optionalSip : AccessSIP? + var codableSet : Set + var objcSet : Set + var objcNumSet : Set + var codableArray : [AccessSIP] + var optCodableSet : Set? + var optCodableArray : [AccessSIP]? + } + + struct AccessSIP: Codable, Hashable { + var username : String + var password : String + var realm : String + } + } +}