Skip to content

Commit 73c5d1e

Browse files
authored
fix: fixed duplicate CodingKeys generated (#63)
1 parent 14830ab commit 73c5d1e

File tree

14 files changed

+232
-58
lines changed

14 files changed

+232
-58
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"swift.disableAutoResolve": true,
1414
"swift.swiftEnvironmentVariables": {
15+
"METACODABLE_CI": "true",
1516
"SWIFT_SYNTAX_EXTENSION_MACRO_FIXED": "true"
1617
},
1718
}

MetaCodableMacro.podspec

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ Pod::Spec.new do |s|
2929
:execution_position => :before_compile
3030
}
3131

32-
s.user_target_xcconfig = {
32+
xcconfig = {
3333
'OTHER_SWIFT_FLAGS' => "-Xfrontend -load-plugin-executable -Xfrontend #{plugin_path}",
3434
'METACODABLE_PLUGIN_BUILD_ENVIRONMENT' => 'METACODABLE_BEING_USED_FROM_COCOAPODS=true'
3535
}
36+
s.user_target_xcconfig = xcconfig
37+
s.pod_target_xcconfig = xcconfig
3638
end

Plugins/MetaProtocolCodable/Config.swift

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,37 @@ struct Config {
2323
/// Files from the target which includes plugin and target dependencies
2424
/// present in current package manifest are checked.
2525
case direct
26-
/// Represents to check all local targets.
26+
/// Represents to check all local target dependencies.
2727
///
2828
/// Files from the target which includes plugin and all targets
29-
/// that are in the same project/package.
29+
/// that are in the same project/package that are dependencies
30+
/// of this target.
3031
case local
3132
/// Represents to check current target and all dependencies.
3233
///
3334
/// Files from the target which includes plugin and all its
3435
/// dependencies are checked.
3536
case recursive
37+
38+
/// Creates a new instance by decoding from the given decoder.
39+
///
40+
/// This initializer throws an error if reading from the decoder fails,
41+
/// or if the data read is corrupted or otherwise invalid.
42+
///
43+
/// - Parameter decoder: The decoder to read data from.
44+
init(from decoder: Decoder) throws {
45+
let rawValue = try String(from: decoder).lowercased()
46+
guard let value = Self(rawValue: rawValue) else {
47+
throw DecodingError.typeMismatch(
48+
Self.self,
49+
.init(
50+
codingPath: decoder.codingPath,
51+
debugDescription: "Data doesn't match any case"
52+
)
53+
)
54+
}
55+
self = value
56+
}
3657
}
3758
}
3859

@@ -47,7 +68,7 @@ extension Config: Codable {
4768
let container = try decoder.container(keyedBy: CodingKeys.self)
4869
self.scan =
4970
try container.decodeIfPresent(
50-
ScanMode.self, forKey: .scan
71+
ScanMode.self, forCaseInsensitiveKey: .scan
5172
) ?? .target
5273
}
5374

@@ -63,3 +84,45 @@ extension Config: Codable {
6384
#endif
6485
}
6586
}
87+
88+
extension KeyedDecodingContainerProtocol {
89+
/// Decodes a value of the given type for the given key case-insensitively,
90+
/// if present.
91+
///
92+
/// This method returns `nil` if the container does not have a value
93+
/// associated with key case-insensitively, or if the value is null.
94+
///
95+
/// - Parameters:
96+
/// - type: The type of value to decode.
97+
/// - key: The key that the decoded value is associated with.
98+
///
99+
/// - Returns: A decoded value of the requested type, or `nil`
100+
/// if the `Decoder` does not have an entry associated with the given
101+
/// key, or if the value is a null value.
102+
///
103+
/// - Throws: `DecodingError.typeMismatch` if the encountered
104+
/// encoded value is not convertible to the requested type or the key
105+
/// value matches multiple value case-insensitively.
106+
func decodeIfPresent<T: Decodable>(
107+
_ type: T.Type,
108+
forCaseInsensitiveKey key: Key
109+
) throws -> T? {
110+
let keys = self.allKeys.filter { eachKey in
111+
eachKey.stringValue.lowercased() == key.stringValue.lowercased()
112+
}
113+
114+
guard keys.count <= 1 else {
115+
throw DecodingError.typeMismatch(
116+
type,
117+
.init(
118+
codingPath: codingPath,
119+
debugDescription: """
120+
Duplicate keys found, keys are case-insensitive.
121+
"""
122+
)
123+
)
124+
}
125+
126+
return try decodeIfPresent(type, forKey: keys.first ?? key)
127+
}
128+
}

Plugins/MetaProtocolCodable/SourceTarget/MetaProtocolCodableSourceTarget.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ extension Config {
7070
targets.append(target)
7171
allTargets = targets
7272
case .local:
73-
allTargets = context.localTargets
73+
allTargets = context.localTargets.filter { localTarget in
74+
return target.recursiveTargets.contains { target in
75+
return target.moduleName == localTarget.moduleName
76+
}
77+
}
7478
modules = allTargets.lazy.map(\.moduleName).filter { module in
7579
return module != target.moduleName
7680
}

Sources/HelperCoders/SequenceCoder/SequenceCoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import MetaCodable
1111
public struct SequenceCoder<Sequence, ElementHelper>: HelperCoder
1212
where
1313
Sequence: SequenceInitializable, ElementHelper: HelperCoder,
14-
Sequence.Element == ElementHelper.Coded, Sequence: Equatable
14+
Sequence.Element == ElementHelper.Coded
1515
{
1616
/// The ``/MetaCodable/HelperCoder`` for element.
1717
///

Sources/HelperCoders/SequenceCoder/SequenceCoderConfiguration.swift

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ extension SequenceCoder {
3333
/// - invalidDefault: The sequence to use in case of invalid data.
3434
/// - emptyDefault: The sequence to use in case of empty data.
3535
public init(
36-
lossy: Bool,
36+
lossy: Bool = false,
3737
invalidDefault: Sequence? = nil, emptyDefault: Sequence? = nil
3838
) {
3939
self.lossy = lossy
@@ -118,6 +118,27 @@ extension SequenceCoder.Configuration: OptionSet {
118118
)
119119
}
120120

121+
/// Returns a Boolean value indicating whether two values are equal.
122+
///
123+
/// Equality is the inverse of inequality. For any values `a` and `b`,
124+
/// `a == b` implies that `a != b` is `false`.
125+
///
126+
/// - Parameters:
127+
/// - lhs: A value to compare.
128+
/// - rhs: Another value to compare.
129+
///
130+
/// - Returns: Whether two values are equal.
131+
private static func areEqual(_ lhs: Sequence?, _ rhs: Sequence?) -> Bool {
132+
func `is`<T: Equatable>(value lhs: T, equalTo rhs: Any?) -> Bool {
133+
return lhs == (rhs as? T)
134+
}
135+
136+
guard
137+
let lhs = lhs as? any Equatable
138+
else { return false }
139+
return `is`(value: lhs, equalTo: rhs)
140+
}
141+
121142
/// Updates current configuration in sync with provided configuration.
122143
///
123144
/// The updated configuration has:
@@ -127,13 +148,13 @@ extension SequenceCoder.Configuration: OptionSet {
127148
/// - Parameter other: The new configuration.
128149
public mutating func formIntersection(_ other: Self) {
129150
let invalidDefault =
130-
if other.invalidDefault == self.invalidDefault {
151+
if Self.areEqual(other.invalidDefault, self.invalidDefault) {
131152
self.invalidDefault
132153
} else {
133154
nil as Sequence?
134155
}
135156
let emptyDefault =
136-
if other.emptyDefault == self.emptyDefault {
157+
if Self.areEqual(other.emptyDefault, self.emptyDefault) {
137158
self.emptyDefault
138159
} else {
139160
nil as Sequence?
@@ -171,7 +192,7 @@ extension SequenceCoder.Configuration: OptionSet {
171192
/// - Returns: True only if all the configuration data match.
172193
public static func == (lhs: Self, rhs: Self) -> Bool {
173194
return lhs.lossy == rhs.lossy
174-
&& lhs.invalidDefault == rhs.invalidDefault
175-
&& lhs.emptyDefault == rhs.emptyDefault
195+
&& areEqual(lhs.invalidDefault, rhs.invalidDefault)
196+
&& areEqual(lhs.emptyDefault, rhs.emptyDefault)
176197
}
177198
}

Sources/MetaCodable/Codable/IgnoreCodingInitialized.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,4 @@
6464
@attached(peer)
6565
@available(swift 5.9)
6666
public macro IgnoreCodingInitialized() =
67-
#externalMacro(
68-
module: "MacroPlugin", type: "IgnoreCodingInitialized"
69-
)
67+
#externalMacro(module: "MacroPlugin", type: "IgnoreCodingInitialized")

Sources/PluginCore/Attributes/CodedAs.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@_implementationOnly import OrderedCollections
12
@_implementationOnly import SwiftSyntax
23
@_implementationOnly import SwiftSyntaxMacros
34

@@ -149,7 +150,7 @@ where Key == [String], Decl: AttributableDeclSyntax, Var: PropertyVariable {
149150
case let path = attr.providedPath,
150151
!path.isEmpty
151152
else { return self.updating(with: self.variable.any) }
152-
let keys = codingKeys.add(keys: path, context: context)
153+
let keys = OrderedSet(codingKeys.add(keys: path, context: context))
153154
let oldVar = self.variable
154155
let newVar = AliasedPropertyVariable(base: oldVar, additionalKeys: keys)
155156
return self.updating(with: newVar.any)

Sources/PluginCore/Variables/Property/AliasedPropertyVariable.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@_implementationOnly import SwiftSyntax
22
@_implementationOnly import SwiftSyntaxBuilder
33
@_implementationOnly import SwiftSyntaxMacros
4+
@_implementationOnly import OrderedCollections
45

56
/// A variable value containing additional `CodingKey`s for decoding.
67
///
@@ -18,7 +19,7 @@ where Wrapped: PropertyVariable {
1819
///
1920
/// Represents all the additional `CodingKey`s that
2021
/// this variable could be encoded at.
21-
let additionalKeys: [CodingKeysMap.Key]
22+
let additionalKeys: OrderedSet<CodingKeysMap.Key>
2223

2324
/// Provides the code syntax for decoding this variable
2425
/// at the provided location.
@@ -41,6 +42,9 @@ where Wrapped: PropertyVariable {
4142
case let .container(container, key, method) = location
4243
else { return base.decoding(in: context, from: location) }
4344
var allKeys = [key]
45+
let additionalKeys = additionalKeys.filter { aKey in
46+
return aKey.expr.trimmedDescription != key.trimmedDescription
47+
}
4448
allKeys.append(contentsOf: additionalKeys.map(\.expr))
4549
let keysName: ExprSyntax = "\(CodingKeysMap.Key.name(for: name))Keys"
4650
let keyList = ArrayExprSyntax(expressions: allKeys)

Sources/PluginCore/Variables/Type/Data/CodingKeysMap/CodingKeysMap.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ package final class CodingKeysMap {
103103
context: some MacroExpansionContext
104104
) -> [Key] {
105105
guard !keys.isEmpty else { return [] }
106-
let currentCases = data.values.map(\.name)
106+
var currentCases: [String] { data.values.map(\.name) }
107107

108108
if let field {
109109
let fieldIncluded = currentCases.contains(Key.name(for: field).text)

0 commit comments

Comments
 (0)