diff --git a/hilt-android/main/java/dagger/hilt/android/internal/work/HiltWorkerMap.java b/hilt-android/main/java/dagger/hilt/android/internal/work/HiltWorkerMap.java new file mode 100644 index 00000000000..35de388c3fc --- /dev/null +++ b/hilt-android/main/java/dagger/hilt/android/internal/work/HiltWorkerMap.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Dagger Authors. + * + * 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 dagger.hilt.android.internal.work; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** + * Internal qualifier for the multibinding map of @HiltWorker workers. + */ +@Qualifier +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.PARAMETER}) +public @interface HiltWorkerMap { + + /** Internal qualifier for the multibinding set of class names annotated with @HiltWorker. */ + @Qualifier + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.METHOD, ElementType.PARAMETER}) + @interface KeySet {} +} diff --git a/hilt-android/main/java/dagger/hilt/android/work/HiltWorker.java b/hilt-android/main/java/dagger/hilt/android/work/HiltWorker.java new file mode 100644 index 00000000000..ca5ca0e0d3a --- /dev/null +++ b/hilt-android/main/java/dagger/hilt/android/work/HiltWorker.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Dagger Authors. + * + * 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 dagger.hilt.android.work; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotates a class that is a WorkManager {@link + * androidx.work.ListenableWorker} to enable injection with Hilt. + * + *
Hilt currently supports the following types of workers: + * + *
The annotated class must have a single constructor annotated with + * {@code @Inject} or {@code @AssistedInject}. + * + *
Example usage with a Worker: + *
+ * @HiltWorker
+ * public class MyWorker extends Worker {
+ * @Inject
+ * public MyWorker(
+ * @Assisted Context context,
+ * @Assisted WorkerParameters workerParams,
+ * MyDependency myDependency
+ * ) {
+ * super(context, workerParams);
+ * }
+ *
+ * @Override
+ * public Result doWork() { ... }
+ * }
+ *
+ */
+@Documented
+@Retention(CLASS)
+@Target(TYPE)
+public @interface HiltWorker {}
diff --git a/hilt-compiler/main/java/dagger/hilt/android/processor/internal/AndroidClassNames.java b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/AndroidClassNames.java
index 1236544eff8..e5a572c56a9 100644
--- a/hilt-compiler/main/java/dagger/hilt/android/processor/internal/AndroidClassNames.java
+++ b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/AndroidClassNames.java
@@ -137,5 +137,14 @@ public final class AndroidClassNames {
public static final ClassName INJECT_VIA_ON_CONTEXT_AVAILABLE_LISTENER =
get("dagger.hilt.android", "InjectViaOnContextAvailableListener");
+ // WorkManager-related class names
+ public static final ClassName LISTENABLE_WORKER = get("androidx.work", "ListenableWorker");
+ public static final ClassName WORKER_PARAMETERS = get("androidx.work", "WorkerParameters");
+ public static final ClassName HILT_WORKER = get("dagger.hilt.android.work", "HiltWorker");
+ public static final ClassName HILT_WORKER_MAP_QUALIFIER =
+ get("dagger.hilt.android.internal.work", "HiltWorkerMap");
+ public static final ClassName HILT_WORKER_KEYS_QUALIFIER =
+ get("dagger.hilt.android.internal.work", "HiltWorkerMap", "KeySet");
+
private AndroidClassNames() {}
}
diff --git a/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerMetadata.kt b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerMetadata.kt
new file mode 100644
index 00000000000..b4845817b7d
--- /dev/null
+++ b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerMetadata.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Dagger Authors.
+ *
+ * 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 dagger.hilt.android.processor.internal.worker
+
+import androidx.room3.compiler.codegen.toJavaPoet
+import androidx.room3.compiler.processing.ExperimentalProcessingApi
+import androidx.room3.compiler.processing.XProcessingEnv
+import androidx.room3.compiler.processing.XTypeElement
+import com.squareup.javapoet.ClassName
+import dagger.hilt.android.processor.internal.AndroidClassNames
+import dagger.hilt.processor.internal.ClassNames
+import dagger.hilt.processor.internal.LazyString
+import dagger.hilt.processor.internal.ProcessorErrors
+import dagger.hilt.processor.internal.Processors
+import dagger.internal.codegen.xprocessing.XAnnotations
+import dagger.internal.codegen.xprocessing.XElements
+import dagger.internal.codegen.xprocessing.XTypes
+
+@OptIn(
+ ExperimentalProcessingApi::class,
+ com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview::class
+)
+internal class HiltWorkerMetadata
+private constructor(
+ val workerElement: XTypeElement,
+ val isAssistedInject: Boolean,
+) {
+ val className = workerElement.asClassName().toJavaPoet()
+
+ val assistedFactoryClassName: ClassName =
+ ClassName.get(workerElement.packageName, "${className.simpleNames().joinToString("_")}_AssistedFactory")
+
+ val modulesClassName =
+ ClassName.get(
+ workerElement.packageName,
+ "${className.simpleNames().joinToString("_")}_HiltModules",
+ )
+
+ companion object {
+ internal fun create(
+ processingEnv: XProcessingEnv,
+ workerElement: XTypeElement,
+ ): HiltWorkerMetadata? {
+ ProcessorErrors.checkState(
+ XTypes.isSubtype(
+ workerElement.type,
+ processingEnv.requireType(AndroidClassNames.LISTENABLE_WORKER),
+ ),
+ workerElement,
+ "@HiltWorker is only supported on types that subclass %s.",
+ AndroidClassNames.LISTENABLE_WORKER,
+ )
+
+ val injectConstructors =
+ workerElement.getConstructors().filter { constructor ->
+ Processors.isAnnotatedWithInject(constructor) ||
+ constructor.hasAnnotation(ClassNames.ASSISTED_INJECT)
+ }
+
+ ProcessorErrors.checkState(
+ injectConstructors.size == 1,
+ workerElement,
+ "@HiltWorker annotated class should contain exactly one @Inject or @AssistedInject annotated constructor.",
+ )
+
+ val injectConstructor = injectConstructors.single()
+
+ ProcessorErrors.checkState(
+ !injectConstructor.isPrivate(),
+ injectConstructor,
+ "%s annotated constructors must not be private.",
+ if (injectConstructor.hasAnnotation(ClassNames.ASSISTED_INJECT)) {
+ "@Inject or @AssistedInject"
+ } else {
+ "@Inject"
+ },
+ )
+
+ ProcessorErrors.checkState(
+ !workerElement.isNested() || workerElement.isStatic(),
+ workerElement,
+ "@HiltWorker may only be used on inner classes if they are static.",
+ )
+
+ Processors.getScopeAnnotations(workerElement).let { scopeAnnotations ->
+ ProcessorErrors.checkState(
+ scopeAnnotations.isEmpty(),
+ workerElement,
+ "@HiltWorker classes should not be scoped. Found: %s",
+ LazyString.of { scopeAnnotations.joinToString { XAnnotations.toStableString(it) } },
+ )
+ }
+
+ val isAssistedInject = injectConstructor.hasAnnotation(ClassNames.ASSISTED_INJECT)
+
+ return HiltWorkerMetadata(workerElement, isAssistedInject)
+ }
+ }
+}
diff --git a/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerModuleGenerator.kt b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerModuleGenerator.kt
new file mode 100644
index 00000000000..05fc8d71abe
--- /dev/null
+++ b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerModuleGenerator.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2024 The Dagger Authors.
+ *
+ * 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 dagger.hilt.android.processor.internal.worker
+
+import androidx.room3.compiler.processing.ExperimentalProcessingApi
+import androidx.room3.compiler.processing.XProcessingEnv
+import androidx.room3.compiler.processing.addOriginatingElement
+import com.squareup.javapoet.AnnotationSpec
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import dagger.hilt.android.processor.internal.AndroidClassNames
+import dagger.hilt.processor.internal.ClassNames
+import dagger.hilt.processor.internal.Processors
+import javax.lang.model.element.Modifier
+
+@OptIn(
+ ExperimentalProcessingApi::class,
+ com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview::class
+)
+internal class HiltWorkerModuleGenerator(
+ private val processingEnv: XProcessingEnv,
+ private val workerMetadata: HiltWorkerMetadata,
+) {
+ fun generate() {
+ generateModules()
+
+ if (workerMetadata.isAssistedInject) {
+ generateAssistedFactory()
+ }
+ }
+
+ private fun generateModules() {
+ val modulesTypeSpec =
+ TypeSpec.classBuilder(workerMetadata.modulesClassName)
+ .apply {
+ addOriginatingElement(workerMetadata.workerElement)
+ Processors.addGeneratedAnnotation(this, processingEnv, HiltWorkerProcessor::class.java)
+ addAnnotation(
+ AnnotationSpec.builder(ClassNames.ORIGINATING_ELEMENT)
+ .addMember(
+ "topLevelClass",
+ "$T.class",
+ workerMetadata.className.topLevelClassName(),
+ )
+ .build()
+ )
+ addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ addType(getBindsModuleTypeSpec())
+ addType(getKeyModuleTypeSpec())
+ addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build())
+ }
+ .build()
+
+ processingEnv.filer.write(
+ JavaFile.builder(workerMetadata.modulesClassName.packageName(), modulesTypeSpec).build()
+ )
+ }
+
+ private fun getBindsModuleTypeSpec() =
+ createModuleTypeSpec("BindsModule")
+ .addModifiers(Modifier.ABSTRACT)
+ .addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build())
+ .addMethod(
+ if (workerMetadata.isAssistedInject) getAssistedWorkerBindsMethod()
+ else getWorkerBindsMethod()
+ )
+ .build()
+
+ private fun getWorkerBindsMethod() =
+ MethodSpec.methodBuilder("binds")
+ .addAnnotation(ClassNames.BINDS)
+ .addAnnotation(ClassNames.INTO_MAP)
+ .addAnnotation(
+ AnnotationSpec.builder(ClassNames.LAZY_CLASS_KEY)
+ .addMember("value", "$T.class", workerMetadata.className)
+ .build()
+ )
+ .addAnnotation(AndroidClassNames.HILT_WORKER_MAP_QUALIFIER)
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .returns(AndroidClassNames.LISTENABLE_WORKER)
+ .addParameter(workerMetadata.className, "worker")
+ .build()
+
+ private fun getAssistedWorkerBindsMethod() =
+ MethodSpec.methodBuilder("bind")
+ .addAnnotation(ClassNames.BINDS)
+ .addAnnotation(ClassNames.INTO_MAP)
+ .addAnnotation(
+ AnnotationSpec.builder(ClassNames.LAZY_CLASS_KEY)
+ .addMember("value", "$T.class", workerMetadata.className)
+ .build()
+ )
+ .addAnnotation(AndroidClassNames.HILT_WORKER_MAP_QUALIFIER)
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addParameter(workerMetadata.assistedFactoryClassName, "factory")
+ .returns(TypeName.OBJECT)
+ .build()
+
+ private fun getKeyModuleTypeSpec() =
+ createModuleTypeSpec("KeyModule")
+ .addModifiers(Modifier.FINAL)
+ .addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build())
+ .addMethod(getWorkerKeyProvidesMethod())
+ .build()
+
+ private fun getWorkerKeyProvidesMethod() =
+ MethodSpec.methodBuilder("provide")
+ .addAnnotation(ClassNames.PROVIDES)
+ .addAnnotation(ClassNames.INTO_MAP)
+ .addAnnotation(
+ AnnotationSpec.builder(ClassNames.LAZY_CLASS_KEY)
+ .addMember("value", "$T.class", workerMetadata.className)
+ .build()
+ )
+ .addAnnotation(AndroidClassNames.HILT_WORKER_KEYS_QUALIFIER)
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .returns(Boolean::class.java)
+ .addStatement("return true")
+ .build()
+
+ private fun createModuleTypeSpec(innerClassName: String) =
+ TypeSpec.classBuilder(innerClassName)
+ .addOriginatingElement(workerMetadata.workerElement)
+ .addAnnotation(ClassNames.MODULE)
+ .addAnnotation(
+ AnnotationSpec.builder(ClassNames.INSTALL_IN)
+ .addMember("value", "$T.class", ClassNames.SINGLETON_COMPONENT)
+ .build()
+ )
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+
+ /**
+ * Generates an @AssistedFactory interface for the worker class.
+ *
+ * Should generate:
+ * ```
+ * @AssistedFactory
+ * interface PeriodicReminderWorker_AssistedFactory {
+ * PeriodicReminderWorker create(Context context, WorkerParameters params);
+ * }
+ * ```
+ */
+ private fun generateAssistedFactory() {
+ val factoryTypeSpec =
+ TypeSpec.interfaceBuilder(workerMetadata.assistedFactoryClassName)
+ .apply {
+ addOriginatingElement(workerMetadata.workerElement)
+ Processors.addGeneratedAnnotation(this, processingEnv, HiltWorkerProcessor::class.java)
+ addAnnotation(ClassNames.ASSISTED_FACTORY)
+ addModifiers(Modifier.PUBLIC)
+ addMethod(getCreateMethod())
+ }
+ .build()
+
+ processingEnv.filer.write(
+ JavaFile.builder(
+ workerMetadata.assistedFactoryClassName.packageName(), factoryTypeSpec
+ ).build()
+ )
+ }
+
+ private fun getCreateMethod() =
+ MethodSpec.methodBuilder("create")
+ .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ .addParameter(AndroidClassNames.CONTEXT, "context")
+ .addParameter(AndroidClassNames.WORKER_PARAMETERS, "workerParameters")
+ .returns(workerMetadata.className)
+ .build()
+}
diff --git a/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerProcessingStep.kt b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerProcessingStep.kt
new file mode 100644
index 00000000000..b50f567cf57
--- /dev/null
+++ b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerProcessingStep.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Dagger Authors.
+ *
+ * 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 dagger.hilt.android.processor.internal.worker
+
+import androidx.room3.compiler.processing.ExperimentalProcessingApi
+import androidx.room3.compiler.processing.XElement
+import androidx.room3.compiler.processing.XProcessingEnv
+import com.google.common.collect.ImmutableSet
+import com.squareup.javapoet.ClassName
+import dagger.hilt.android.processor.internal.AndroidClassNames
+import dagger.hilt.processor.internal.BaseProcessingStep
+import dagger.internal.codegen.xprocessing.XElements
+
+@OptIn(ExperimentalProcessingApi::class)
+class HiltWorkerProcessingStep(env: XProcessingEnv) : BaseProcessingStep(env) {
+
+ override fun annotationClassNames() = ImmutableSet.of(AndroidClassNames.HILT_WORKER)
+
+ override fun processEach(annotation: ClassName, element: XElement) {
+ val typeElement = XElements.asTypeElement(element)
+ HiltWorkerMetadata.create(processingEnv(), typeElement)?.let { workerMetadata ->
+ HiltWorkerModuleGenerator(processingEnv(), workerMetadata).generate()
+ }
+ }
+}
diff --git a/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerProcessor.kt b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerProcessor.kt
new file mode 100644
index 00000000000..84cad03f6fa
--- /dev/null
+++ b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerProcessor.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Dagger Authors.
+ *
+ * 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 dagger.hilt.android.processor.internal.worker
+
+import androidx.room3.compiler.processing.ExperimentalProcessingApi
+import com.google.auto.service.AutoService
+import dagger.hilt.processor.internal.BaseProcessingStep
+import dagger.hilt.processor.internal.JavacBaseProcessingStepProcessor
+import javax.annotation.processing.Processor
+import net.ltgt.gradle.incap.IncrementalAnnotationProcessor
+import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType
+
+@AutoService(Processor::class)
+@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.ISOLATING)
+class HiltWorkerProcessor : JavacBaseProcessingStepProcessor() {
+ @OptIn(ExperimentalProcessingApi::class)
+ override fun processingStep(): BaseProcessingStep = HiltWorkerProcessingStep(xProcessingEnv)
+}
diff --git a/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerValidationPlugin.kt b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerValidationPlugin.kt
new file mode 100644
index 00000000000..24278f94945
--- /dev/null
+++ b/hilt-compiler/main/java/dagger/hilt/android/processor/internal/worker/HiltWorkerValidationPlugin.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Dagger Authors.
+ *
+ * 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.
+ */
+
+@file:OptIn(ExperimentalProcessingApi::class)
+
+package dagger.hilt.android.processor.internal.worker
+
+import androidx.room3.compiler.processing.ExperimentalProcessingApi
+import androidx.room3.compiler.processing.XProcessingEnv
+import androidx.room3.compiler.processing.XProcessingEnv.Companion.create
+import com.google.auto.service.AutoService
+import com.google.common.graph.EndpointPair
+import com.google.common.graph.ImmutableNetwork
+import dagger.hilt.android.processor.internal.AndroidClassNames
+import dagger.hilt.processor.internal.hasAnnotation
+import dagger.spi.model.Binding
+import dagger.spi.model.BindingGraph
+import dagger.spi.model.BindingGraph.Edge
+import dagger.spi.model.BindingGraph.Node
+import dagger.spi.model.BindingGraphPlugin
+import dagger.spi.model.BindingKind
+import dagger.spi.model.DaggerProcessingEnv
+import dagger.spi.model.DiagnosticReporter
+import javax.tools.Diagnostic.Kind
+
+/**
+ * Plugin to validate users do not inject @HiltWorker classes directly.
+ */
+@AutoService(BindingGraphPlugin::class)
+class HiltWorkerValidationPlugin : BindingGraphPlugin {
+
+ private lateinit var env: XProcessingEnv
+ private lateinit var daggerProcessingEnv: DaggerProcessingEnv
+
+ override fun init(processingEnv: DaggerProcessingEnv, options: MutableMap