Skip to content

Commit fb711be

Browse files
BridgeJS: normalize keyword-escaped names in generated code
`@JS func `default`() {}` produced `bjs_`default`` as the WebAssembly export symbol — an invalid `@_expose`/`@_cdecl` name — because the codegenerator read `TokenSyntax.text` (which keeps backticks for keyword-escaped identifiers) and never normalized it. The backticks leaked into ABI names, generated Swift identifiers, and JS/d.ts names. The fix splits name escaping into three context-aware helpers, grounded in swift-syntax's grammar (`isValidSwiftIdentifier(for:)`): - `backtickIfNeeded()` — declaration position (`var`/`let`/property/ `let`-binding). Keywords are not valid bare here; escapes all keywords including `self` (`var self: Int` is invalid). - `backtickIfNeededForMemberAccess()` — `.name` position. Most keywords are valid bare (`obj.class`, `.break`); `self` is the exception because `obj.self` is the identity expression, not a member access, so a property named `self` must be written as `` obj.`self` ``. - `backtickIfNeededForLocalReference()` — body references. `self` is valid bare (refers to the parameter); other keywords still need escaping. Parameter *declarations* (`_ name: Type`) stay bare for every keyword except `inout`. All helpers are idempotent (no double-wrapping) and escape dotted paths per-component. Names are normalized (backticks stripped) at extraction in `SwiftToSkeleton` so ABI/JSON/JS names are clean. Adds `IdentifierEscapingTests` pinning the rules against `isValidSwiftIdentifier(for:)`, a `KeywordNames` codegen snapshot covering all positions, and macro tests for keyword names under `@JSFunction`/`@JSGetter`/`@JSSetter`. 200 tests pass; no existing snapshots changed.
1 parent e53155d commit fb711be

12 files changed

