Skip to content

Commit f0e39cd

Browse files
WIP
1 parent 1cde0c1 commit f0e39cd

File tree

15 files changed

+200
-68
lines changed

15 files changed

+200
-68
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ImportSwiftMacros.swift

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,20 @@ public final class ImportSwiftMacros {
162162
var getters: [ImportedGetterSkeleton] = []
163163
var setters: [ImportedSetterSkeleton] = []
164164

165+
// First pass: collect all explicit @JSSetter functions to track which properties have explicit setters
166+
var propertiesWithExplicitSetters = Set<String>()
167+
for member in node.memberBlock.members {
168+
if let function = member.decl.as(FunctionDeclSyntax.self),
169+
hasJSSetterAttribute(function.attributes),
170+
!isStatic(function.modifiers)
171+
{
172+
if let setter = parseSetterSkeleton(function, enclosingTypeName: typeName) {
173+
propertiesWithExplicitSetters.insert(setter.name)
174+
}
175+
}
176+
}
177+
178+
// Second pass: process all members
165179
for member in node.memberBlock.members {
166180
if let initializer = member.decl.as(InitializerDeclSyntax.self) {
167181
if hasJSFunctionAttribute(initializer.attributes) {
@@ -187,7 +201,11 @@ public final class ImportSwiftMacros {
187201
if let parsed = parseFunction(function, enclosingTypeName: typeName, isStaticMember: true) {
188202
importedFunctions.append(parsed)
189203
}
190-
} else if let parsed = parseFunction(function, enclosingTypeName: typeName, isStaticMember: false) {
204+
} else if let parsed = parseFunction(
205+
function,
206+
enclosingTypeName: typeName,
207+
isStaticMember: false
208+
) {
191209
methods.append(parsed)
192210
}
193211
continue
@@ -213,16 +231,11 @@ public final class ImportSwiftMacros {
213231
if isStatic(variable.modifiers) {
214232
collectTopLevelProperty(from: variable, enclosingTypeName: typeName)
215233
} else {
216-
// Parse as getter/setter skeletons
234+
// Parse as getter skeleton only
235+
// Setters are ONLY generated from explicit @JSSetter functions, never from properties
217236
if let getter = parseGetterSkeleton(variable, enclosingTypeName: typeName) {
218237
getters.append(getter)
219238
}
220-
let isReadonly = variable.bindingSpecifier.tokenKind == .keyword(.let)
221-
if !isReadonly {
222-
if let setter = parseSetterSkeletonFromProperty(variable, enclosingTypeName: typeName) {
223-
setters.append(setter)
224-
}
225-
}
226239
}
227240
continue
228241
}
@@ -496,9 +509,19 @@ public final class ImportSwiftMacros {
496509
let functionName = node.name.text
497510

498511
// Extract property name from setter function name (e.g., "setFoo" -> "foo")
499-
let propertyName = String(functionName.dropFirst(3))
500-
// Convert first character to lowercase (e.g., "Foo" -> "foo")
501-
let baseName = propertyName.prefix(1).lowercased() + propertyName.dropFirst()
512+
// Strip backticks if present (e.g., "set`prefix`" -> "prefix")
513+
let rawFunctionName =
514+
functionName.hasPrefix("`") && functionName.hasSuffix("`") && functionName.count > 2
515+
? String(functionName.dropFirst().dropLast())
516+
: functionName
517+
518+
guard rawFunctionName.hasPrefix("set"), rawFunctionName.count > 3 else {
519+
return nil
520+
}
521+
522+
let propertyName = String(rawFunctionName.dropFirst(3))
523+
// Convert first character to lowercase (e.g., "Foo" -> "foo") and normalize
524+
let baseName = normalizeIdentifier(propertyName.prefix(1).lowercased() + propertyName.dropFirst())
502525

503526
// Get the parameter type
504527
let parameters = node.signature.parameterClause.parameters

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,7 +1164,8 @@ struct BridgeJSLink {
11641164
for getter in type.getters {
11651165
propertyNames.insert(getter.name)
11661166
let hasSetter = type.setters.contains { $0.name == getter.name }
1167-
let propertySignature = hasSetter
1167+
let propertySignature =
1168+
hasSetter
11681169
? "\(getter.name): \(resolveTypeScriptType(getter.type));"
11691170
: "readonly \(getter.name): \(resolveTypeScriptType(getter.type));"
11701171
printer.write(propertySignature)
@@ -2900,7 +2901,7 @@ extension BridgeJSLink {
29002901
importObjectBuilder.assignToImportObject(name: getterAbiName, function: js)
29012902
importObjectBuilder.appendDts(dts)
29022903
}
2903-
2904+
29042905
for setter in type.setters {
29052906
let setterAbiName = setter.abiName(context: type)
29062907
let (js, dts) = try renderImportedSetter(

Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ extension JSFunctionMacro: BodyMacro {
1616
let isStatic = JSMacroHelper.isStatic(functionDecl.modifiers)
1717
let isInstanceMember = enclosingTypeName != nil && !isStatic
1818

19-
let name = functionDecl.name.text
19+
// Strip backticks from function name (e.g., "`prefix`" -> "prefix")
20+
// Backticks are only needed for Swift identifiers, not function names
21+
let name = JSMacroHelper.stripBackticks(functionDecl.name.text)
2022
let glueName = JSMacroHelper.glueName(baseName: name, enclosingTypeName: enclosingTypeName)
2123

2224
var arguments: [String] = []

Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ extension JSGetterMacro: AccessorMacro {
2323
let isStatic = JSMacroHelper.isStatic(variableDecl.modifiers)
2424
let isInstanceMember = enclosingTypeName != nil && !isStatic
2525

26-
let propertyName = identifier.identifier.text
26+
// Strip backticks from property name (e.g., "`prefix`" -> "prefix")
27+
// Backticks are only needed for Swift identifiers, not function names
28+
let propertyName = JSMacroHelper.stripBackticks(identifier.identifier.text)
2729
let getterName = JSMacroHelper.glueName(
2830
baseName: propertyName,
2931
enclosingTypeName: enclosingTypeName,

Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ enum JSMacroMessage: String, DiagnosticMessage {
77
case unsupportedDeclaration = "@JSFunction can only be applied to functions or initializers."
88
case unsupportedVariable = "@JSGetter can only be applied to single-variable declarations."
99
case unsupportedSetterDeclaration = "@JSSetter can only be applied to functions."
10-
case invalidSetterName = "@JSSetter function name must start with 'set' followed by a property name (e.g., 'setFoo')."
10+
case invalidSetterName =
11+
"@JSSetter function name must start with 'set' followed by a property name (e.g., 'setFoo')."
1112
case setterRequiresParameter = "@JSSetter function must have at least one parameter."
1213

1314
var message: String { rawValue }
@@ -86,4 +87,14 @@ enum JSMacroHelper {
8687
return ""
8788
}
8889
}
90+
91+
/// Strips backticks from an identifier name.
92+
/// Swift identifiers with keywords are escaped with backticks (e.g., `` `prefix` ``),
93+
/// but function names should not include backticks.
94+
static func stripBackticks(_ name: String) -> String {
95+
if name.hasPrefix("`") && name.hasSuffix("`") && name.count > 2 {
96+
return String(name.dropFirst().dropLast())
97+
}
98+
return name
99+
}
89100
}

Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,55 +12,59 @@ extension JSSetterMacro: BodyMacro {
1212
in context: some MacroExpansionContext
1313
) throws -> [CodeBlockItemSyntax] {
1414
guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else {
15-
context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedSetterDeclaration))
15+
context.diagnose(
16+
Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedSetterDeclaration)
17+
)
1618
return []
1719
}
1820

1921
let functionName = functionDecl.name.text
20-
22+
2123
// Extract property name from setter function name (e.g., "setFoo" -> "foo")
22-
guard functionName.hasPrefix("set"), functionName.count > 3 else {
24+
// Strip backticks if present (e.g., "set`prefix`" -> "prefix")
25+
let rawFunctionName = JSMacroHelper.stripBackticks(functionName)
26+
guard rawFunctionName.hasPrefix("set"), rawFunctionName.count > 3 else {
2327
context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.invalidSetterName))
2428
return []
2529
}
26-
27-
let propertyName = String(functionName.dropFirst(3))
30+
31+
let propertyName = String(rawFunctionName.dropFirst(3))
2832
guard !propertyName.isEmpty else {
2933
context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.invalidSetterName))
3034
return []
3135
}
32-
36+
3337
// Convert first character to lowercase (e.g., "Foo" -> "foo")
3438
let baseName = propertyName.prefix(1).lowercased() + propertyName.dropFirst()
35-
39+
3640
let enclosingTypeName = JSMacroHelper.enclosingTypeName(from: context)
3741
let isStatic = JSMacroHelper.isStatic(functionDecl.modifiers)
3842
let isInstanceMember = enclosingTypeName != nil && !isStatic
39-
43+
4044
let glueName = JSMacroHelper.glueName(
4145
baseName: baseName,
4246
enclosingTypeName: enclosingTypeName,
4347
operation: "set"
4448
)
45-
49+
4650
var arguments: [String] = []
4751
if isInstanceMember {
4852
arguments.append("self.jsObject")
4953
}
50-
54+
5155
// Get the parameter name(s) - setters typically have one parameter
5256
let parameters = functionDecl.signature.parameterClause.parameters
5357
guard let firstParam = parameters.first else {
5458
context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.setterRequiresParameter))
5559
return []
5660
}
57-
61+
5862
let paramName = firstParam.secondName ?? firstParam.firstName
5963
arguments.append(paramName.text)
60-
64+
6165
let argsJoined = arguments.joined(separator: ", ")
6266
let call = "\(glueName)(\(argsJoined))"
63-
67+
6468
// Setters should throw JSException, so always use try
6569
return [CodeBlockItemSyntax(stringLiteral: "try \(call)")]
6670
}

Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,13 @@ private func recursivelyCollectSwiftFiles(from directory: URL) -> [URL] {
232232
var swiftFiles: [URL] = []
233233
let fileManager = FileManager.default
234234

235-
guard let enumerator = fileManager.enumerator(
236-
at: directory,
237-
includingPropertiesForKeys: [.isRegularFileKey, .isDirectoryKey],
238-
options: [.skipsHiddenFiles]
239-
) else {
235+
guard
236+
let enumerator = fileManager.enumerator(
237+
at: directory,
238+
includingPropertiesForKeys: [.isRegularFileKey, .isDirectoryKey],
239+
options: [.skipsHiddenFiles]
240+
)
241+
else {
240242
return []
241243
}
242244

Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ import BridgeJSMacros
169169
""",
170170
diagnostics: [
171171
DiagnosticSpec(
172-
message: "@JSSetter function name must start with 'set' followed by a property name (e.g., 'setFoo').",
172+
message:
173+
"@JSSetter function name must start with 'set' followed by a property name (e.g., 'setFoo').",
173174
line: 1,
174175
column: 1
175176
)
@@ -190,7 +191,8 @@ import BridgeJSMacros
190191
""",
191192
diagnostics: [
192193
DiagnosticSpec(
193-
message: "@JSSetter function name must start with 'set' followed by a property name (e.g., 'setFoo').",
194+
message:
195+
"@JSSetter function name must start with 'set' followed by a property name (e.g., 'setFoo').",
194196
line: 1,
195197
column: 1
196198
)

Plugins/PackageToJS/Sources/PackageToJS.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -602,19 +602,21 @@ struct PackagingPlanner {
602602
let bridgeDts = outputDir.appending(path: "bridge-js.d.ts")
603603
packageInputs.append(
604604
make.addTask(inputFiles: exportedSkeletons + importedSkeletons, output: bridgeJs) { _, scope in
605-
let link = try BridgeJSLink(
606-
exportedSkeletons: exportedSkeletons.map {
607-
let decoder = JSONDecoder()
608-
let data = try Data(contentsOf: URL(fileURLWithPath: scope.resolve(path: $0).path))
609-
return try decoder.decode(ExportedSkeleton.self, from: data)
610-
},
611-
importedSkeletons: importedSkeletons.map {
612-
let decoder = JSONDecoder()
613-
let data = try Data(contentsOf: URL(fileURLWithPath: scope.resolve(path: $0).path))
614-
return try decoder.decode(ImportedModuleSkeleton.self, from: data)
615-
},
605+
var link = BridgeJSLink(
606+
exportedSkeletons: [],
607+
importedSkeletons: [],
616608
sharedMemory: Self.isSharedMemoryEnabled(triple: triple)
617609
)
610+
611+
// Decode unified skeleton format
612+
// Unified skeleton files can contain both exported and imported parts
613+
// Deduplicate file paths since we may have the same file in both lists
614+
let allSkeletonPaths = Set(exportedSkeletons + importedSkeletons)
615+
for skeletonPath in allSkeletonPaths {
616+
let data = try Data(contentsOf: URL(fileURLWithPath: scope.resolve(path: skeletonPath).path))
617+
try link.addSkeletonFile(data: data)
618+
}
619+
618620
let (outputJs, outputDts) = try link.link()
619621
try system.writeFile(atPath: scope.resolve(path: bridgeJs).path, content: Data(outputJs.utf8))
620622
try system.writeFile(atPath: scope.resolve(path: bridgeDts).path, content: Data(outputDts.utf8))

Plugins/PackageToJS/Sources/PackageToJSPlugin.swift

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -706,10 +706,8 @@ class SkeletonCollector {
706706
private var visitedProducts: Set<Product.ID> = []
707707
private var visitedTargets: Set<Target.ID> = []
708708

709-
var exportedSkeletons: [URL] = []
710-
var importedSkeletons: [URL] = []
711-
let exportedSkeletonFile = "BridgeJS.ExportSwift.json"
712-
let importedSkeletonFile = "BridgeJS.ImportTS.json"
709+
var skeletons: [URL] = []
710+
let skeletonFile = "BridgeJS.json"
713711
let context: PluginContext
714712

715713
init(context: PluginContext) {
@@ -721,7 +719,9 @@ class SkeletonCollector {
721719
return ([], [])
722720
}
723721
visit(product: product, package: context.package)
724-
return (exportedSkeletons, importedSkeletons)
722+
// Unified skeleton files contain both exported and imported parts
723+
// Return the same list for both since BridgeJSLink.addSkeletonFile handles the unified format
724+
return (skeletons, skeletons)
725725
}
726726

727727
func collectFromTests() -> (exportedSkeletons: [URL], importedSkeletons: [URL]) {
@@ -732,7 +732,9 @@ class SkeletonCollector {
732732
for test in tests {
733733
visit(target: test, package: context.package)
734734
}
735-
return (exportedSkeletons, importedSkeletons)
735+
// Unified skeleton files contain both exported and imported parts
736+
// Return the same list for both since BridgeJSLink.addSkeletonFile handles the unified format
737+
return (skeletons, skeletons)
736738
}
737739

738740
private func visit(product: Product, package: Package) {
@@ -746,6 +748,11 @@ class SkeletonCollector {
746748
private func visit(target: Target, package: Package) {
747749
if visitedTargets.contains(target.id) { return }
748750
visitedTargets.insert(target.id)
751+
if let sourceModuleTarget = target as? SourceModuleTarget {
752+
if sourceModuleTarget.kind == .macro {
753+
return
754+
}
755+
}
749756
if let target = target as? SwiftSourceModuleTarget {
750757
let directories = [
751758
target.directoryURL.appending(path: "Generated/JavaScript"),
@@ -755,13 +762,9 @@ class SkeletonCollector {
755762
.appending(path: "outputs/\(package.id)/\(target.name)/destination/BridgeJS"),
756763
]
757764
for directory in directories {
758-
let exportedSkeletonURL = directory.appending(path: exportedSkeletonFile)
759-
let importedSkeletonURL = directory.appending(path: importedSkeletonFile)
760-
if FileManager.default.fileExists(atPath: exportedSkeletonURL.path) {
761-
exportedSkeletons.append(exportedSkeletonURL)
762-
}
763-
if FileManager.default.fileExists(atPath: importedSkeletonURL.path) {
764-
importedSkeletons.append(importedSkeletonURL)
765+
let skeletonURL = directory.appending(path: skeletonFile)
766+
if FileManager.default.fileExists(atPath: skeletonURL.path) {
767+
skeletons.append(skeletonURL)
765768
}
766769
}
767770
}

0 commit comments

Comments
 (0)