Skip to content

Commit 5ccc689

Browse files
committed
BridgeJS: Include Swift doc comments in generated d.ts
Propagate Swift `///` and `/** */` documentation on exported declarations into the generated TypeScript declarations as JSDoc, so editors surface hover docs for the bridged API. The exporter now captures the leading doc comment for functions, classes, methods, properties, constructors, structs, and enums into the skeleton, and the linker renders it as a single JSDoc block. The Swift DocC field list is mapped as the inverse of the TS2Swift importer: the leading description becomes the JSDoc body, `- Parameters:`/`- Parameter x:` become `@param`, `- Returns:` becomes `@returns`, and `- Throws:` becomes `@throws`. Existing default-value annotations are merged into the same block so a parameter never emits two comment blocks; declarations without doc comments produce byte-identical output to before.
1 parent 2fc75d4 commit 5ccc689

14 files changed

Lines changed: 1808 additions & 68 deletions

File tree

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"methods" : [
1717
{
1818
"abiName" : "bjs_PlayBridgeJS_updateDetailed",
19+
"documentation" : "Structured entry point used by the playground so JS doesn't need to parse diagnostics.",
1920
"effects" : {
2021
"isAsync" : false,
2122
"isStatic" : false,

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,10 +1266,56 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
12661266
returnType: returnType,
12671267
effects: effects,
12681268
namespace: finalNamespace,
1269-
staticContext: staticContext
1269+
staticContext: staticContext,
1270+
documentation: extractDocumentation(from: node)
12701271
)
12711272
}
12721273

1274+
/// Returns the doc comment (`///` or `/** */`) attached to a declaration, with
1275+
/// markers stripped and DocC field lists (`- Parameters:`, `- Returns:`) preserved.
1276+
private func extractDocumentation(from node: some SyntaxProtocol) -> String? {
1277+
var run: [String] = []
1278+
for piece in node.leadingTrivia {
1279+
switch piece {
1280+
case .docLineComment(let text):
1281+
var line = Substring(text)
1282+
if line.hasPrefix("///") { line = line.dropFirst(3) }
1283+
if line.first == " " { line = line.dropFirst() }
1284+
if line.last == "\r" { line = line.dropLast() }
1285+
run.append(String(line))
1286+
case .docBlockComment(let text):
1287+
run.append(contentsOf: stripBlockComment(text))
1288+
case .newlines(let count), .carriageReturns(let count), .carriageReturnLineFeeds(let count):
1289+
if count >= 2 { run.removeAll() }
1290+
case .lineComment, .blockComment:
1291+
run.removeAll()
1292+
default:
1293+
continue
1294+
}
1295+
}
1296+
// Trim boundary blank lines so line (`///`) and block (`/** */`) comments
1297+
// produce a consistent skeleton value.
1298+
while run.first?.trimmingCharacters(in: .whitespaces).isEmpty == true { run.removeFirst() }
1299+
while run.last?.trimmingCharacters(in: .whitespaces).isEmpty == true { run.removeLast() }
1300+
return run.isEmpty ? nil : run.joined(separator: "\n")
1301+
}
1302+
1303+
private func stripBlockComment(_ text: String) -> [String] {
1304+
var body = Substring(text)
1305+
if body.hasPrefix("/**") { body = body.dropFirst(3) }
1306+
if body.hasSuffix("*/") { body = body.dropLast(2) }
1307+
return body.split(separator: "\n", omittingEmptySubsequences: false).map { raw -> String in
1308+
var line = raw[...]
1309+
if line.last == "\r" { line = line.dropLast() }
1310+
while let first = line.first, first == " " || first == "\t" { line = line.dropFirst() }
1311+
if line.first == "*" {
1312+
line = line.dropFirst()
1313+
if line.first == " " { line = line.dropFirst() }
1314+
}
1315+
return String(line)
1316+
}
1317+
}
1318+
12731319
private func collectEffects(signature: FunctionSignatureSyntax, isStatic: Bool = false) -> Effects? {
12741320
let isAsync = signature.effectSpecifiers?.asyncSpecifier != nil
12751321
var isThrows = false
@@ -1360,7 +1406,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
13601406
let constructor = ExportedConstructor(
13611407
abiName: "bjs_\(classAbiName)_init",
13621408
parameters: parameters,
1363-
effects: effects
1409+
effects: effects,
1410+
documentation: extractDocumentation(from: node)
13641411
)
13651412
exportedClassByName[classKey]?.constructor = constructor
13661413

@@ -1383,7 +1430,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
13831430
let constructor = ExportedConstructor(
13841431
abiName: "bjs_\(structAbiName)_init",
13851432
parameters: parameters,
1386-
effects: effects
1433+
effects: effects,
1434+
documentation: extractDocumentation(from: node)
13871435
)
13881436
exportedStructByName[structKey]?.constructor = constructor
13891437

@@ -1490,7 +1538,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
14901538
isReadonly: isReadonly,
14911539
isStatic: isStatic,
14921540
namespace: finalNamespace,
1493-
staticContext: staticContext
1541+
staticContext: staticContext,
1542+
documentation: extractDocumentation(from: node)
14941543
)
14951544

14961545
if case .enumBody(_, let key) = state {
@@ -1537,7 +1586,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
15371586
methods: [],
15381587
properties: [],
15391588
namespace: effectiveNamespace,
1540-
identityMode: classIdentityMode
1589+
identityMode: classIdentityMode,
1590+
documentation: extractDocumentation(from: node)
15411591
)
15421592
let uniqueKey = makeKey(name: name, namespace: effectiveNamespace)
15431593

@@ -1657,7 +1707,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
16571707
namespace: effectiveNamespace,
16581708
emitStyle: emitStyle,
16591709
staticMethods: [],
1660-
staticProperties: []
1710+
staticProperties: [],
1711+
documentation: extractDocumentation(from: node)
16611712
)
16621713

16631714
let enumUniqueKey = makeKey(name: name, namespace: effectiveNamespace)
@@ -1774,7 +1825,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
17741825
name: name,
17751826
methods: [],
17761827
properties: [],
1777-
namespace: effectiveNamespace
1828+
namespace: effectiveNamespace,
1829+
documentation: extractDocumentation(from: node)
17781830
)
17791831

17801832
stateStack.push(state: .protocolBody(name: name, key: protocolUniqueKey))
@@ -1798,7 +1850,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
17981850
name: name,
17991851
methods: methods,
18001852
properties: exportedProtocolByName[protocolUniqueKey]?.properties ?? [],
1801-
namespace: effectiveNamespace
1853+
namespace: effectiveNamespace,
1854+
documentation: extractDocumentation(from: node)
18021855
)
18031856

18041857
exportedProtocolByName[protocolUniqueKey] = exportedProtocol
@@ -1874,7 +1927,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18741927
isReadonly: true,
18751928
isStatic: false,
18761929
namespace: effectiveNamespace,
1877-
staticContext: nil
1930+
staticContext: nil,
1931+
documentation: extractDocumentation(from: varDecl)
18781932
)
18791933
properties.append(property)
18801934
}
@@ -1888,7 +1942,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18881942
explicitAccessControl: explicitAccessControl,
18891943
properties: properties,
18901944
methods: [],
1891-
namespace: effectiveNamespace
1945+
namespace: effectiveNamespace,
1946+
documentation: extractDocumentation(from: node)
18921947
)
18931948

18941949
exportedStructByName[structUniqueKey] = exportedStruct
@@ -1981,7 +2036,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
19812036
returnType: returnType,
19822037
effects: effects,
19832038
namespace: namespace,
1984-
staticContext: nil
2039+
staticContext: nil,
2040+
documentation: extractDocumentation(from: node)
19852041
)
19862042
}
19872043

@@ -2022,7 +2078,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
20222078
let exportedProperty = ExportedProtocolProperty(
20232079
name: propertyName,
20242080
type: propertyType,
2025-
isReadonly: isReadonly
2081+
isReadonly: isReadonly,
2082+
documentation: extractDocumentation(from: node)
20262083
)
20272084

20282085
if var currentProtocol = exportedProtocolByName[protocolKey] {
@@ -2033,7 +2090,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
20332090
name: currentProtocol.name,
20342091
methods: currentProtocol.methods,
20352092
properties: properties,
2036-
namespace: currentProtocol.namespace
2093+
namespace: currentProtocol.namespace,
2094+
documentation: currentProtocol.documentation
20372095
)
20382096
exportedProtocolByName[protocolKey] = currentProtocol
20392097
}

0 commit comments

Comments
 (0)