diff --git a/Sources/CodableToTypeScript/Basic/NameProvider.swift b/Sources/CodableToTypeScript/Basic/NameProvider.swift new file mode 100644 index 0000000..25c9eea --- /dev/null +++ b/Sources/CodableToTypeScript/Basic/NameProvider.swift @@ -0,0 +1,40 @@ +import TypeScriptAST + +struct NameProvider { + init() {} + + private var used: Set = [] + + mutating func register(signature: TSFunctionDecl) { + for param in signature.params { + register(name: param.name) + } + } + + mutating func register(name: String) { + used.insert(name) + } + + mutating func provide(base: String) -> String { + if let name = provideIfUnused(name: base) { + return name + } + + var i = 2 + while true { + let cand = "\(base)\(i)" + if let name = provideIfUnused(name: cand) { + return name + } + i += 1 + } + } + + mutating func provideIfUnused(name: String) -> String? { + if used.contains(name) { + return nil + } + used.insert(name) + return name + } +} diff --git a/Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift b/Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift index 13ff9f6..84aa683 100644 --- a/Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift @@ -269,6 +269,8 @@ private struct DecodeObjFuncGen { } private func decodeAssociatedValues( + nameProvider: inout NameProvider, + names: inout [String: String], caseElement: EnumCaseElementDecl, json: any TSExpr ) throws -> [TSVarDecl] { @@ -282,8 +284,11 @@ private struct DecodeObjFuncGen { expr = try generator.converter(for: value.interfaceType) .callDecodeField(json: expr) + let varName = nameProvider.provide(base: TSKeyword.escaped(label)) + names[label] = varName + return TSVarDecl( - kind: .const, name: TSKeyword.escaped(label), + kind: .const, name: varName, initializer: expr ) } @@ -291,17 +296,25 @@ private struct DecodeObjFuncGen { } } - private func buildCaseObject(caseElement: EnumCaseElementDecl) -> TSObjectExpr { - return TSObjectExpr(caseElement.associatedValues.map { (value) in + private func buildCaseObject( + names: [String: String], + caseElement: EnumCaseElementDecl + ) throws -> TSObjectExpr { + return TSObjectExpr(try caseElement.associatedValues.map { (value) in let label = value.codableLabel + let varName = try names[label].unwrap(name: "var name") return TSObjectExpr.Field.named( name: label, - value: TSIdentExpr(TSKeyword.escaped(label)) + value: TSIdentExpr(varName) ) }) } - private func thenCode(caseElement: EnumCaseElementDecl) throws -> TSBlockStmt { + private func thenCode( + nameProvider: NameProvider, + caseElement: EnumCaseElementDecl + ) throws -> TSBlockStmt { + var nameProvider = nameProvider var block: [any ASTNode] = [] if !caseElement.associatedValues.isEmpty { @@ -313,9 +326,13 @@ private struct DecodeObjFuncGen { ) ) block.append(j) + nameProvider.register(name: "j") } + var names: [String: String] = [:] block += try decodeAssociatedValues( + nameProvider: &nameProvider, + names: &names, caseElement: caseElement, json: TSIdentExpr("j") ) @@ -327,7 +344,10 @@ private struct DecodeObjFuncGen { ), .named( name: caseElement.name, - value: buildCaseObject(caseElement: caseElement) + value: try buildCaseObject( + names: names, + caseElement: caseElement + ) ) ]) @@ -352,6 +372,8 @@ private struct DecodeObjFuncGen { func generate() throws -> TSFunctionDecl? { guard let decl = try converter.decodeSignature() else { return nil } + var nameProvider = NameProvider() + nameProvider.register(signature: decl) var topStmt: (any TSStmt)? func appendElse(stmt: any TSStmt) { @@ -377,7 +399,7 @@ private struct DecodeObjFuncGen { collect(at: ce.name) { let ifSt = TSIfStmt( condition: condCode(caseElement: ce), - then: try thenCode(caseElement: ce), + then: try thenCode(nameProvider: nameProvider, caseElement: ce), else: nil ) @@ -401,7 +423,11 @@ private struct EncodeObjFuncGen { var converter: EnumConverter var type: EnumDecl - func encodeAssociatedValues(element: EnumCaseElementDecl) throws -> [TSVarDecl] { + func encodeAssociatedValues( + nameProvider: inout NameProvider, + names: inout [String: String], + element: EnumCaseElementDecl + ) throws -> [TSVarDecl] { return try withErrorCollector { collect in element.associatedValues.enumerated().compactMap { (i, value) in collect(at: value.interfaceName ?? "_\(i)") { @@ -412,8 +438,11 @@ private struct EncodeObjFuncGen { expr = try generator.converter(for: value.interfaceType) .callEncodeField(entity: expr) + let varName = nameProvider.provide(base: TSKeyword.escaped(value.codableLabel)) + names[value.codableLabel] = varName + return TSVarDecl( - kind: .const, name: TSKeyword.escaped(value.codableLabel), + kind: .const, name: varName, initializer: expr ) } @@ -421,16 +450,24 @@ private struct EncodeObjFuncGen { } } - func buildCaseObject(element: EnumCaseElementDecl) throws -> TSObjectExpr { - return TSObjectExpr(element.associatedValues.map { (value) in + func buildCaseObject( + names: [String: String], + element: EnumCaseElementDecl + ) throws -> TSObjectExpr { + return TSObjectExpr(try element.associatedValues.map { (value) in + let varName = try names[value.codableLabel].unwrap(name: "var name") return TSObjectExpr.Field.named( name: value.codableLabel, - value: TSIdentExpr(TSKeyword.escaped(value.codableLabel)) + value: TSIdentExpr(varName) ) }) } - func caseBody(element: EnumCaseElementDecl) throws -> [any ASTNode] { + func caseBody( + nameProvider: NameProvider, + element: EnumCaseElementDecl + ) throws -> [any ASTNode] { + var nameProvider = nameProvider var code: [any ASTNode] = [] if !element.associatedValues.isEmpty { @@ -439,11 +476,17 @@ private struct EncodeObjFuncGen { initializer: TSMemberExpr(base: TSIdentExpr.entity, name: element.name) ) code.append(e) + nameProvider.register(name: "e") } - code += try encodeAssociatedValues(element: element) + var names: [String: String] = [:] + code += try encodeAssociatedValues( + nameProvider: &nameProvider, + names: &names, + element: element + ) - let innerObject = try buildCaseObject(element: element) + let innerObject = try buildCaseObject(names: names, element: element) let outerObject = TSObjectExpr([ .named(name: element.name, value: innerObject) @@ -456,8 +499,14 @@ private struct EncodeObjFuncGen { func generate() throws -> TSFunctionDecl? { guard let decl = try converter.encodeSignature() else { return nil } + var nameProvider = NameProvider() + nameProvider.register(signature: decl) + if type.caseElements.count == 1 { - decl.body.elements += try caseBody(element: type.caseElements[0]) + decl.body.elements += try caseBody( + nameProvider: nameProvider, + element: type.caseElements[0] + ) return decl } @@ -470,7 +519,12 @@ private struct EncodeObjFuncGen { collect(at: caseElement.name) { `switch`.cases.append( TSCaseStmt(expr: TSStringLiteralExpr(caseElement.name), elements: [ - TSBlockStmt(try caseBody(element: caseElement)) + TSBlockStmt( + try caseBody( + nameProvider: nameProvider, + element: caseElement + ) + ) ]) ) } diff --git a/Sources/CodableToTypeScript/TypeConverter/StructConverter.swift b/Sources/CodableToTypeScript/TypeConverter/StructConverter.swift index 6388061..2088f8d 100644 --- a/Sources/CodableToTypeScript/TypeConverter/StructConverter.swift +++ b/Sources/CodableToTypeScript/TypeConverter/StructConverter.swift @@ -80,7 +80,9 @@ public struct StructConverter: TypeConverter { public func decodeDecl() throws -> TSFunctionDecl? { guard let function = try decodeSignature() else { return nil } - var fields: [TSObjectExpr.Field] = [] + var nameProvider = NameProvider() + nameProvider.register(signature: function) + var varNames: [String: String] = [:] try withErrorCollector { collect in for field in decl.storedProperties.instances { @@ -93,8 +95,11 @@ public struct StructConverter: TypeConverter { .callDecodeField(json: expr) } + let varName = nameProvider.provide(base: TSKeyword.escaped(field.name)) + varNames[field.name] = varName + let def = TSVarDecl( - kind: .const, name: TSKeyword.escaped(field.name), + kind: .const, name: varName, initializer: expr ) @@ -102,8 +107,10 @@ public struct StructConverter: TypeConverter { } } + var fields: [TSObjectExpr.Field] = [] for field in decl.storedProperties.instances { - let expr = TSIdentExpr(TSKeyword.escaped(field.name)) + let varName = try varNames[field.name].unwrap(name: "var name") + let expr = TSIdentExpr(varName) fields.append( .named( @@ -138,7 +145,9 @@ public struct StructConverter: TypeConverter { public func encodeDecl() throws -> TSFunctionDecl? { guard let function = try encodeSignature() else { return nil } - var fields: [TSObjectExpr.Field] = [] + var nameProvider = NameProvider() + nameProvider.register(signature: function) + var varNames: [String: String] = [:] for field in decl.storedProperties.instances { var expr: any TSExpr = TSMemberExpr( @@ -149,16 +158,21 @@ public struct StructConverter: TypeConverter { expr = try generator.converter(for: field.interfaceType) .callEncodeField(entity: expr) + let varName = nameProvider.provide(base: TSKeyword.escaped(field.name)) + varNames[field.name] = varName + let def = TSVarDecl( - kind: .const, name: TSKeyword.escaped(field.name), + kind: .const, name: varName, initializer: expr ) function.body.elements.append(def) } + var fields: [TSObjectExpr.Field] = [] for field in decl.storedProperties.instances { - let expr = TSIdentExpr(TSKeyword.escaped(field.name)) + let varName = try varNames[field.name].unwrap(name: "var name") + let expr = TSIdentExpr(varName) fields.append( .named( diff --git a/Tests/CodableToTypeScriptTests/Generate/GenerateEnumTests.swift b/Tests/CodableToTypeScriptTests/Generate/GenerateEnumTests.swift index 2fb49ee..6b71ddc 100644 --- a/Tests/CodableToTypeScriptTests/Generate/GenerateEnumTests.swift +++ b/Tests/CodableToTypeScriptTests/Generate/GenerateEnumTests.swift @@ -339,4 +339,40 @@ export function E_decode(json: E$JSON): E { ] ) } + + func testConflictPropertyName() throws { + try assertGenerate( + source: """ +enum E { + case entity(entity: String, json: String, e: String, j: String) + case json + case t(T) +} +""", + expecteds: [ + // decode +""" +const json2 = j.json; +""", """ +const j2 = j.j; +""", """ +json: json2 +""", """ +j: j2 +""", + +// encode +""" +const entity2 = e.entity; +""", """ +const e2 = e.e; +""", """ +entity: entity2, +""", """ +e: e2 +""" + ], + unexpecteds: [] + ) + } } diff --git a/Tests/CodableToTypeScriptTests/Generate/GenerateStructTests.swift b/Tests/CodableToTypeScriptTests/Generate/GenerateStructTests.swift index af82ee0..4526923 100644 --- a/Tests/CodableToTypeScriptTests/Generate/GenerateStructTests.swift +++ b/Tests/CodableToTypeScriptTests/Generate/GenerateStructTests.swift @@ -430,4 +430,32 @@ export type S = { ] ) } + + func testConflictPropertyName() throws { + try assertGenerate( + source: """ +struct S { + var entity: String + var json: String + var t: T +} +""", + expecteds: [ + // decode +""" +const json2 = json.json; +""", """ +json: json2, +""", + +// encode +""" +const entity2 = entity.entity; +""", """ +entity: entity2, +""" + ], + unexpecteds: [] + ) + } }