From f14168c5c77b53b374a519b94813ffb6759e6de7 Mon Sep 17 00:00:00 2001 From: Rubin Yoo Date: Mon, 22 Sep 2025 12:01:19 -0700 Subject: [PATCH 1/3] Prepare for release 0.4.0-alpha08. --- CHANGELOG.md | 3 +++ gradle.properties | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11af69a0..537554f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.4.0-alpha08 +* Add support for skipping field initialization when initializing a scope. + # 0.4.0-alpha06 * Add support for Kotlin 2.1.0 diff --git a/gradle.properties b/gradle.properties index 211fa6d2..3fe674ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # GROUP=com.uber.motif -VERSION_NAME=0.4.0-alpha08-SNAPSHOT +VERSION_NAME=0.4.0-alpha08 POM_DESCRIPTION=Simple DI API for Android / Java. POM_URL=https://github.com/uber/motif/ POM_SCM_URL=https://github.com/uber/motif/ From d281274f56fd08f0bc902f1dcef7a67a2a71861f Mon Sep 17 00:00:00 2001 From: Rubin Yoo Date: Mon, 22 Sep 2025 13:27:32 -0700 Subject: [PATCH 2/3] Prepare next development version. --- gradle.properties | 2 +- gradle/dependencies.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- samples/sample/build.gradle | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index 3fe674ed..4ff3fd4b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # GROUP=com.uber.motif -VERSION_NAME=0.4.0-alpha08 +VERSION_NAME=0.4.0-alpha09-SNAPSHOT POM_DESCRIPTION=Simple DI API for Android / Java. POM_URL=https://github.com/uber/motif/ POM_SCM_URL=https://github.com/uber/motif/ diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 1092923c..8f93028f 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -43,11 +43,11 @@ ext.deps = [ gradlePlugins: [ android: 'com.android.tools.build:gradle:7.4.2', - intellij: 'org.jetbrains.intellij:org.jetbrains.intellij.gradle.plugin:1.15.0', + intellij: 'org.jetbrains.intellij:org.jetbrains.intellij.gradle.plugin:1.17.4', kotlin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}", ksp: "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:${versions.ksp}", dokka: "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}", - mavenPublish: 'com.vanniktech:gradle-maven-publish-plugin:0.27.0', + mavenPublish: 'com.vanniktech:gradle-maven-publish-plugin:0.34.0', spotless: "com.diffplug.spotless:spotless-plugin-gradle:7.0.2", shadow: "com.github.johnrengelman:shadow:8.1.0", ] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1f017e4e..3ae1e2f1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/sample/build.gradle b/samples/sample/build.gradle index 8ef8443d..1607a488 100644 --- a/samples/sample/build.gradle +++ b/samples/sample/build.gradle @@ -12,8 +12,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } defaultConfig { From 221e505b6810a97258008beffd358b6f95bab286 Mon Sep 17 00:00:00 2001 From: Rubin Yoo Date: Wed, 1 Oct 2025 16:25:17 -0700 Subject: [PATCH 3/3] The null field initialization code has a race condition, and this fix ensures correctness by checking the appropriate variable. --- .../motif/compiler/JavaCodeGenerator.kt | 5 +- .../motif/compiler/KotlinCodeGenerator.kt | 3 +- .../GRAPH.txt | 32 +++++++++ .../Scope.kt | 32 +++++++++ .../Test.kt | 71 +++++++++++++++++++ .../GRAPH.txt | 32 +++++++++ .../Scope.java | 35 +++++++++ .../Test.java | 65 +++++++++++++++++ 8 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/GRAPH.txt create mode 100644 tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Scope.kt create mode 100644 tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Test.kt create mode 100644 tests/src/main/java/testcases/T078_use_null_field_concurrency_java/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Scope.java create mode 100644 tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Test.java diff --git a/compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt b/compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt index 739821fe..30c56e05 100644 --- a/compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt +++ b/compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt @@ -194,7 +194,8 @@ object JavaCodeGenerator { .add("Object $localFieldName = \$N;\n", cacheFieldName) .beginControlFlow("if (\$N == null)", localFieldName) .beginControlFlow("synchronized (this)") - .beginControlFlow("if (\$N == null)", cacheFieldName) + .add("\$N = \$N;\n", localFieldName, cacheFieldName) + .beginControlFlow("if (\$N == null)", localFieldName) .add("\$N = \$L;\n", localFieldName, instantiation.spec()) .beginControlFlow("if (\$N == null)", localFieldName) .add( @@ -203,7 +204,7 @@ object JavaCodeGenerator { "Factory method cannot return null", ) .endControlFlow() - .add("\$N = \$L;\n", cacheFieldName, localFieldName) + .add("\$N = \$N;\n", cacheFieldName, localFieldName) .endControlFlow() .endControlFlow() .endControlFlow() diff --git a/compiler/src/main/kotlin/motif/compiler/KotlinCodeGenerator.kt b/compiler/src/main/kotlin/motif/compiler/KotlinCodeGenerator.kt index fe0f205b..4bdc2654 100644 --- a/compiler/src/main/kotlin/motif/compiler/KotlinCodeGenerator.kt +++ b/compiler/src/main/kotlin/motif/compiler/KotlinCodeGenerator.kt @@ -231,7 +231,8 @@ object KotlinCodeGenerator { .addStatement("var $localFieldName = %N;\n", cacheFieldName) .beginControlFlow("if (%N == null)", localFieldName) .beginControlFlow("synchronized (this)") - .beginControlFlow("if (%N == null)", cacheFieldName) + .addStatement("%N = %N", localFieldName, cacheFieldName) + .beginControlFlow("if (%N == null)", localFieldName) .addStatement("%N = %L", localFieldName, instantiation.spec()) .addStatement("%N = %N", cacheFieldName, localFieldName) .endControlFlow() diff --git a/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/GRAPH.txt b/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/GRAPH.txt new file mode 100644 index 00000000..325f6fba --- /dev/null +++ b/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/GRAPH.txt @@ -0,0 +1,32 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ==== Provides ==== + + ---- Object | Objects.fooObject ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooObject() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + diff --git a/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Scope.kt b/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Scope.kt new file mode 100644 index 00000000..9a0ef8f2 --- /dev/null +++ b/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Scope.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018-2019 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT009_use_null_field_concurrency_kotlin + +import motif.Creatable + +@motif.Scope(useNullFieldInitialization = true) +interface Scope : Creatable { + fun fooObject(): Any + + @motif.Objects + abstract class Objects { + fun fooObject(): Any { + return Any() + } + } + + interface Dependencies +} \ No newline at end of file diff --git a/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Test.kt b/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Test.kt new file mode 100644 index 00000000..cf5da319 --- /dev/null +++ b/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Test.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2018-2019 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT009_use_null_field_concurrency_kotlin + +import com.google.common.truth.Truth +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +object Test { + /** + * This tests if the ScopeImpl synchronized blocks check and assign correct values + */ + @JvmStatic + fun run() { + val scope: Scope = ScopeImpl() + val nThreads = 2 + val executorService = Executors.newFixedThreadPool(nThreads) + val getFooObjectLatch = CountDownLatch(nThreads) + val end = CountDownLatch(nThreads) + val isFooObjectNull = AtomicBoolean(true) + val count = AtomicInteger(0) + try { + synchronized(scope) { + // blocks the synchronized in scope.fooObject + for (i in 0 until nThreads) { + executorService.submit { + getFooObjectLatch.countDown() + try { + val fooObject = scope.fooObject() + if (fooObject != null) { + count.incrementAndGet() + isFooObjectNull.set(false) + } + } catch (e: Exception) { + isFooObjectNull.set(true) + } + end.countDown() + } + } + getFooObjectLatch.await(1000, TimeUnit.MILLISECONDS) + } + + // at this point, the two threads will compete to create the fooObject + + // Verify + if (end.await(1000, TimeUnit.MILLISECONDS)) { + Truth.assertThat(isFooObjectNull.get()).isFalse() + } + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + Truth.assertThat(count.get()).isEqualTo(nThreads) + Truth.assertThat(true).isNull() + } +} diff --git a/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/GRAPH.txt b/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/GRAPH.txt new file mode 100644 index 00000000..325f6fba --- /dev/null +++ b/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/GRAPH.txt @@ -0,0 +1,32 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ==== Provides ==== + + ---- Object | Objects.fooObject ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooObject() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + diff --git a/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Scope.java b/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Scope.java new file mode 100644 index 00000000..9e3c109c --- /dev/null +++ b/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Scope.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018-2019 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T078_use_null_field_concurrency_java; + +import motif.Creatable; + +@motif.Scope(useNullFieldInitialization = true) +public interface Scope extends Creatable { + + Object fooObject(); + + @motif.Objects + class Objects { + + Object fooObject() { + return new Object(); + } + + } + + interface Dependencies {} +} \ No newline at end of file diff --git a/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Test.java b/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Test.java new file mode 100644 index 00000000..6085a7ab --- /dev/null +++ b/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Test.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018-2019 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T078_use_null_field_concurrency_java; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class Test { + + /** + * This tests if the ScopeImpl synchronized blocks check and assign correct values + */ + public static void run() { + Scope scope = new ScopeImpl(); + int nThreads = 2; + ExecutorService executorService = Executors.newFixedThreadPool(nThreads); + CountDownLatch getFooObjectLatch = new CountDownLatch(nThreads); + CountDownLatch end = new CountDownLatch(nThreads); + AtomicBoolean isFooObjectNull = new AtomicBoolean(false); + try { + synchronized (scope) { // blocks the synchronized in scope.fooObject + for (int i = 0; i < nThreads; i++) { + executorService.submit(() -> { + getFooObjectLatch.countDown(); + Object fooObject = scope.fooObject(); + if(fooObject == null) { + isFooObjectNull.set(true); + } + end.countDown(); + }); + } + getFooObjectLatch.await(1000, TimeUnit.MILLISECONDS); + } + // at this point, the two threads will compete to create the fooObject + + // Verify + if(end.await(1000, TimeUnit.MILLISECONDS)) { + assertThat(isFooObjectNull.get()).isFalse(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +}