Force open class for classes with same-namespace subclasses
This commit is contained in:
parent
3a033057fb
commit
9dbd2b5a7a
3 changed files with 81 additions and 8 deletions
|
|
@ -43,6 +43,10 @@ public struct AnalysisResult {
|
|||
/// cross-namespace ancestors (those are not real Swift superclasses).
|
||||
public var inheritedMethodNames: [String: Set<String>]
|
||||
|
||||
/// Set of fully-qualified type names (e.g. `"Gtk.Widget"`) that have
|
||||
/// at least one subclass in the same namespace.
|
||||
public var typesWithSubclasses: Set<String> = []
|
||||
|
||||
/// A function pattern rename rule extracted from the config.
|
||||
public struct FunctionPatternEntry {
|
||||
/// The fully-qualified type name this pattern applies to.
|
||||
|
|
@ -79,6 +83,8 @@ public struct AnalysisResult {
|
|||
/// - functionPatterns: Glob-style function rename rules.
|
||||
/// - inheritedMethodNames: Map from each class to the set of method
|
||||
/// names it inherits transitively from its parent class chain.
|
||||
/// - typesWithSubclasses: Set of fully-qualified type names that have
|
||||
/// at least one same-namespace subclass.
|
||||
public init(classHierarchy: [String: String] = [:], generatedTypes: Set<String> = [],
|
||||
manualTypes: Set<String> = [], ignoredTypes: Set<String> = [],
|
||||
classOverrides: [String: ObjectOverrides] = [:],
|
||||
|
|
@ -86,7 +92,8 @@ public struct AnalysisResult {
|
|||
signalOverrides: [String: SignalOverrides] = [:],
|
||||
propertyOverrides: [String: PropertyOverrides] = [:],
|
||||
functionPatterns: [FunctionPatternEntry] = [],
|
||||
inheritedMethodNames: [String: Set<String>] = [:]) {
|
||||
inheritedMethodNames: [String: Set<String>] = [:],
|
||||
typesWithSubclasses: Set<String> = []) {
|
||||
self.classHierarchy = classHierarchy
|
||||
self.generatedTypes = generatedTypes
|
||||
self.manualTypes = manualTypes
|
||||
|
|
@ -97,6 +104,7 @@ public struct AnalysisResult {
|
|||
self.propertyOverrides = propertyOverrides
|
||||
self.functionPatterns = functionPatterns
|
||||
self.inheritedMethodNames = inheritedMethodNames
|
||||
self.typesWithSubclasses = typesWithSubclasses
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +164,21 @@ public struct Analyzer {
|
|||
}
|
||||
}
|
||||
|
||||
// Invert the hierarchy to find parents that have at least one
|
||||
// same-namespace subclass. Only same-namespace children are real
|
||||
// Swift subclasses for the purpose of the "final" rule.
|
||||
var typesWithSubclasses: Set<String> = []
|
||||
for ns in repository.namespaces {
|
||||
for cls in ns.classes {
|
||||
let fullName = "\(ns.name).\(cls.name)"
|
||||
guard let parentFullName = classHierarchy[fullName] else { continue }
|
||||
let parentNs = parentFullName.split(separator: ".").first.map(String.init) ?? ""
|
||||
if parentNs == ns.name {
|
||||
typesWithSubclasses.insert(parentFullName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the inherited-method-name set for each class by walking the
|
||||
// class hierarchy transitively. A subclass inherits the union of all
|
||||
// method names declared by its parent, grandparent, etc.
|
||||
|
|
@ -225,7 +248,8 @@ public struct Analyzer {
|
|||
signalOverrides: signalOverrides,
|
||||
propertyOverrides: propertyOverrides,
|
||||
functionPatterns: functionPatterns,
|
||||
inheritedMethodNames: inheritedMethodNames
|
||||
inheritedMethodNames: inheritedMethodNames,
|
||||
typesWithSubclasses: typesWithSubclasses
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,9 @@ public struct CodeGenerator {
|
|||
swift += Self.formatDocComment(cls.doc)
|
||||
if concurrency == .mainActor { swift += "@MainActor\n" }
|
||||
if let cfg = override?.cfgCondition { swift += "#if \(cfg)\n" }
|
||||
let isFinal = override?.finalType ?? true
|
||||
let userWantsFinal = override?.finalType ?? true
|
||||
let hasSubclasses = analysis.typesWithSubclasses.contains("\(namespace).\(cls.name)")
|
||||
let isFinal = userWantsFinal && !hasSubclasses
|
||||
let classKeyword = isFinal ? "final class" : "open class"
|
||||
let parentClause = cls.parent.map { ": \($0)" } ?? ""
|
||||
swift += "public \(classKeyword) \(cls.name)\(parentClause) {\n"
|
||||
|
|
|
|||
|
|
@ -605,8 +605,55 @@ func testRecordInGenerateFiles() throws {
|
|||
#expect(!output.contains(", canFocus)"))
|
||||
}
|
||||
|
||||
@Test("Optional boolean return is mapped with != 0")
|
||||
func testOptionalBooleanReturnMapping() {
|
||||
let result = CodeGenerator.wrapCReturnValue(callExpression: "g_value_get_boolean(&value)", returnType: .optional(.boolean))
|
||||
#expect(result == "g_value_get_boolean(&value).map { $0 != 0 }")
|
||||
}
|
||||
@Test("Optional boolean return is mapped with != 0")
|
||||
func testOptionalBooleanReturnMapping() {
|
||||
let result = CodeGenerator.wrapCReturnValue(callExpression: "g_value_get_boolean(&value)", returnType: .optional(.boolean))
|
||||
#expect(result == "g_value_get_boolean(&value).map { $0 != 0 }")
|
||||
}
|
||||
|
||||
@Test("Class with subclasses is not final")
|
||||
func testClassWithSubclassesIsNotFinal() throws {
|
||||
let repo = Repository(namespaces: [
|
||||
Namespace(name: "Gtk", version: "4.0", classes: [
|
||||
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.InitiallyUnowned"),
|
||||
Class(name: "Button", cType: "GtkButton", parent: "Widget"),
|
||||
])
|
||||
])
|
||||
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||
targetDirectory: "", externalLibraries: [],
|
||||
generate: ["Gtk.Widget", "Gtk.Button"], manual: [], ignore: [], objects: [])
|
||||
let generator = CodeGenerator(config: config)
|
||||
let files = try generator.generateFiles(repository: repo, analysis: Analyzer(config: config).analyze(repository: repo))
|
||||
let widgetOutput = files["Widget.swift"] ?? ""
|
||||
let buttonOutput = files["Button.swift"] ?? ""
|
||||
|
||||
// Widget has a subclass (Button), so it should NOT be final
|
||||
#expect(widgetOutput.contains("open class Widget"))
|
||||
#expect(!widgetOutput.contains("final class Widget"))
|
||||
|
||||
// Button has no subclasses, so it CAN be final
|
||||
#expect(buttonOutput.contains("final class Button"))
|
||||
}
|
||||
|
||||
@Test("Cross-namespace child does not force open class")
|
||||
func testCrossNamespaceChildDoesNotForceOpen() throws {
|
||||
// Base is in GObject namespace. Derived is in Gtk namespace (cross-namespace).
|
||||
// Base should remain `final class` since the child is in a different namespace.
|
||||
let repo = Repository(namespaces: [
|
||||
Namespace(name: "GObject", version: "2.0", classes: [
|
||||
Class(name: "Base", cType: "GBase", parent: nil),
|
||||
]),
|
||||
Namespace(name: "Gtk", version: "4.0", classes: [
|
||||
Class(name: "Derived", cType: "GtkDerived", parent: "GObject.Base"),
|
||||
]),
|
||||
])
|
||||
let config = GenerationConfig(library: "GObject", version: "2.0", girsDirectories: [],
|
||||
targetDirectory: "", externalLibraries: [],
|
||||
generate: ["GObject.Base"], manual: [], ignore: [], objects: [])
|
||||
let generator = CodeGenerator(config: config)
|
||||
let files = try generator.generateFiles(repository: repo, analysis: Analyzer(config: config).analyze(repository: repo))
|
||||
let baseOutput = files["Base.swift"] ?? ""
|
||||
|
||||
// Base has a child (Derived) but in a different namespace, so it stays final
|
||||
#expect(baseOutput.contains("final class Base"))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue