Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Sources/CodableToTypeScript/Basic/NameProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import TypeScriptAST

struct NameProvider {
init() {}

private var used: Set<String> = []

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
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2から始めて衝突しなくなるまでインクリメントし続ける。

}

mutating func provideIfUnused(name: String) -> String? {
if used.contains(name) {
return nil
}
used.insert(name)
return name
}
}
88 changes: 71 additions & 17 deletions Sources/CodableToTypeScript/TypeConverter/EnumConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ private struct DecodeObjFuncGen {
}

private func decodeAssociatedValues(
nameProvider: inout NameProvider,
names: inout [String: String],
caseElement: EnumCaseElementDecl,
json: any TSExpr
) throws -> [TSVarDecl] {
Expand All @@ -282,26 +284,37 @@ private struct DecodeObjFuncGen {
expr = try generator.converter(for: value.interfaceType)
.callDecodeField(json: expr)

let varName = nameProvider.provide(base: TSKeyword.escaped(label))
names[label] = varName
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

用意された名前は後で参照する時のために辞書に入れておく


return TSVarDecl(
kind: .const, name: TSKeyword.escaped(label),
kind: .const, name: varName,
initializer: expr
)
}
}
}
}

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")
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

用意した名前を辞書から引く。
エスケープ済みの名前を格納してるので利用側のエスケープ処理が不要になっている。

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 {
Expand All @@ -313,9 +326,13 @@ private struct DecodeObjFuncGen {
)
)
block.append(j)
nameProvider.register(name: "j")
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enumについては ej という変数も使用するが、
これとも衝突しないように利用済みの名前を追記しておく

}

var names: [String: String] = [:]
block += try decodeAssociatedValues(
nameProvider: &nameProvider,
names: &names,
caseElement: caseElement,
json: TSIdentExpr("j")
)
Expand All @@ -327,7 +344,10 @@ private struct DecodeObjFuncGen {
),
.named(
name: caseElement.name,
value: buildCaseObject(caseElement: caseElement)
value: try buildCaseObject(
names: names,
caseElement: caseElement
)
)
])

Expand All @@ -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)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

関数の仮引数名を登録しておく。

これは関数の実装部全体で有効なのでこのnameProviderをこの先で引き継ぐが、
ローカルスコープ同士の間では名前の衝突は起きない。

そこはうまく局所的な生成関数に nameProvider をコピー渡しすることで、隣のスコープ同士が影響しないようにしつつ、関数仮引数という上流のスコープの影響は引き継がせる。

var topStmt: (any TSStmt)?

func appendElse(stmt: any TSStmt) {
Expand All @@ -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
)

Expand All @@ -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)") {
Expand All @@ -412,25 +438,36 @@ 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
)
}
}
}
}

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 {
Expand All @@ -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)
Expand All @@ -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
}

Expand All @@ -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
)
)
])
)
}
Expand Down
26 changes: 20 additions & 6 deletions Sources/CodableToTypeScript/TypeConverter/StructConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -93,17 +95,22 @@ 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
)

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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
36 changes: 36 additions & 0 deletions Tests/CodableToTypeScriptTests/Generate/GenerateEnumTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -339,4 +339,40 @@ export function E_decode(json: E$JSON): E {
]
)
}

func testConflictPropertyName() throws {
try assertGenerate(
source: """
enum E<T> {
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: []
)
}
}
28 changes: 28 additions & 0 deletions Tests/CodableToTypeScriptTests/Generate/GenerateStructTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,32 @@ export type S = {
]
)
}

func testConflictPropertyName() throws {
try assertGenerate(
source: """
struct S<T> {
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: []
)
}
}
Loading