From 5c6d7ba6aede3284adedde1d5b483c4f982da548 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Wed, 16 Apr 2025 00:13:26 +0900 Subject: [PATCH] [jextract] Misc improvements * Introduce `Swift2JavaVisitor.typeContext` to handle nested types * Unify `shouldImport(log:)` method using `WithModifiersSyntax & WithAttributesSyntax` * Unify `accessControlModifiers` as an extention to `WithModifiersSyntax` * Don't import `JavaKit` declarations. e.g. `@JavaClass` * Don't import non-public variable decls. * Introduce `DeclSyntaxProtocol.qualifiedNameForDebug` and use it for log messages * Introduce `DeclSyntaxProtocol.signatureString` and use it as the snippet in the exported `.java` files. --- .../Convenience/SwiftSyntax+Extensions.swift | 163 ++++++++++++++++-- Sources/JExtractSwift/ImportedDecls.swift | 2 +- .../JExtractSwift/NominalTypeResolution.swift | 2 +- .../JExtractSwift/Swift2JavaTranslator.swift | 2 +- Sources/JExtractSwift/Swift2JavaVisitor.swift | 90 +++++----- .../FunctionDescriptorImportTests.swift | 2 +- .../MethodImportTests.swift | 4 +- 7 files changed, 200 insertions(+), 65 deletions(-) diff --git a/Sources/JExtractSwift/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwift/Convenience/SwiftSyntax+Extensions.swift index 6b814f8a..a7c12cc9 100644 --- a/Sources/JExtractSwift/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwift/Convenience/SwiftSyntax+Extensions.swift @@ -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 @@ -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 @@ -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)") + } + } +} diff --git a/Sources/JExtractSwift/ImportedDecls.swift b/Sources/JExtractSwift/ImportedDecls.swift index 72204e66..c246dcbc 100644 --- a/Sources/JExtractSwift/ImportedDecls.swift +++ b/Sources/JExtractSwift/ImportedDecls.swift @@ -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 diff --git a/Sources/JExtractSwift/NominalTypeResolution.swift b/Sources/JExtractSwift/NominalTypeResolution.swift index d2421a24..b1185cde 100644 --- a/Sources/JExtractSwift/NominalTypeResolution.swift +++ b/Sources/JExtractSwift/NominalTypeResolution.swift @@ -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 { diff --git a/Sources/JExtractSwift/Swift2JavaTranslator.swift b/Sources/JExtractSwift/Swift2JavaTranslator.swift index 6577a37c..8c0c78c6 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator.swift @@ -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 diff --git a/Sources/JExtractSwift/Swift2JavaVisitor.swift b/Sources/JExtractSwift/Swift2JavaVisitor.swift index 5b577589..96aab517 100644 --- a/Sources/JExtractSwift/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwift/Swift2JavaVisitor.swift @@ -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)] = [] + + /// 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 } @@ -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)") } } @@ -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)") } } @@ -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 } @@ -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 { @@ -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 } @@ -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 @@ -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 @@ -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 diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index 734d9eab..38d43432 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -49,7 +49,7 @@ final class FunctionDescriptorTests { // #MySwiftClass.counter!getter: (MySwiftClass) -> () -> Int32 : @$s14MySwiftLibrary0aB5ClassC7counters5Int32Vvg\t// MySwiftClass.counter.getter // #MySwiftClass.counter!setter: (MySwiftClass) -> (Int32) -> () : @$s14MySwiftLibrary0aB5ClassC7counters5Int32Vvs\t// MySwiftClass.counter.setter // #MySwiftClass.counter!modify: (MySwiftClass) -> () -> () : @$s14MySwiftLibrary0aB5ClassC7counters5Int32VvM\t// MySwiftClass.counter.modify - var counter: Int32 + public var counter: Int32 } """ diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 39d72dc0..38cfd8f2 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -483,7 +483,7 @@ final class MethodImportTests { * Create an instance of {@code MySwiftStruct}. * * {@snippet lang=swift : - * public init(len: Swift.Int, cap: Swift.Int) {} + * public init(len: Swift.Int, cap: Swift.Int) * } */ public MySwiftStruct(long len, long cap) { @@ -494,7 +494,7 @@ final class MethodImportTests { * This instance is managed by the passed in {@link SwiftArena} and may not outlive the arena's lifetime. * * {@snippet lang=swift : - * public init(len: Swift.Int, cap: Swift.Int) {} + * public init(len: Swift.Int, cap: Swift.Int) * } */