Skip to content

Commit 4c63183

Browse files
authored
Merge pull request #133 from Rock-Connotation/feat/issue-132
feat: 继承宏支持 public 级别访问,不支持 open
2 parents 8464f63 + efbe189 commit 4c63183

2 files changed

Lines changed: 261 additions & 9 deletions

File tree

Sources/SmartCodableMacros/SmartSubclassMacro.swift

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ import SwiftSyntaxMacros
1313

1414
/// A macro that automatically implements SmartCodable inheritance support
1515
public struct SmartSubclassMacro: MemberMacro {
16+
private enum SynthesizedMemberAccess {
17+
case inheritedDefault
18+
case publicVisible
19+
20+
var prefix: String {
21+
switch self {
22+
case .inheritedDefault:
23+
return ""
24+
case .publicVisible:
25+
return "public "
26+
}
27+
}
28+
}
29+
1630
public static func expansion(
1731
of node: AttributeSyntax,
1832
providingMembersOf declaration: some DeclGroupSyntax,
@@ -47,24 +61,25 @@ public struct SmartSubclassMacro: MemberMacro {
4761

4862
// 获取类的属性
4963
let properties = try extractProperties(from: classDecl)
64+
let memberAccess = synthesizedMemberAccess(for: classDecl)
5065

5166
var members: [DeclSyntax] = []
5267

5368
// 生成CodingKeys枚举
5469
members.append(generateCodingKeysEnum(for: properties))
5570

5671
// 生成init(from:)方法
57-
members.append(generateInitFromDecoder(for: properties))
72+
members.append(generateInitFromDecoder(for: properties, access: memberAccess))
5873

5974
// 生成encode(to:)方法
60-
members.append(generateEncodeToEncoder(for: properties))
75+
members.append(generateEncodeToEncoder(for: properties, access: memberAccess))
6176

6277

6378
if hasRequiredInitializer(classDecl) {
6479
return members
6580
} else {
6681
// 生成required init()方法
67-
members.append(generateRequiredInit())
82+
members.append(generateRequiredInit(access: memberAccess))
6883
return members
6984
}
7085
}
@@ -148,7 +163,10 @@ public struct SmartSubclassMacro: MemberMacro {
148163
}
149164

150165
// 辅助方法:生成init(from:)方法
151-
private static func generateInitFromDecoder(for properties: [ModelMemberProperty]) -> DeclSyntax {
166+
private static func generateInitFromDecoder(
167+
for properties: [ModelMemberProperty],
168+
access: SynthesizedMemberAccess
169+
) -> DeclSyntax {
152170
let decodingStatements = properties.map { property in
153171
let propertyName = property.accessName
154172
let propertyType = property.type
@@ -163,7 +181,7 @@ public struct SmartSubclassMacro: MemberMacro {
163181
}.joined(separator: "\n")
164182

165183
return """
166-
required init(from decoder: Decoder) throws {
184+
\(raw: access.prefix)required init(from decoder: Decoder) throws {
167185
try super.init(from: decoder)
168186
169187
let container = try decoder.container(keyedBy: CodingKeys.self)
@@ -173,7 +191,10 @@ public struct SmartSubclassMacro: MemberMacro {
173191
}
174192

175193
// 辅助方法:生成encode(to:)方法
176-
private static func generateEncodeToEncoder(for properties: [ModelMemberProperty]) -> DeclSyntax {
194+
private static func generateEncodeToEncoder(
195+
for properties: [ModelMemberProperty],
196+
access: SynthesizedMemberAccess
197+
) -> DeclSyntax {
177198
let encodingStatements = properties.map { property in
178199
if property.type.hasSuffix("?") {
179200
return "try container.encodeIfPresent(\(property.accessName), forKey: .\(property.codingKeyName))"
@@ -183,7 +204,7 @@ public struct SmartSubclassMacro: MemberMacro {
183204
}.joined(separator: "\n")
184205

185206
return """
186-
override func encode(to encoder: Encoder) throws {
207+
\(raw: access.prefix)override func encode(to encoder: Encoder) throws {
187208
try super.encode(to: encoder)
188209
189210
var container = encoder.container(keyedBy: CodingKeys.self)
@@ -206,11 +227,22 @@ public struct SmartSubclassMacro: MemberMacro {
206227
}
207228

208229
// 辅助方法:生成required init()方法
209-
private static func generateRequiredInit() -> DeclSyntax {
230+
private static func generateRequiredInit(access: SynthesizedMemberAccess) -> DeclSyntax {
210231
return """
211-
required init() {
232+
\(raw: access.prefix)required init() {
212233
super.init()
213234
}
214235
"""
215236
}
237+
238+
private static func synthesizedMemberAccess(for classDecl: ClassDeclSyntax) -> SynthesizedMemberAccess {
239+
if classDecl.modifiers.contains(where: { modifier in
240+
let name = modifier.name.text
241+
return name == "public" || name == "open"
242+
}) {
243+
return .publicVisible
244+
}
245+
246+
return .inheritedDefault
247+
}
216248
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import XCTest
2+
import SwiftSyntaxMacros
3+
import SwiftSyntaxMacrosTestSupport
4+
@testable import SmartCodableMacros
5+
6+
/// Tests for `@SmartSubclass` macro access control inference.
7+
///
8+
/// These tests verify that the macro correctly derives access modifiers for
9+
/// synthesized members (`init(from:)`, `encode(to:)`, `init()`) based on the
10+
/// visibility of the host class.
11+
final class SmartSubclassMacroAccessControlTests: XCTestCase {
12+
private let macros: [String: Macro.Type] = [
13+
"SmartSubclass": SmartSubclassMacro.self
14+
]
15+
16+
// MARK: - Helpers
17+
18+
/// Base model definition shared across all test cases.
19+
private let baseModelDefinition = """
20+
class BaseModel {
21+
var name: String = ""
22+
23+
required init() {}
24+
required init(from decoder: Decoder) throws {}
25+
func encode(to encoder: Encoder) throws {}
26+
}
27+
"""
28+
29+
/// Asserts macro expansion with consistent base model context.
30+
///
31+
/// - Parameters:
32+
/// - classDeclaration: The subclass declaration with `@SmartSubclass` attribute.
33+
/// - expectedClassOutput: The expected expanded source code.
34+
/// - file: Source file for failure reporting.
35+
/// - line: Line number for failure reporting.
36+
private func assertAccessControlMacroExpansion(
37+
classDeclaration: String,
38+
expectedClassOutput: String,
39+
file: StaticString = #file,
40+
line: UInt = #line
41+
) {
42+
let input = """
43+
\(baseModelDefinition)
44+
45+
@SmartSubclass
46+
\(classDeclaration)
47+
"""
48+
49+
let expectedOutput = """
50+
\(baseModelDefinition)
51+
\(expectedClassOutput)
52+
"""
53+
54+
assertMacroExpansion(
55+
input,
56+
expandedSource: expectedOutput,
57+
macros: macros,
58+
file: file,
59+
line: line
60+
)
61+
}
62+
63+
// MARK: - Tests
64+
65+
/// Verifies that `public` class generates members with `public` modifiers.
66+
func testPublicClassExpansionAddsPublicAccessModifiers() {
67+
assertAccessControlMacroExpansion(
68+
classDeclaration: """
69+
public class PublicStudent: BaseModel {
70+
var age: Int = 0
71+
}
72+
""",
73+
expectedClassOutput: """
74+
public class PublicStudent: BaseModel {
75+
var age: Int = 0
76+
77+
enum CodingKeys: CodingKey {
78+
case age
79+
}
80+
81+
public required init(from decoder: Decoder) throws {
82+
try super.init(from: decoder)
83+
84+
let container = try decoder.container(keyedBy: CodingKeys.self)
85+
self.age = try container.decodeIfPresent(Int.self, forKey: .age) ?? self.age
86+
}
87+
88+
public override func encode(to encoder: Encoder) throws {
89+
try super.encode(to: encoder)
90+
91+
var container = encoder.container(keyedBy: CodingKeys.self)
92+
try container.encode(age, forKey: .age)
93+
}
94+
95+
public required init() {
96+
super.init()
97+
}
98+
}
99+
"""
100+
)
101+
}
102+
103+
/// Verifies that `open` class generates members with `public` modifiers (Phase 1).
104+
func testOpenClassExpansionAddsPublicAccessModifiers() {
105+
assertAccessControlMacroExpansion(
106+
classDeclaration: """
107+
open class OpenStudent: BaseModel {
108+
var age: Int = 0
109+
}
110+
""",
111+
expectedClassOutput: """
112+
open class OpenStudent: BaseModel {
113+
var age: Int = 0
114+
115+
enum CodingKeys: CodingKey {
116+
case age
117+
}
118+
119+
public required init(from decoder: Decoder) throws {
120+
try super.init(from: decoder)
121+
122+
let container = try decoder.container(keyedBy: CodingKeys.self)
123+
self.age = try container.decodeIfPresent(Int.self, forKey: .age) ?? self.age
124+
}
125+
126+
public override func encode(to encoder: Encoder) throws {
127+
try super.encode(to: encoder)
128+
129+
var container = encoder.container(keyedBy: CodingKeys.self)
130+
try container.encode(age, forKey: .age)
131+
}
132+
133+
public required init() {
134+
super.init()
135+
}
136+
}
137+
"""
138+
)
139+
}
140+
141+
/// Verifies that internal/default class does not add explicit `public` modifiers.
142+
func testInternalClassDoesNotAddPublicAccessModifiers() {
143+
assertAccessControlMacroExpansion(
144+
classDeclaration: """
145+
class InternalStudent: BaseModel {
146+
var age: Int = 0
147+
}
148+
""",
149+
expectedClassOutput: """
150+
class InternalStudent: BaseModel {
151+
var age: Int = 0
152+
153+
enum CodingKeys: CodingKey {
154+
case age
155+
}
156+
157+
required init(from decoder: Decoder) throws {
158+
try super.init(from: decoder)
159+
160+
let container = try decoder.container(keyedBy: CodingKeys.self)
161+
self.age = try container.decodeIfPresent(Int.self, forKey: .age) ?? self.age
162+
}
163+
164+
override func encode(to encoder: Encoder) throws {
165+
try super.encode(to: encoder)
166+
167+
var container = encoder.container(keyedBy: CodingKeys.self)
168+
try container.encode(age, forKey: .age)
169+
}
170+
171+
required init() {
172+
super.init()
173+
}
174+
}
175+
"""
176+
)
177+
}
178+
179+
/// Verifies that existing `required init()` prevents duplicate generation.
180+
func testClassWithExistingRequiredInitSkipsGeneratedInit() {
181+
assertAccessControlMacroExpansion(
182+
classDeclaration: """
183+
public class StudentWithInit: BaseModel {
184+
var age: Int = 0
185+
186+
required init() {
187+
super.init()
188+
}
189+
}
190+
""",
191+
expectedClassOutput: """
192+
public class StudentWithInit: BaseModel {
193+
var age: Int = 0
194+
195+
required init() {
196+
super.init()
197+
}
198+
199+
enum CodingKeys: CodingKey {
200+
case age
201+
}
202+
203+
public required init(from decoder: Decoder) throws {
204+
try super.init(from: decoder)
205+
206+
let container = try decoder.container(keyedBy: CodingKeys.self)
207+
self.age = try container.decodeIfPresent(Int.self, forKey: .age) ?? self.age
208+
}
209+
210+
public override func encode(to encoder: Encoder) throws {
211+
try super.encode(to: encoder)
212+
213+
var container = encoder.container(keyedBy: CodingKeys.self)
214+
try container.encode(age, forKey: .age)
215+
}
216+
}
217+
"""
218+
)
219+
}
220+
}

0 commit comments

Comments
 (0)