From efbe189a04df30340a7327aa51769ae09d80cdfe Mon Sep 17 00:00:00 2001 From: Rock-Connotation <787661104@qq.com> Date: Fri, 6 Mar 2026 17:24:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=A7=E6=89=BF=E5=AE=8F=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20public=20=E7=BA=A7=E5=88=AB=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E6=94=AF=E6=8C=81=20open?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SmartSubclassMacro.swift | 50 +++- ...SmartSubclassMacroAccessControlTests.swift | 220 ++++++++++++++++++ 2 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 Tests/SmartSubclassMacroAccessControlTests.swift diff --git a/Sources/SmartCodableMacros/SmartSubclassMacro.swift b/Sources/SmartCodableMacros/SmartSubclassMacro.swift index 60e9192..ffb0dcb 100644 --- a/Sources/SmartCodableMacros/SmartSubclassMacro.swift +++ b/Sources/SmartCodableMacros/SmartSubclassMacro.swift @@ -13,6 +13,20 @@ import SwiftSyntaxMacros /// A macro that automatically implements SmartCodable inheritance support public struct SmartSubclassMacro: MemberMacro { + private enum SynthesizedMemberAccess { + case inheritedDefault + case publicVisible + + var prefix: String { + switch self { + case .inheritedDefault: + return "" + case .publicVisible: + return "public " + } + } + } + public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, @@ -47,6 +61,7 @@ public struct SmartSubclassMacro: MemberMacro { // 获取类的属性 let properties = try extractProperties(from: classDecl) + let memberAccess = synthesizedMemberAccess(for: classDecl) var members: [DeclSyntax] = [] @@ -54,17 +69,17 @@ public struct SmartSubclassMacro: MemberMacro { members.append(generateCodingKeysEnum(for: properties)) // 生成init(from:)方法 - members.append(generateInitFromDecoder(for: properties)) + members.append(generateInitFromDecoder(for: properties, access: memberAccess)) // 生成encode(to:)方法 - members.append(generateEncodeToEncoder(for: properties)) + members.append(generateEncodeToEncoder(for: properties, access: memberAccess)) if hasRequiredInitializer(classDecl) { return members } else { // 生成required init()方法 - members.append(generateRequiredInit()) + members.append(generateRequiredInit(access: memberAccess)) return members } } @@ -148,7 +163,10 @@ public struct SmartSubclassMacro: MemberMacro { } // 辅助方法:生成init(from:)方法 - private static func generateInitFromDecoder(for properties: [ModelMemberProperty]) -> DeclSyntax { + private static func generateInitFromDecoder( + for properties: [ModelMemberProperty], + access: SynthesizedMemberAccess + ) -> DeclSyntax { let decodingStatements = properties.map { property in let propertyName = property.accessName let propertyType = property.type @@ -163,7 +181,7 @@ public struct SmartSubclassMacro: MemberMacro { }.joined(separator: "\n") return """ - required init(from decoder: Decoder) throws { + \(raw: access.prefix)required init(from decoder: Decoder) throws { try super.init(from: decoder) let container = try decoder.container(keyedBy: CodingKeys.self) @@ -173,7 +191,10 @@ public struct SmartSubclassMacro: MemberMacro { } // 辅助方法:生成encode(to:)方法 - private static func generateEncodeToEncoder(for properties: [ModelMemberProperty]) -> DeclSyntax { + private static func generateEncodeToEncoder( + for properties: [ModelMemberProperty], + access: SynthesizedMemberAccess + ) -> DeclSyntax { let encodingStatements = properties.map { property in if property.type.hasSuffix("?") { return "try container.encodeIfPresent(\(property.accessName), forKey: .\(property.codingKeyName))" @@ -183,7 +204,7 @@ public struct SmartSubclassMacro: MemberMacro { }.joined(separator: "\n") return """ - override func encode(to encoder: Encoder) throws { + \(raw: access.prefix)override func encode(to encoder: Encoder) throws { try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) @@ -206,11 +227,22 @@ public struct SmartSubclassMacro: MemberMacro { } // 辅助方法:生成required init()方法 - private static func generateRequiredInit() -> DeclSyntax { + private static func generateRequiredInit(access: SynthesizedMemberAccess) -> DeclSyntax { return """ - required init() { + \(raw: access.prefix)required init() { super.init() } """ } + + private static func synthesizedMemberAccess(for classDecl: ClassDeclSyntax) -> SynthesizedMemberAccess { + if classDecl.modifiers.contains(where: { modifier in + let name = modifier.name.text + return name == "public" || name == "open" + }) { + return .publicVisible + } + + return .inheritedDefault + } } diff --git a/Tests/SmartSubclassMacroAccessControlTests.swift b/Tests/SmartSubclassMacroAccessControlTests.swift new file mode 100644 index 0000000..d41c52e --- /dev/null +++ b/Tests/SmartSubclassMacroAccessControlTests.swift @@ -0,0 +1,220 @@ +import XCTest +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +@testable import SmartCodableMacros + +/// Tests for `@SmartSubclass` macro access control inference. +/// +/// These tests verify that the macro correctly derives access modifiers for +/// synthesized members (`init(from:)`, `encode(to:)`, `init()`) based on the +/// visibility of the host class. +final class SmartSubclassMacroAccessControlTests: XCTestCase { + private let macros: [String: Macro.Type] = [ + "SmartSubclass": SmartSubclassMacro.self + ] + + // MARK: - Helpers + + /// Base model definition shared across all test cases. + private let baseModelDefinition = """ + class BaseModel { + var name: String = "" + + required init() {} + required init(from decoder: Decoder) throws {} + func encode(to encoder: Encoder) throws {} + } + """ + + /// Asserts macro expansion with consistent base model context. + /// + /// - Parameters: + /// - classDeclaration: The subclass declaration with `@SmartSubclass` attribute. + /// - expectedClassOutput: The expected expanded source code. + /// - file: Source file for failure reporting. + /// - line: Line number for failure reporting. + private func assertAccessControlMacroExpansion( + classDeclaration: String, + expectedClassOutput: String, + file: StaticString = #file, + line: UInt = #line + ) { + let input = """ + \(baseModelDefinition) + + @SmartSubclass + \(classDeclaration) + """ + + let expectedOutput = """ + \(baseModelDefinition) + \(expectedClassOutput) + """ + + assertMacroExpansion( + input, + expandedSource: expectedOutput, + macros: macros, + file: file, + line: line + ) + } + + // MARK: - Tests + + /// Verifies that `public` class generates members with `public` modifiers. + func testPublicClassExpansionAddsPublicAccessModifiers() { + assertAccessControlMacroExpansion( + classDeclaration: """ + public class PublicStudent: BaseModel { + var age: Int = 0 + } + """, + expectedClassOutput: """ + public class PublicStudent: BaseModel { + var age: Int = 0 + + enum CodingKeys: CodingKey { + case age + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.age = try container.decodeIfPresent(Int.self, forKey: .age) ?? self.age + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(age, forKey: .age) + } + + public required init() { + super.init() + } + } + """ + ) + } + + /// Verifies that `open` class generates members with `public` modifiers (Phase 1). + func testOpenClassExpansionAddsPublicAccessModifiers() { + assertAccessControlMacroExpansion( + classDeclaration: """ + open class OpenStudent: BaseModel { + var age: Int = 0 + } + """, + expectedClassOutput: """ + open class OpenStudent: BaseModel { + var age: Int = 0 + + enum CodingKeys: CodingKey { + case age + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.age = try container.decodeIfPresent(Int.self, forKey: .age) ?? self.age + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(age, forKey: .age) + } + + public required init() { + super.init() + } + } + """ + ) + } + + /// Verifies that internal/default class does not add explicit `public` modifiers. + func testInternalClassDoesNotAddPublicAccessModifiers() { + assertAccessControlMacroExpansion( + classDeclaration: """ + class InternalStudent: BaseModel { + var age: Int = 0 + } + """, + expectedClassOutput: """ + class InternalStudent: BaseModel { + var age: Int = 0 + + enum CodingKeys: CodingKey { + case age + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.age = try container.decodeIfPresent(Int.self, forKey: .age) ?? self.age + } + + override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(age, forKey: .age) + } + + required init() { + super.init() + } + } + """ + ) + } + + /// Verifies that existing `required init()` prevents duplicate generation. + func testClassWithExistingRequiredInitSkipsGeneratedInit() { + assertAccessControlMacroExpansion( + classDeclaration: """ + public class StudentWithInit: BaseModel { + var age: Int = 0 + + required init() { + super.init() + } + } + """, + expectedClassOutput: """ + public class StudentWithInit: BaseModel { + var age: Int = 0 + + required init() { + super.init() + } + + enum CodingKeys: CodingKey { + case age + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.age = try container.decodeIfPresent(Int.self, forKey: .age) ?? self.age + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(age, forKey: .age) + } + } + """ + ) + } +}