Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,11 @@ extension FFMSwift2JavaGenerator {
paramDecls.append("AllocatingSwiftArena swiftArena$")
}

// TODO: we could copy the Swift method's documentation over here, that'd be great UX
printDeclDocumentation(&printer, decl)
TranslatedDocumentation.printDocumentation(
importedFunc: decl,
translatedDecl: translated,
in: &printer
)
printer.printBraceBlock(
"""
\(annotationsStr)\(modifiers) \(returnTy) \(methodName)(\(paramDecls.joined(separator: ", ")))
Expand All @@ -386,19 +389,6 @@ extension FFMSwift2JavaGenerator {
}
}

private func printDeclDocumentation(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
printer.print(
"""
/**
* Downcall to Swift:
* {@snippet lang=swift :
* \(decl.signatureString)
* }
*/
"""
)
}

/// Print the actual downcall to the Swift API.
///
/// This assumes that all the parameters are passed-in with appropriate names.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,11 @@ extension JNISwift2JavaGenerator {

if shouldGenerateGlobalArenaVariation {
if let importedFunc {
printDeclDocumentation(&printer, importedFunc)
TranslatedDocumentation.printDocumentation(
importedFunc: importedFunc,
translatedDecl: translatedDecl,
in: &printer
)
}
var modifiers = modifiers

Expand All @@ -564,7 +568,11 @@ extension JNISwift2JavaGenerator {
parameters.append("SwiftArena swiftArena$")
}
if let importedFunc {
printDeclDocumentation(&printer, importedFunc)
TranslatedDocumentation.printDocumentation(
importedFunc: importedFunc,
translatedDecl: translatedDecl,
in: &printer
)
}
let signature = "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)"
if skipMethodBody {
Expand Down Expand Up @@ -633,19 +641,6 @@ extension JNISwift2JavaGenerator {
}
}

private func printDeclDocumentation(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
printer.print(
"""
/**
* Downcall to Swift:
* {@snippet lang=swift :
* \(decl.signatureString)
* }
*/
"""
)
}

private func printTypeMetadataAddressFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) {
printer.print("private static native long $typeMetadataAddressDowncall();")

Expand Down
175 changes: 175 additions & 0 deletions Sources/JExtractSwiftLib/SwiftDocumentationParsing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
import SwiftSyntax

struct SwiftDocumentation: Equatable {
struct Parameter: Equatable {
var name: String
var description: String
}

var summary: String?
var discussion: String?
var parameters: [Parameter] = []
var returns: String?
}

enum SwiftDocumentationParser {
private enum State {
case summary
case discussion
case parameter(Int)
case returns
}

// TODO: Replace with Regex
// Capture Groups: 1=Tag, 2=Arg(Optional), 3=Description
private static let tagRegex = try! NSRegularExpression(pattern: "^-\\s*(\\w+)(?:\\s+([^:]+))?\\s*:\\s*(.*)$")

static func parse(_ syntax: some SyntaxProtocol) -> SwiftDocumentation? {
// We must have at least one docline and newline, for this to be valid
guard syntax.leadingTrivia.count >= 2 else { return nil }

var comments = [String]()
var pieces = syntax.leadingTrivia.pieces

// We always expect a newline follows a docline comment
while case .newlines(1) = pieces.popLast(), case .docLineComment(let text) = pieces.popLast() {
comments.append(text)
}

guard !comments.isEmpty else { return nil }

return parse(comments.reversed())
}

private static func parse(_ doclines: [String]) -> SwiftDocumentation? {
var doc = SwiftDocumentation()
var state: State = .summary

let lines = doclines.map { line -> String in
let trimmed = line.trimmingCharacters(in: .whitespaces)
return trimmed.hasPrefix("///") ? String(trimmed.dropFirst(3)).trimmingCharacters(in: .whitespaces) : trimmed
}

// If no lines or all empty, we don't have any documentation.
if lines.isEmpty || lines.allSatisfy(\.isEmpty) {
return nil
}

for line in lines {
if line.starts(with: "-"), let (tag, arg, content) = Self.parseTagHeader(line) {
switch tag.lowercased() {
case "parameter":
guard let arg else { continue }
doc.parameters.append(
SwiftDocumentation.Parameter(
name: arg,
description: content
)
)
state = .parameter(doc.parameters.count > 0 ? doc.parameters.count : 0)

case "parameters":
state = .parameter(0)

case "returns":
doc.returns = content
state = .returns

default:
// Parameter names are marked like
// - myString: description
if case .parameter = state {
state = .parameter(doc.parameters.count > 0 ? doc.parameters.count : 0)

doc.parameters.append(
SwiftDocumentation.Parameter(
name: tag,
description: content
)
)
} else {
state = .discussion
append(&doc.discussion, line)
}
}
} else if line.isEmpty {
// Any blank lines will move us to discussion
state = .discussion
if let discussion = doc.discussion, !discussion.isEmpty {
if !discussion.hasSuffix("\n") {
doc.discussion?.append("\n")
}
}
} else {
appendLineToState(state, line: line, doc: &doc)
}
}

// Remove any trailing newlines in discussion
while doc.discussion?.last == "\n" {
doc.discussion?.removeLast()
}

return doc
}

private static func appendLineToState(_ state: State, line: String, doc: inout SwiftDocumentation) {
switch state {
case .summary: append(&doc.summary, line)
case .discussion: append(&doc.discussion, line)
case .returns: append(&doc.returns, line)
case .parameter(let index):
if index < doc.parameters.count {
append(&doc.parameters[index].description, line)
}
}
}

private static func append(_ existing: inout String, _ new: String) {
existing += "\n" + new
}

private static func append(_ existing: inout String?, _ new: String) {
if existing == nil { existing = new }
else {
existing! += "\n" + new
}
}

private static func parseTagHeader(_ line: String) -> (type: String, arg: String?, description: String)? {
let range = NSRange(location: 0, length: line.utf16.count)
guard let match = Self.tagRegex.firstMatch(in: line, options: [], range: range) else { return nil }

// Group 1: Tag Name
guard let typeRange = Range(match.range(at: 1), in: line) else { return nil }
let type = String(line[typeRange])

// Group 2: Argument (Optional)
var arg: String? = nil
let argRangeNs = match.range(at: 2)
if argRangeNs.location != NSNotFound, let argRange = Range(argRangeNs, in: line) {
arg = String(line[argRange])
}

// Group 3: Description (Always present, potentially empty)
guard let descRange = Range(match.range(at: 3), in: line) else { return nil }
let description = String(line[descRange])

return (type, arg, description)
}
}
109 changes: 109 additions & 0 deletions Sources/JExtractSwiftLib/TranslatedDocumentation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

enum TranslatedDocumentation {
static func printDocumentation(
importedFunc: ImportedFunc,
translatedDecl: FFMSwift2JavaGenerator.TranslatedFunctionDecl,
in printer: inout CodePrinter
) {
var documentation = SwiftDocumentationParser.parse(importedFunc.swiftDecl)

if translatedDecl.translatedSignature.requiresSwiftArena {
documentation?.parameters.append(
SwiftDocumentation.Parameter(
name: "swiftArena$",
description: "the arena that will manage the lifetime and allocation of Swift objects"
)
)
}

printDocumentation(documentation, syntax: importedFunc.swiftDecl, in: &printer)
}

static func printDocumentation(
importedFunc: ImportedFunc,
translatedDecl: JNISwift2JavaGenerator.TranslatedFunctionDecl,
in printer: inout CodePrinter
) {
var documentation = SwiftDocumentationParser.parse(importedFunc.swiftDecl)

if translatedDecl.translatedFunctionSignature.requiresSwiftArena {
documentation?.parameters.append(
SwiftDocumentation.Parameter(
name: "swiftArena$",
description: "the arena that the the returned object will be attached to"
)
)
}

printDocumentation(documentation, syntax: importedFunc.swiftDecl, in: &printer)
}

private static func printDocumentation(
_ parsedDocumentation: SwiftDocumentation?,
syntax: some DeclSyntaxProtocol,
in printer: inout CodePrinter
) {
var groups = [String]()
if let summary = parsedDocumentation?.summary {
groups.append("\(summary)")
}

if let discussion = parsedDocumentation?.discussion {
let paragraphs = discussion.split(separator: "\n\n")
for paragraph in paragraphs {
groups.append("<p>\(paragraph)")
}
}

groups.append(
"""
\(parsedDocumentation != nil ? "<p>" : "")Downcall to Swift:
{@snippet lang=swift :
\(syntax.signatureString)
}
"""
)

var annotationsGroup = [String]()

for param in parsedDocumentation?.parameters ?? [] {
annotationsGroup.append("@param \(param.name) \(param.description)")
}

if let returns = parsedDocumentation?.returns {
annotationsGroup.append("@return \(returns)")
}

if !annotationsGroup.isEmpty {
groups.append(annotationsGroup.joined(separator: "\n"))
}

printer.print("/**")
let oldIdentationText = printer.indentationText
printer.indentationText += " * "
for (idx, group) in groups.enumerated() {
printer.print(group)
if idx < groups.count - 1 {
printer.print("")
}
}
printer.indentationText = oldIdentationText
printer.print(" */")

}
}
4 changes: 3 additions & 1 deletion Tests/JExtractSwiftTests/MethodImportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ final class MethodImportTests {
expected:
"""
/**
* Downcall to Swift:
* Hello World!
*
* <p>Downcall to Swift:
* {@snippet lang=swift :
* public func helloWorld()
* }
Expand Down
Loading
Loading