From da0a4c4e35ae4074c18235c789effc77e7fc5bc8 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Tue, 3 Nov 2026 14:26:18 +0100 Subject: [PATCH 1/2] JNI: fix Int closures and inout thunks; infer unannotated variable types - `(Int) -> Int` closures: the parameter translation chose the closure's own interface (by Swift type), but the helper printer matched `LongUnaryOperator` by Java type and skipped printing it, so the Java didn't compile; the upcall likewise looked up `applyAsLong` by JNI signature on an object that only has `apply`. Both now use the Swift-type lookup, and the method name travels with the `closureLowering` conversion. - `inout` parameters: the thunk passed a converted temporary, which doesn't compile. They have no Java mapping, so such functions are skipped with `inoutParametersNotSupported` (and reported). - Unannotated variables: infer the type from literals (`Int`, `Double`, `String`, `Bool`) and from `T(...)` / `A.B(...)` initializer calls whose callee resolves to a nominal type, instead of dropping them with `missingTypeAnnotation`. Other initializers still need an annotation. Co-Authored-By: Claude Opus 5.5 --- .../Sources/MySwiftLibrary/Closures.swift | 10 + .../java/com/example/swift/ClosuresTest.java | 10 + ...t2JavaGenerator+JavaBindingsPrinting.swift | 8 +- ...ISwift2JavaGenerator+JavaTranslation.swift | 8 + ...wift2JavaGenerator+NativeTranslation.swift | 49 +++-- .../SwiftTypes/SwiftFunctionSignature.swift | 57 +++++- .../JNI/JNICodegenFixesTests.swift | 193 ++++++++++++++++++ .../JNI/JNISkippedDeclarationsTests.swift | 6 +- 8 files changed, 323 insertions(+), 18 deletions(-) create mode 100644 Tests/JExtractSwiftTests/JNI/JNICodegenFixesTests.swift diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift index 3013a0ce2..bb403ffce 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift @@ -117,3 +117,13 @@ public func closureMultipleArguments( closure(input1, input2) } // snippet.end + +/// Platform-width `Int` closures get their own functional interface. +public func applyIntClosure(_ f: (Int) -> Int, to x: Int) -> Int { + f(x) +} + +public enum ClosureConstants { + /// No type annotation: inferred as `Int` from the literal. + public static let answer = 42 +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java index 89c40a66a..32b9297b2 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java @@ -176,4 +176,14 @@ void globalCallMeLongToDoubleFunction() { double result = MySwiftLibrary.globalCallMeLongToDoubleFunction((long a) -> { return (double) a; }); assertEquals(1.0, result); } + + @Test + void intClosure() { + assertEquals(10, MySwiftLibrary.applyIntClosure(x -> x * 2, 5)); + } + + @Test + void unannotatedStaticLet() { + assertEquals(42, ClosureConstants.getAnswer()); + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index cac874099..7015e4a3e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -840,10 +840,12 @@ extension JNISwift2JavaGenerator { return } - // If contains one function type and it's known java functional interface type, we don't need to print the whole class + // If contains one function type and it's known java functional interface type, we don't need to print the whole class. + // Match on the Swift type, as the parameter translation does: `(Int) -> Int` has the Java shape of + // `LongUnaryOperator` but is translated to its own interface, which must then be printed. if translated.functionTypes.count == 1, let ty = translated.functionTypes.first, - KnownJavaFunctionalInterface.find(ty) != nil + KnownJavaFunctionalInterface.find(ty.swiftType) != nil { return } @@ -854,7 +856,7 @@ extension JNISwift2JavaGenerator { """ ) { printer in for functionType in translated.functionTypes { - if KnownJavaFunctionalInterface.find(functionType) != nil { + if KnownJavaFunctionalInterface.find(functionType.swiftType) != nil { continue } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 713e809ae..5dbf8c8b4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -198,6 +198,11 @@ extension JNISwift2JavaGenerator { } func translate(_ decl: ExtractedFunc) throws -> TranslatedFunctionDecl { + // Java has no `inout`; the thunk would pass a converted temporary, which doesn't compile. + if decl.functionSignature.parameters.contains(where: { $0.convention == .inout }) { + throw JavaTranslationError.inoutParametersNotSupported + } + let nativeTranslation = NativeJavaTranslation( config: self.config, javaPackage: self.javaPackage, @@ -2263,6 +2268,9 @@ extension JNISwift2JavaGenerator { /// a java class. case wrappedJavaClassTranslationNotProvided(SwiftType) + /// `inout` parameters have no Java equivalent yet. + case inoutParametersNotSupported + // FIXME: Remove once we support protocol variables case protocolVariablesNotSupported diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index f572570fb..e40f409de 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -430,8 +430,9 @@ extension JNISwift2JavaGenerator { } let result = try translateClosureResult(functionType.resultType) + let knownInterface = KnownJavaFunctionalInterface.find(functionType) let interfaceJavaType = - if let known = KnownJavaFunctionalInterface.find(functionType) { + if let known = knownInterface { known.javaType } else { JavaType.class( @@ -449,7 +450,10 @@ extension JNISwift2JavaGenerator { ], conversion: .closureLowering( parameters: parameters, - result: result + result: result, + // The method to call is decided with the interface, from the Swift type: the JNI + // signature alone can't tell `(Int) -> Int`'s own interface from `LongUnaryOperator`. + methodName: knownInterface?.method ?? "apply" ), indirectConversion: nil, conversionCheck: nil @@ -733,6 +737,16 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(type) } + // Platform-width integers are Java `long`s, but `Int.JNIType` is `jint` on 32-bit + // platforms: read the `long` as a 64-bit value, then narrow. + if let wideType = platformIntegerWideType(knownType) { + return NativeResult( + javaType: javaType, + conversion: .labelessInitializer(.initFromJNI(.placeholder, swiftType: wideType), swiftType: type), + outParameters: [] + ) + } + // Only support primitives for now. return NativeResult( javaType: javaType, @@ -756,6 +770,15 @@ extension JNISwift2JavaGenerator { } } + /// `Int64`/`UInt64` for `Int`/`UInt`, whose JNI type is only 64-bit on 64-bit platforms. + private func platformIntegerWideType(_ knownType: SwiftKnownTypeDeclKind) -> SwiftType? { + switch knownType { + case .int: knownTypes.int64 + case .uint: knownTypes.uint64 + default: nil + } + } + func translateClosureParameter( _ type: SwiftType, parameterName: String @@ -769,12 +792,21 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(type) } + // Platform-width integers widen to 64 bits, so the `jvalue` holds the Java `long` the + // functional interface expects, also on 32-bit platforms. + let value: NativeSwiftConversionStep = + if let wideType = platformIntegerWideType(knownType) { + .labelessInitializer(.placeholder, swiftType: wideType) + } else { + .placeholder + } + // Only support primitives for now. return NativeParameter( parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .getJValue(.placeholder), + conversion: .getJValue(value), indirectConversion: nil, conversionCheck: nil ) @@ -1363,7 +1395,7 @@ extension JNISwift2JavaGenerator { /// of the `Unsafe(Mutable)Pointer` types in Swift. indirect case pointee(NativeSwiftConversionStep) - indirect case closureLowering(parameters: [NativeParameter], result: NativeResult) + indirect case closureLowering(parameters: [NativeParameter], result: NativeResult, methodName: String) /// Escaping closure lowering using the protocol infrastructure. /// This uses UpcallConversionStep for full support of optionals, arrays, custom types, etc. @@ -1663,7 +1695,7 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(inner).pointee" - case .closureLowering(let parameters, let nativeResult): + case .closureLowering(let parameters, let nativeResult, let methodName): var printer = SwiftPrinter() let methodSignature = MethodSignature( @@ -1689,13 +1721,6 @@ extension JNISwift2JavaGenerator { } - let methodName = - if let known = KnownJavaFunctionalInterface.find(methodSignature) { - known.method - } else { - "apply" - } - printer.print( """ let class$ = environment.interface.GetObjectClass(environment, \(placeholder)) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 6f73b99eb..33d9b3c52 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -352,6 +352,53 @@ extension SwiftFunctionSignature { return (parameters, effectSpecifiers, thrownTypedError) } + /// The type of an unannotated variable, when its initializer makes it unambiguous without type + /// checking: a literal (`42`, `-1`, `1.5`, `"text"`, `true`), or an initializer call `T(...)` / + /// `A.B(...)` whose callee resolves to a nominal type. Anything else, such as `T.member` or + /// `makeValue()`, still needs an explicit annotation. + static func inferredType(of expr: ExprSyntax, lookupContext: SwiftTypeLookupContext) -> SwiftType? { + let knownTypes = SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + if expr.is(IntegerLiteralExprSyntax.self) { + return knownTypes.int + } + if let prefix = expr.as(PrefixOperatorExprSyntax.self), prefix.operator.text == "-", + prefix.expression.is(IntegerLiteralExprSyntax.self) + { + return knownTypes.int + } + if expr.is(FloatLiteralExprSyntax.self) { + return knownTypes.double + } + if expr.is(StringLiteralExprSyntax.self) { + return knownTypes.string + } + if expr.is(BooleanLiteralExprSyntax.self) { + return knownTypes.bool + } + if let call = expr.as(FunctionCallExprSyntax.self), + let typeSyntax = typeSyntax(naming: call.calledExpression), + let type = try? SwiftType(typeSyntax, lookupContext: lookupContext), + case .nominal = type, !type.isUnresolvedTypePlaceholder + { + return type + } + return nil + } + + /// `Foo` or `A.B` written as an expression, as a type. + private static func typeSyntax(naming expr: ExprSyntax) -> TypeSyntax? { + if let reference = expr.as(DeclReferenceExprSyntax.self), reference.argumentNames == nil { + return TypeSyntax(IdentifierTypeSyntax(name: reference.baseName.trimmed)) + } + if let member = expr.as(MemberAccessExprSyntax.self), let base = member.base, + member.declName.argumentNames == nil, + let baseType = typeSyntax(naming: base) + { + return TypeSyntax(MemberTypeSyntax(baseType: baseType, name: member.declName.baseName.trimmed)) + } + return nil + } + public init( _ varNode: VariableDeclSyntax, isSet: Bool, @@ -369,10 +416,16 @@ extension SwiftFunctionSignature { throw SwiftFunctionTranslationError.multipleBindings(varNode) } - guard let varTypeNode = binding.typeAnnotation?.type else { + let valueType: SwiftType + if let varTypeNode = binding.typeAnnotation?.type { + valueType = try SwiftType(varTypeNode, lookupContext: lookupContext) + } else if let initializer = binding.initializer?.value, + let inferred = Self.inferredType(of: initializer, lookupContext: lookupContext) + { + valueType = inferred + } else { throw SwiftFunctionTranslationError.missingTypeAnnotation(varNode) } - let valueType = try SwiftType(varTypeNode, lookupContext: lookupContext) var accessorEffects: AccessorEffects? = nil switch binding.accessorBlock?.accessors { diff --git a/Tests/JExtractSwiftTests/JNI/JNICodegenFixesTests.swift b/Tests/JExtractSwiftTests/JNI/JNICodegenFixesTests.swift new file mode 100644 index 000000000..94cadf7a8 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNICodegenFixesTests.swift @@ -0,0 +1,193 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 CodePrinting +import JExtractSwiftLib +import SwiftExtract +import SwiftJavaConfigurationShared +import Testing + +@Suite +struct JNICodegenFixesTests { + // MARK: `(Int) -> Int` closures + + @Test + func intClosure_interfaceIsPrinted() throws { + try assertOutput( + input: "public func apply(_ f: (Int) -> Int, to x: Int) -> Int { f(x) }", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static class apply { + /** Corresponds to the Swift closure parameter of type {@code (Int) -> Int}. */ + @FunctionalInterface + public interface f { + long apply(long _0); + } + } + """, + "public static long apply(com.example.swift.SwiftModule.apply.f f, long x)", + ] + ) + } + + @Test + func intClosure_upcallCallsTheInterfaceMethod() throws { + try assertOutput( + input: "public func apply(_ f: (Int) -> Int, to x: Int) -> Int { f(x) }", + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + #"let methodID$ = environment.interface.GetMethodID(environment, class$, "apply", "(J)J")!"# + ], + notExpectedChunks: [ + "applyAsLong" + ] + ) + } + + @Test + func intClosure_widensForThirtyTwoBitPlatforms() throws { + try assertOutput( + input: "public func apply(_ f: (Int) -> Int, to x: Int) -> Int { f(x) }", + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + "let arguments$: [jvalue] = [Int64(_0).getJValue(in: environment)]", + "return Int(Int64(fromJNI: environment.interface.CallLongMethodA(environment, f, methodID$, arguments$), in: environment))", + ] + ) + } + + @Test + func int64Closure_stillUsesTheKnownInterface() throws { + try assertOutput( + input: "public func apply(_ f: (Int64) -> Int64, to x: Int64) -> Int64 { f(x) }", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public static long apply(java.util.function.LongUnaryOperator f, long x)" + ] + ) + } + + // MARK: `inout` + + @Test + func inoutParameter_isSkippedAndReported() throws { + let generator = try generate("public func bump(_ x: inout Int64) {}") + #expect(generator.skippedDeclarations.count == 1, "\(generator.skippedDeclarations)") + #expect(generator.skippedDeclarations.first?.hasPrefix("bump(_:): inoutParametersNotSupported") == true) + } + + // MARK: Unannotated variables + + @Test + func unannotatedLiterals_areInferred() throws { + try assertOutput( + input: """ + public enum Constants { + public static let answer = 42 + public static let below = -1 + public static let ratio = 1.5 + public static let title = "Hello" + public static let enabled = true + } + """, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public static long getAnswer() {", + "public static long getBelow() {", + "public static double getRatio() {", + "public static java.lang.String getTitle() {", + "public static boolean isEnabled() {", + ] + ) + } + + @Test + func unannotatedInitializerCall_isInferred() throws { + try assertOutput( + input: """ + public struct Item { + public init() {} + public struct Detail { + public init() {} + } + } + public final class Holder { + public var item = Item() + public var detail = Item.Detail() + } + """, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public Item getItem(SwiftArena swiftArena) {", + "public Item.Detail getDetail(SwiftArena swiftArena) {", + ] + ) + } + + @Test + func ambiguousInitializers_stillNeedAnAnnotation() throws { + let sink = SkippedDeclarationCollector() + _ = try generate( + """ + public struct Item { + public static func make() -> Item { Item() } + public static let shared: Item = Item() + } + public func makeItem() -> Item { Item() } + public final class Holder { + public var made = Item.make() + public var fromFunction = makeItem() + public var member = Item.shared + } + """, + sink: sink + ) + let skipped = sink.skipped.map { String($0.prefix { $0 != ":" }) }.sorted() + #expect(skipped == ["Holder.fromFunction", "Holder.made", "Holder.member"], "\(sink.skipped)") + } + + private func generate(_ source: String, sink: SkippedDeclarationCollector? = nil) throws -> JNISwift2JavaGenerator { + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = makeSwiftJavaAnalyzer(config: config, diagnosticsSink: sink) + try translator.analyze(path: "/fake/Fake.swiftinterface", text: source) + let generator = JNISwift2JavaGenerator( + config: config, + translator: translator, + javaPackage: "com.example.swift", + swiftOutputDirectory: "/fake", + javaOutputDirectory: "/fake", + javaClassLookupTable: [:], + moduleJavaPackages: [:] + ) + var javaPrinter = JavaPrinter(mode: .accumulateAll) + try generator.writeExportedJavaSources(&javaPrinter) + var swiftPrinter = SwiftPrinter(mode: .accumulateAll) + try generator.writeSwiftThunkSources(&swiftPrinter) + return generator + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNISkippedDeclarationsTests.swift b/Tests/JExtractSwiftTests/JNI/JNISkippedDeclarationsTests.swift index ee6c94dcb..2ca570200 100644 --- a/Tests/JExtractSwiftTests/JNI/JNISkippedDeclarationsTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNISkippedDeclarationsTests.swift @@ -64,7 +64,11 @@ struct JNISkippedDeclarationsTests { @Test func analysisSkipsAreCollected() throws { let sink = SkippedDeclarationCollector() - _ = try generate("public struct S { public static let untyped = 42 }", sink: sink) + // `S.make()` can't be typed without the type checker, so it still needs an annotation. + _ = try generate( + "public struct S { public static func make() -> S { S() }; public static let untyped = S.make() }", + sink: sink + ) #expect(sink.skipped.count == 1, "\(sink.skipped)") #expect(sink.skipped.first?.hasPrefix("S.untyped") == true) } From 1810a9f9bd7c2efcaaafbe404b9c886b123a9971 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Tue, 3 Nov 2026 15:37:32 +0100 Subject: [PATCH 2/2] Samples: Int closures and unannotated properties, from Kotlin Co-Authored-By: Claude Opus 5.5 --- .../KotlinSampleLibrary/InferredTypes.swift | 35 ++++++++++++++++ .../com/example/kotlin/InferredTypesTest.kt | 40 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/InferredTypes.swift create mode 100644 Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/InferredTypesTest.kt diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/InferredTypes.swift b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/InferredTypes.swift new file mode 100644 index 000000000..126121abc --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/Sources/KotlinSampleLibrary/InferredTypes.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +/// Closures over platform-width `Int` get their own functional interface. +public enum Arithmetic { + public static func apply(_ f: (Int) -> Int, to x: Int) -> Int { + f(x) + } + + public static func combine(_ a: Int, _ b: Int, _ f: (Int, Int) -> Int) -> Int { + f(a, b) + } +} + +/// Properties without a type annotation: the type is inferred from a literal or an initializer call. +public final class Settings { + public static let maximumCount = 42 + public var isEnabled = false + public var title = "Untitled" + public var ratio = 0.5 + public var featured = Item(id: "featured", count: 0) + + public init() {} +} diff --git a/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/InferredTypesTest.kt b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/InferredTypesTest.kt new file mode 100644 index 000000000..4c53cac29 --- /dev/null +++ b/Samples/SwiftJavaExtractJNIKotlinSampleApp/src/test/kotlin/com/example/kotlin/InferredTypesTest.kt @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +package com.example.kotlin + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class InferredTypesTest { + @Test + fun intClosures() { + assertEquals(10L, Arithmetic.apply({ it * 2 }, 5)) + assertEquals(5L, Arithmetic.combine(2, 3) { a, b -> a + b }) + } + + @Test + fun unannotatedProperties() { + val settings = Settings.init() + assertEquals(42L, Settings.getMaximumCount()) + assertFalse(settings.isEnabled) + assertEquals("Untitled", settings.title) + assertEquals(0.5, settings.ratio) + assertEquals("featured", settings.featured.id) + + settings.title = "Renamed" + assertEquals("Renamed", settings.title) + } +}