Skip to content

[jextract] Misc improvements #223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 16, 2025
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
163 changes: 145 additions & 18 deletions Sources/JExtractSwift/Convenience/SwiftSyntax+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,7 @@
import SwiftDiagnostics
import SwiftSyntax

extension DeclGroupSyntax {
internal var accessControlModifiers: DeclModifierListSyntax {
modifiers.filter { modifier in
modifier.isAccessControl
}
}
}

extension FunctionDeclSyntax {
internal var accessControlModifiers: DeclModifierListSyntax {
modifiers.filter { modifier in
modifier.isAccessControl
}
}
}

extension VariableDeclSyntax {
extension WithModifiersSyntax {
internal var accessControlModifiers: DeclModifierListSyntax {
modifiers.filter { modifier in
modifier.isAccessControl
Expand Down Expand Up @@ -89,7 +73,7 @@ extension DeclModifierSyntax {
var isAccessControl: Bool {
switch self.name.tokenKind {
case .keyword(.private), .keyword(.fileprivate), .keyword(.internal), .keyword(.package),
.keyword(.public):
.keyword(.public), .keyword(.open):
return true
default:
return false
Expand All @@ -105,7 +89,150 @@ extension DeclModifierSyntax {
case .keyword(.internal): return false
case .keyword(.package): return false
case .keyword(.public): return true
case .keyword(.open): return true
default: return false
}
}
}

extension WithModifiersSyntax {
var isPublic: Bool {
self.modifiers.contains { modifier in
modifier.isPublic
}
}
}

extension AttributeListSyntax.Element {
/// Whether this node has `JavaKit` attributes.
var isJava: Bool {
guard case let .attribute(attr) = self else {
// FIXME: Handle #if.
return false
}
let attrName = attr.attributeName.description
switch attrName {
case "JavaClass", "JavaInterface", "JavaField", "JavaStaticField", "JavaMethod", "JavaStaticMethod", "JavaImplementation":
return true
default:
return false
}
}
}

extension DeclSyntaxProtocol {
/// Find inner most "decl" node in ancestors.
var ancestorDecl: DeclSyntax? {
var node: Syntax = Syntax(self)
while let parent = node.parent {
if let decl = parent.as(DeclSyntax.self) {
return decl
}
node = parent
}
return nil
}

/// Declaration name primarily for debugging.
var nameForDebug: String {
return switch DeclSyntax(self).as(DeclSyntaxEnum.self) {
case .accessorDecl(let node):
node.accessorSpecifier.text
case .actorDecl(let node):
node.name.text
case .associatedTypeDecl(let node):
node.name.text
case .classDecl(let node):
node.name.text
case .deinitializerDecl(_):
"deinit"
case .editorPlaceholderDecl:
""
case .enumCaseDecl(let node):
// FIXME: Handle multiple elements.
if let element = node.elements.first {
element.name.text
} else {
"case"
}
case .enumDecl(let node):
node.name.text
case .extensionDecl(let node):
node.extendedType.description
case .functionDecl(let node):
node.name.text + "(" + node.signature.parameterClause.parameters.map({ $0.firstName.text + ":" }).joined() + ")"
case .ifConfigDecl(_):
"#if"
case .importDecl(_):
"import"
case .initializerDecl(let node):
"init" + "(" + node.signature.parameterClause.parameters.map({ $0.firstName.text + ":" }).joined() + ")"
case .macroDecl(let node):
node.name.text
case .macroExpansionDecl(let node):
"#" + node.macroName.trimmedDescription
case .missingDecl(_):
"(missing)"
case .operatorDecl(let node):
node.name.text
case .poundSourceLocation(_):
"#sourceLocation"
case .precedenceGroupDecl(let node):
node.name.text
case .protocolDecl(let node):
node.name.text
case .structDecl(let node):
node.name.text
case .subscriptDecl(let node):
"subscript" + "(" + node.parameterClause.parameters.map({ $0.firstName.text + ":" }).joined() + ")"
case .typeAliasDecl(let node):
node.name.text
case .variableDecl(let node):
// FIXME: Handle multiple variables.
if let element = node.bindings.first {
element.pattern.trimmedDescription
} else {
"var"
}
}
}

/// Qualified declaration name primarily for debugging.
var qualifiedNameForDebug: String {
if let parent = ancestorDecl {
parent.qualifiedNameForDebug + "." + nameForDebug
} else {
nameForDebug
}
}

/// Signature part of the declaration. I.e. without body or member block.
var signatureString: String {
return switch DeclSyntax(self.detached).as(DeclSyntaxEnum.self) {
case .functionDecl(let node):
node.with(\.body, nil).trimmedDescription
case .initializerDecl(let node):
node.with(\.body, nil).trimmedDescription
case .classDecl(let node):
node.with(\.memberBlock, "").trimmedDescription
case .structDecl(let node):
node.with(\.memberBlock, "").trimmedDescription
case .protocolDecl(let node):
node.with(\.memberBlock, "").trimmedDescription
case .accessorDecl(let node):
node.with(\.body, nil).trimmedDescription
case .variableDecl(let node):
node
.with(\.bindings, PatternBindingListSyntax(
node.bindings.map {
$0.detached
.with(\.accessorBlock, nil)
.with(\.initializer, nil)
}
))
.trimmedDescription
default:
fatalError("unimplemented \(self.kind)")
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

That’s neat, thanks!

2 changes: 1 addition & 1 deletion Sources/JExtractSwift/ImportedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible {
public var swiftDecl: any DeclSyntaxProtocol

public var syntax: String? {
"\(self.swiftDecl)"
self.swiftDecl.signatureString
}

public var isInit: Bool = false
Expand Down
2 changes: 1 addition & 1 deletion Sources/JExtractSwift/NominalTypeResolution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class NominalTypeResolution {

/// A syntax node for a nominal type declaration.
@_spi(Testing)
public typealias NominalTypeDeclSyntaxNode = any DeclGroupSyntax & NamedDeclSyntax
public typealias NominalTypeDeclSyntaxNode = any DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax

// MARK: Nominal type name resolution.
extension NominalTypeResolution {
Expand Down
2 changes: 1 addition & 1 deletion Sources/JExtractSwift/Swift2JavaTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ extension Swift2JavaTranslator {
/// Try to resolve the given nominal type node into its imported
/// representation.
func importedNominalType(
_ nominal: some DeclGroupSyntax & NamedDeclSyntax
_ nominal: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax
) -> ImportedNominalType? {
if !nominal.shouldImport(log: log) {
return nil
Expand Down
90 changes: 49 additions & 41 deletions Sources/JExtractSwift/Swift2JavaVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ final class Swift2JavaVisitor: SyntaxVisitor {
/// store this along with type names as we import them.
let targetJavaPackage: String

var currentType: ImportedNominalType? = nil
/// Type context stack associated with the syntax.
var typeContext: [(syntaxID: Syntax.ID, type: ImportedNominalType)] = []
Copy link
Member

Choose a reason for hiding this comment

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

Yes, thank you!


/// Innermost type context.
var currentType: ImportedNominalType? { typeContext.last?.type }

/// The current type name as a nested name like A.B.C.
var currentTypeName: String? { self.currentType?.swiftTypeName }
Expand All @@ -41,37 +45,50 @@ final class Swift2JavaVisitor: SyntaxVisitor {
super.init(viewMode: .all)
}

/// Push specified type to the type context associated with the syntax.
func pushTypeContext(syntax: some SyntaxProtocol, importedNominal: ImportedNominalType) {
typeContext.append((syntax.id, importedNominal))
}

/// Pop type context if the current context is associated with the syntax.
func popTypeContext(syntax: some SyntaxProtocol) -> Bool {
if typeContext.last?.syntaxID == syntax.id {
typeContext.removeLast()
return true
} else {
return false
}
}

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
log.debug("Visit \(node.kind): \(node)")
log.debug("Visit \(node.kind): '\(node.qualifiedNameForDebug)'")
guard let importedNominalType = translator.importedNominalType(node) else {
return .skipChildren
}

self.currentType = importedNominalType
self.pushTypeContext(syntax: node, importedNominal: importedNominalType)
return .visitChildren
}

override func visitPost(_ node: ClassDeclSyntax) {
if currentType != nil {
if self.popTypeContext(syntax: node) {
log.debug("Completed import: \(node.kind) \(node.name)")
self.currentType = nil
}
}

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
log.debug("Visit \(node.kind): \(node)")
log.debug("Visit \(node.kind): \(node.qualifiedNameForDebug)")
guard let importedNominalType = translator.importedNominalType(node) else {
return .skipChildren
}

self.currentType = importedNominalType
self.pushTypeContext(syntax: node, importedNominal: importedNominalType)
return .visitChildren
}

override func visitPost(_ node: StructDeclSyntax) {
if currentType != nil {
log.debug("Completed import: \(node.kind) \(node.name)")
self.currentType = nil
if self.popTypeContext(syntax: node) {
log.debug("Completed import: \(node.kind) \(node.qualifiedNameForDebug)")
}
}

Expand All @@ -84,13 +101,13 @@ final class Swift2JavaVisitor: SyntaxVisitor {
return .skipChildren
}

self.currentType = importedNominalType
self.pushTypeContext(syntax: node, importedNominal: importedNominalType)
return .visitChildren
}

override func visitPost(_ node: ExtensionDeclSyntax) {
if currentType != nil {
self.currentType = nil
if self.popTypeContext(syntax: node) {
log.debug("Completed import: \(node.kind) \(node.qualifiedNameForDebug)")
}
}

Expand Down Expand Up @@ -148,6 +165,10 @@ final class Swift2JavaVisitor: SyntaxVisitor {
}

override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
guard node.shouldImport(log: log) else {
return .skipChildren
}

guard let binding = node.bindings.first else {
return .skipChildren
}
Expand All @@ -156,7 +177,7 @@ final class Swift2JavaVisitor: SyntaxVisitor {

// TODO: filter out kinds of variables we cannot import

self.log.debug("Import variable: \(node.kind) \(fullName)")
self.log.debug("Import variable: \(node.kind) '\(node.qualifiedNameForDebug)'")

let returnTy: TypeSyntax
if let typeAnnotation = binding.typeAnnotation {
Expand All @@ -169,7 +190,7 @@ final class Swift2JavaVisitor: SyntaxVisitor {
do {
javaResultType = try cCompatibleType(for: returnTy)
} catch {
self.log.info("Unable to import variable \(node.debugDescription) - \(error)")
log.info("Unable to import variable '\(node.qualifiedNameForDebug)' - \(error)")
return .skipChildren
}

Expand All @@ -190,7 +211,7 @@ final class Swift2JavaVisitor: SyntaxVisitor {
log.debug("Record variable in \(currentTypeName)")
translator.importedTypes[currentTypeName]!.variables.append(varDecl)
} else {
fatalError("Global variables are not supported yet: \(node.debugDescription)")
fatalError("Global variables are not supported yet: \(node.qualifiedNameForDebug)")
}

return .skipChildren
Expand All @@ -206,7 +227,7 @@ final class Swift2JavaVisitor: SyntaxVisitor {
return .skipChildren
}

self.log.debug("Import initializer: \(node.kind) \(currentType.javaType.description)")
self.log.debug("Import initializer: \(node.kind) '\(node.qualifiedNameForDebug)'")
let params: [ImportedParam]
do {
params = try node.signature.parameterClause.parameters.map { param in
Expand Down Expand Up @@ -247,37 +268,24 @@ final class Swift2JavaVisitor: SyntaxVisitor {
}
}

extension DeclGroupSyntax where Self: NamedDeclSyntax {
extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax {
func shouldImport(log: Logger) -> Bool {
guard (accessControlModifiers.first { $0.isPublic }) != nil else {
log.trace("Cannot import \(self.name) because: is not public")
guard accessControlModifiers.contains(where: { $0.isPublic }) else {
log.trace("Skip import '\(self.qualifiedNameForDebug)': not public")
return false
}

return true
}
}

extension InitializerDeclSyntax {
func shouldImport(log: Logger) -> Bool {
let isFailable = self.optionalMark != nil

if isFailable {
log.warning("Skip importing failable initializer: \(self)")
guard !attributes.contains(where: { $0.isJava }) else {
log.trace("Skip import '\(self.qualifiedNameForDebug)': is Java")
return false
}

// Ok, import it
log.warning("Import initializer: \(self)")
return true
}
}
if let node = self.as(InitializerDeclSyntax.self) {
let isFailable = node.optionalMark != nil

extension FunctionDeclSyntax {
func shouldImport(log: Logger) -> Bool {
guard (accessControlModifiers.first { $0.isPublic }) != nil else {
log.trace("Cannot import \(self.name) because: is not public")
return false
if isFailable {
log.warning("Skip import '\(self.qualifiedNameForDebug)': failable initializer")
return false
}
}

return true
Expand Down
Loading