Lines changed: 1630 additions & 60 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -209,20 +209,29 @@ public class ExportSwift {
209209
func liftParameter(param: Parameter) throws {
210210
parameters.append(param)
211211
let liftingInfo = try param.type.liftParameterInfo()
212-
let argumentsToLift: [String]
212+
// `referenceNames` are used in the function body to forward the parameter
213+
// into a lifting call, so keyword names (e.g. `in`) must be escaped here.
214+
// `declarationNames` are the ABI parameter declaration names, which are valid
215+
// bare for every keyword except `inout`, so they stay unescaped.
216+
let referenceNames: [String]
217+
let declarationNames: [String]
213218
if liftingInfo.parameters.count == 1 {
214-
argumentsToLift = [param.name]
219+
referenceNames = [param.name.backtickIfNeededForLocalReference()]
220+
declarationNames = [param.name]
215221
} else {
216-
argumentsToLift = liftingInfo.parameters.map { (name, _) in param.name + name.capitalizedFirstLetter }
222+
let synthesized = liftingInfo.parameters.map { (name, _) in param.name + name.capitalizedFirstLetter }
223+
referenceNames = synthesized
224+
declarationNames = synthesized
217225
}
226+
let argumentsToLift = referenceNames
218227

219228
let typeNameForIntrinsic: String
220229
let liftingExpr: ExprSyntax
221230

222231
switch param.type {
223232
case .closure(let signature, _):
224233
typeNameForIntrinsic = param.type.swiftType
225-
liftingExpr = ExprSyntax("_BJS_Closure_\(raw: signature.mangleName).bridgeJSLift(\(raw: param.name))")
234+
liftingExpr = ExprSyntax("_BJS_Closure_\(raw: signature.mangleName).bridgeJSLift(\(raw: param.name.backtickIfNeededForLocalReference()))")
226235
case .swiftStruct(let structName):
227236
typeNameForIntrinsic = structName
228237
liftingExpr = ExprSyntax("\(raw: structName).bridgeJSLiftParameter()")
@@ -248,7 +257,7 @@ public class ExportSwift {
248257
}
249258

250259
liftedParameterExprs.append(liftingExpr)
251-
for (name, type) in zip(argumentsToLift, liftingInfo.parameters.map { $0.type }) {
260+
for (name, type) in zip(declarationNames, liftingInfo.parameters.map { $0.type }) {
252261
abiParameterSignatures.append((name, type))
253262
}
254263
}
@@ -560,15 +569,15 @@ public class ExportSwift {
560569
func callName(for property: ExportedProperty) -> String {
561570
switch self {
562571
case .enumStatic(let enumDef):
563-
return "\(enumDef.swiftCallName).\(property.name)"
572+
return "\(enumDef.swiftCallName).\(property.name.backtickIfNeededForMemberAccess())"
564573
case .classStatic(let klass):
565574
// property.callName() would use staticContext (the ABI name) as prefix;
566575
// use swiftCallName directly so the emitted expression is valid Swift.
567-
return "\(klass.swiftCallName).\(property.name)"
576+
return "\(klass.swiftCallName).\(property.name.backtickIfNeededForMemberAccess())"
568577
case .classInstance:
569-
return property.callName()
578+
return property.name.backtickIfNeededForMemberAccess()
570579
case .structStatic(let structDef):
571-
return "\(structDef.swiftCallName).\(property.name)"
580+
return "\(structDef.swiftCallName).\(property.name.backtickIfNeededForMemberAccess())"
572581
}
573582
}
574583
}
@@ -626,7 +635,7 @@ public class ExportSwift {
626635

627636
if isStatic {
628637
let klassName = callName.components(separatedBy: ".").dropLast().joined(separator: ".")
629-
setterBuilder.callStaticPropertySetter(klassName: klassName, propertyName: property.name)
638+
setterBuilder.callStaticPropertySetter(klassName: klassName, propertyName: property.name.backtickIfNeededForMemberAccess())
630639
} else {
631640
setterBuilder.callPropertySetter(propertyName: callName)
632641
}
@@ -645,10 +654,10 @@ public class ExportSwift {
645654
}
646655

647656
if function.effects.isStatic, let staticContext = function.staticContext {
648-
let callName = "\(staticContextBaseName(staticContext)).\(function.name)"
657+
let callName = "\(staticContextBaseName(staticContext).backtickIfNeeded()).\(function.name.backtickIfNeededForMemberAccess())"
649658
builder.call(name: callName, returnType: function.returnType)
650659
} else {
651-
builder.call(name: function.name, returnType: function.returnType)
660+
builder.call(name: function.name.backtickIfNeeded(), returnType: function.returnType)
652661
}
653662

654663
try builder.lowerReturnValue(returnType: function.returnType)
@@ -691,9 +700,9 @@ public class ExportSwift {
691700
}
692701

693702
if method.effects.isStatic {
694-
builder.call(name: "\(ownerTypeName).\(method.name)", returnType: method.returnType)
703+
builder.call(name: "\(ownerTypeName).\(method.name.backtickIfNeededForMemberAccess())", returnType: method.returnType)
695704
} else {
696-
builder.callMethod(methodName: method.name, returnType: method.returnType)
705+
builder.callMethod(methodName: method.name.backtickIfNeededForMemberAccess(), returnType: method.returnType)
697706
}
698707
try builder.lowerReturnValue(returnType: method.returnType)
699708
return builder.render(abiName: method.abiName)
@@ -1104,7 +1113,7 @@ struct EnumCodegen {
11041113
for (index, enumCase) in enumDef.cases.enumerated() {
11051114
printer.write("case \(index):")
11061115
printer.indent {
1107-
printer.write("self = .\(enumCase.name)")
1116+
printer.write("self = .\(enumCase.name.backtickIfNeededForMemberAccess())")
11081117
}
11091118
}
11101119
printer.write("default:")
@@ -1120,7 +1129,7 @@ struct EnumCodegen {
11201129
printer.indent {
11211130
printer.write("switch self {")
11221131
for (index, enumCase) in enumDef.cases.enumerated() {
1123-
printer.write("case .\(enumCase.name):")
1132+
printer.write("case .\(enumCase.name.backtickIfNeededForMemberAccess()):")
11241133
printer.indent {
11251134
printer.write("return \(index)")
11261135
}
@@ -1186,7 +1195,7 @@ struct EnumCodegen {
11861195
if enumCase.associatedValues.isEmpty {
11871196
printer.write("case \(caseIndex):")
11881197
printer.indent {
1189-
printer.write("return .\(enumCase.name)")
1198+
printer.write("return .\(enumCase.name.backtickIfNeededForMemberAccess())")
11901199
}
11911200
} else {
11921201
printer.write("case \(caseIndex):")
@@ -1201,7 +1210,7 @@ struct EnumCodegen {
12011210
return "\(labelPrefix)\(liftExpr)"
12021211
}
12031212
printer.indent {
1204-
printer.write("return .\(enumCase.name)(\(argList.joined(separator: ", ")))")
1213+
printer.write("return .\(enumCase.name.backtickIfNeededForMemberAccess())(\(argList.joined(separator: ", ")))")
12051214
}
12061215
}
12071216
}
@@ -1215,7 +1224,7 @@ struct EnumCodegen {
12151224
let paramName = associatedValue.label ?? "param\(index)"
12161225
let statements = stackCodegen.lowerStatements(
12171226
for: associatedValue.type,
1218-
accessor: paramName,
1227+
accessor: paramName.backtickIfNeededForLocalReference(),
12191228
varPrefix: paramName
12201229
)
12211230
for statement in statements {
@@ -1227,15 +1236,15 @@ struct EnumCodegen {
12271236
private func generateReturnSwitchCases(printer: CodeFragmentPrinter, enumDef: ExportedEnum) {
12281237
for (caseIndex, enumCase) in enumDef.cases.enumerated() {
12291238
if enumCase.associatedValues.isEmpty {
1230-
printer.write("case .\(enumCase.name):")
1239+
printer.write("case .\(enumCase.name.backtickIfNeededForMemberAccess()):")
12311240
printer.indent {
12321241
printer.write("return Int32(\(caseIndex))")
12331242
}
12341243
} else {
12351244
let pattern = enumCase.associatedValues.enumerated()
1236-
.map { index, associatedValue in "let \(associatedValue.label ?? "param\(index)")" }
1245+
.map { index, associatedValue in "let \((associatedValue.label ?? "param\(index)").backtickIfNeeded())" }
12371246
.joined(separator: ", ")
1238-
printer.write("case .\(enumCase.name)(\(pattern)):")
1247+
printer.write("case .\(enumCase.name.backtickIfNeededForMemberAccess())(\(pattern)):")
12391248
printer.indent {
12401249
generatePayloadPushingCode(printer: printer, associatedValues: enumCase.associatedValues)
12411250
// Push tag AFTER payloads so it's popped first (LIFO) by the JS lift function.
@@ -1332,12 +1341,12 @@ struct StructCodegen {
13321341
let instanceProps = structDef.properties.filter { !$0.isStatic }
13331342

13341343
for property in instanceProps.reversed() {
1335-
let fieldName = property.name
1344+
let fieldName = property.name.backtickIfNeeded()
13361345
let liftExpr = stackCodegen.liftExpression(for: property.type)
13371346
lines.append("let \(fieldName) = \(liftExpr)")
13381347
}
13391348

1340-
let initArgs = instanceProps.map { "\($0.name): \($0.name)" }.joined(separator: ", ")
1349+
let initArgs = instanceProps.map { "\($0.name): \($0.name.backtickIfNeededForLocalReference())" }.joined(separator: ", ")
13411350
lines.append("return \(structDef.swiftCallName)(\(initArgs))")
13421351

13431352
return lines
@@ -1350,7 +1359,7 @@ struct StructCodegen {
13501359
for property in instanceProps {
13511360
let statements = stackCodegen.lowerStatements(
13521361
for: property.type,
1353-
accessor: "self.\(property.name)",
1362+
accessor: "self.\(property.name.backtickIfNeededForMemberAccess())",
13541363
varPrefix: property.name
13551364
)
13561365
for statement in statements {
@@ -1418,7 +1427,7 @@ struct ProtocolCodegen {
14181427
)
14191428
externDecls.append(DeclSyntax("\(raw: externDeclPrinter.lines.joined(separator: "\n"))"))
14201429
let methodImplPrinter = CodeFragmentPrinter()
1421-
methodImplPrinter.write("func \(method.name)\(signature) {")
1430+
methodImplPrinter.write("func \(method.name.backtickIfNeeded())\(signature) {")
14221431
methodImplPrinter.indent {
14231432
methodImplPrinter.write(lines: builder.body.lines)
14241433
}
@@ -1441,7 +1450,7 @@ struct ProtocolCodegen {
14411450
}
14421451

14431452
let structDeclPrinter = CodeFragmentPrinter()
1444-
structDeclPrinter.write("struct \(wrapperName): \(protocolName), _BridgedSwiftProtocolWrapper {")
1453+
structDeclPrinter.write("struct \(wrapperName): \(protocolName.backtickIfNeeded()), _BridgedSwiftProtocolWrapper {")
14451454
structDeclPrinter.indent {
14461455
structDeclPrinter.write("let jsObject: JSObject")
14471456
structDeclPrinter.nextLine()
@@ -1509,7 +1518,7 @@ struct ProtocolCodegen {
15091518
let getterExternDecl = DeclSyntax("\(raw: getterExternDeclPrinter.lines.joined(separator: "\n"))")
15101519
var externDecls: [DeclSyntax] = [getterExternDecl]
15111520

1512-
printer.write("var \(property.name): \(property.type.swiftType) {")
1521+
printer.write("var \(property.name.backtickIfNeeded()): \(property.type.swiftType) {")
15131522
try printer.indent {
15141523
printer.write("get {")
15151524
printer.indent {
@@ -1612,19 +1621,19 @@ extension BridgeType {
16121621
case .jsValue: return "JSValue"
16131622
case .jsObject(nil): return "JSObject"
16141623
case .jsObject(let name?): return name
1615-
case .swiftHeapObject(let name): return name
1624+
case .swiftHeapObject(let name): return name.backtickIfNeeded()
16161625
case .unsafePointer(let ptr): return ptr.swiftType
1617-
case .swiftProtocol(let name): return "Any\(name)"
1626+
case .swiftProtocol(let name): return "Any\(name.backtickIfNeeded())"
16181627
case .void: return "Void"
16191628
case .nullable(let wrappedType, let kind):
16201629
return kind == .null ? "Optional<\(wrappedType.swiftType)>" : "JSUndefinedOr<\(wrappedType.swiftType)>"
16211630
case .array(let elementType): return "[\(elementType.swiftType)]"
16221631
case .dictionary(let valueType): return "[String: \(valueType.swiftType)]"
1623-
case .caseEnum(let name): return name
1624-
case .rawValueEnum(let name, _): return name
1625-
case .associatedValueEnum(let name): return name
1626-
case .swiftStruct(let name): return name
1627-
case .namespaceEnum(let name): return name
1632+
case .caseEnum(let name): return name.backtickIfNeeded()
1633+
case .rawValueEnum(let name, _): return name.backtickIfNeeded()
1634+
case .associatedValueEnum(let name): return name.backtickIfNeeded()
1635+
case .swiftStruct(let name): return name.backtickIfNeeded()
1636+
case .namespaceEnum(let name): return name.backtickIfNeeded()
16281637
case .closure(let signature, let useJSTypedClosure):
16291638
let paramTypes = signature.parameters.map { $0.swiftType }.joined(separator: ", ")
16301639
let effectsStr = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")

0 commit comments

Comments
 (0)