(), simpleName.toString())
34 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | env:
9 | GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false"
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: actions/setup-java@v3
18 | with:
19 | distribution: 'zulu'
20 | java-version: 8
21 |
22 | - run: ./gradlew build
23 |
24 | - run: ./gradlew uploadArchives
25 | env:
26 | ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
27 | ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
28 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }}
29 |
30 | - name: Extract release notes
31 | id: release_notes
32 | uses: ffurrer2/extract-release-notes@v1
33 |
34 | - name: Create release
35 | uses: softprops/action-gh-release@v1
36 | with:
37 | body: ${{ steps.release_notes.outputs.release_notes }}
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 |
41 | - name: Deploy docs to website
42 | uses: JamesIves/github-pages-deploy-action@releases/v3
43 | with:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | BRANCH: site
46 | FOLDER: inflation-inject/build/docs/javadoc
47 | TARGET_FOLDER: 1.x/
48 | CLEAN: true
49 |
--------------------------------------------------------------------------------
/inflation-inject/src/main/java/app/cash/inject/inflation/InflationModule.java:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation;
2 |
3 | import java.lang.annotation.Retention;
4 | import java.lang.annotation.Target;
5 |
6 | import static java.lang.annotation.ElementType.TYPE;
7 | import static java.lang.annotation.RetentionPolicy.CLASS;
8 |
9 | /**
10 | * Mark a Dagger {@code @Module} which will have a peer module generated with bindings for all of
11 | * the {@link InflationInject @InflationInject}-annotated types in this compilation unit.
12 | * The generated module must then be added to this module's {@code includes} array.
13 | *
14 | * For example:
15 | *
16 | * {@literal @}InflationModule
17 | * {@literal @}Module(includes = InflationInject_PresenterModule.class)
18 | * abstract class PresenterModule {}
19 | *
20 | *
21 | * The generated module's bindings look approximately like this:
22 | *
23 | * {@literal @}Binds
24 | * {@literal @}IntoMap
25 | * {@literal @}StringKey("com.example.CustomView")
26 | * abstract ViewFactory bindComExampleCustomView(CustomView_InflationFactory factory)
27 | *
28 | * {@code CustomView_InflationFactory} is also a generated type from annotating one of
29 | * {@code CustomView}'s constructors with {@link InflationInject @InflationInject}.
30 | *
31 | * The result is that a {@code Map} is available in the graph. However, you
32 | * usually want to interact with it by injecting {@link InflationInjectFactory} rather than dealing
33 | * directly with the map.
34 | *
35 | * @see InflationInjectFactory
36 | */
37 | @Target(TYPE)
38 | @Retention(CLASS)
39 | public @interface InflationModule {
40 | }
41 |
--------------------------------------------------------------------------------
/inflation-inject-processor/src/main/java/app/cash/inject/inflation/processor/internal/javaPoet.kt:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation.processor.internal
2 |
3 | import com.squareup.javapoet.AnnotationSpec
4 | import com.squareup.javapoet.ClassName
5 | import com.squareup.javapoet.CodeBlock
6 | import com.squareup.javapoet.ParameterizedTypeName
7 | import com.squareup.javapoet.TypeName
8 | import javax.lang.model.element.AnnotationMirror
9 | import javax.lang.model.element.TypeElement
10 | import javax.lang.model.type.TypeMirror
11 | import kotlin.reflect.KClass
12 |
13 | fun TypeElement.toClassName(): ClassName = ClassName.get(this)
14 | fun TypeMirror.toTypeName(): TypeName = TypeName.get(this)
15 | fun KClass<*>.toClassName(): ClassName = ClassName.get(java)
16 |
17 | fun AnnotationMirror.toAnnotationSpec(): AnnotationSpec = AnnotationSpec.get(this)
18 |
19 | fun Iterable.joinToCode(separator: String = ", ") = CodeBlock.join(this, separator)
20 |
21 | /**
22 | * Like [ClassName.peerClass] except instead of honoring the enclosing class names they are
23 | * concatenated with `$` similar to the reflection name. `foo.Bar.Baz` invoking this function with
24 | * `Fuzz` will produce `foo.Baz$Fuzz`.
25 | */
26 | fun ClassName.peerClassWithReflectionNesting(name: String): ClassName {
27 | var prefix = ""
28 | var peek = this
29 | while (true) {
30 | peek = peek.enclosingClassName() ?: break
31 | prefix = peek.simpleName() + "$" + prefix
32 | }
33 | return ClassName.get(packageName(), prefix + name)
34 | }
35 |
36 | // TODO https://github.com/square/javapoet/issues/671
37 | fun TypeName.rawClassName(): ClassName = when (this) {
38 | is ClassName -> this
39 | is ParameterizedTypeName -> rawType
40 | else -> throw IllegalStateException("Cannot extract raw class name from $this")
41 | }
42 |
--------------------------------------------------------------------------------
/inflation-inject/src/test/java/app/cash/inject/inflation/InflationInjectFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import org.junit.Assert.assertNull
8 | import org.junit.Assert.assertSame
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.robolectric.RobolectricTestRunner
12 | import org.robolectric.RuntimeEnvironment
13 | import org.robolectric.annotation.Config
14 |
15 | @Config(sdk = [26])
16 | @RunWith(RobolectricTestRunner::class)
17 | class InflationInjectFactoryTest {
18 | private val context = RuntimeEnvironment.systemContext
19 |
20 | @Test fun viewFactoryInMap() {
21 | val expected = View(context)
22 | val factories = mapOf("com.example.View" to ViewFactory { _, _ -> expected })
23 | val inflater = InflationInjectFactory(factories, ThrowingFactory)
24 | val actual = inflater.onCreateView("com.example.View", context, null)
25 | assertSame(expected, actual)
26 | }
27 |
28 | @Test fun viewFactoryMissingWithDelegateDelegates() {
29 | val expected = View(context)
30 | val factories = emptyMap()
31 | val delegate = LayoutInflater.Factory { _, _, _ -> expected }
32 | val inflater = InflationInjectFactory(factories, delegate)
33 | val actual = inflater.onCreateView("com.example.View", context, null)
34 | assertSame(expected, actual)
35 | }
36 |
37 | @Test fun viewFactoryMissingWithoutDelegateReturnsNull() {
38 | val factories = emptyMap()
39 | val inflater = InflationInjectFactory(factories, null)
40 | val actual = inflater.onCreateView("com.example.View", context, null)
41 | assertNull(actual)
42 | }
43 | }
44 |
45 | private object ThrowingFactory : LayoutInflater.Factory {
46 | override fun onCreateView(name: String, context: Context, attrs: AttributeSet?): View? {
47 | throw AssertionError()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/inflation-inject-processor/src/main/java/app/cash/inject/inflation/processor/internal/generatedAnnotation.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.inject.inflation.processor.internal
17 |
18 | import com.squareup.javapoet.AnnotationSpec
19 | import javax.annotation.processing.Processor
20 | import javax.lang.model.SourceVersion
21 | import javax.lang.model.SourceVersion.RELEASE_8
22 | import javax.lang.model.util.Elements
23 |
24 | /**
25 | * Create a `@Generated` annotation using the correct type based on source version and availability
26 | * on the compilation classpath, a `value` with the fully-qualified class name of the calling
27 | * [Processor], and a comment pointing to this project's GitHub repo. Returns `null` if no
28 | * annotation type is available on the classpath.
29 | */
30 | fun Processor.createGeneratedAnnotation(
31 | sourceVersion: SourceVersion,
32 | elements: Elements
33 | ): AnnotationSpec? {
34 | val annotationTypeName = when {
35 | sourceVersion <= RELEASE_8 -> "javax.annotation.Generated"
36 | else -> "javax.annotation.processing.Generated"
37 | }
38 | val generatedType = elements.getTypeElement(annotationTypeName) ?: return null
39 | return AnnotationSpec.builder(generatedType.toClassName())
40 | .addMember("value", "\$S", javaClass.name)
41 | .addMember("comments", "\$S", "https://github.com/cashapp/InflationInject")
42 | .build()
43 | }
44 |
--------------------------------------------------------------------------------
/inflation-inject-processor/src/main/java/app/cash/inject/inflation/processor/Key.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.inject.inflation.processor
17 |
18 | import com.google.auto.common.MoreTypes
19 | import app.cash.inject.inflation.processor.internal.hasAnnotation
20 | import app.cash.inject.inflation.processor.internal.toAnnotationSpec
21 | import app.cash.inject.inflation.processor.internal.toTypeName
22 | import com.squareup.javapoet.AnnotationSpec
23 | import com.squareup.javapoet.ParameterizedTypeName
24 | import com.squareup.javapoet.TypeName
25 | import javax.lang.model.element.VariableElement
26 | import javax.lang.model.type.TypeMirror
27 |
28 | /** Represents a type and an optional qualifier annotation for a binding. */
29 | data class Key(
30 | val type: TypeName,
31 | val qualifier: AnnotationSpec? = null,
32 | val useProvider: Boolean = true
33 | ) {
34 | override fun toString() = qualifier?.let { "$it $type" } ?: type.toString()
35 | }
36 |
37 | /** Create a [Key] from this type and any qualifier annotation. */
38 | fun VariableElement.asKey(mirror: TypeMirror = asType()): Key {
39 | val type = mirror.toTypeName()
40 | val qualifier = annotationMirrors.find {
41 | it.annotationType.asElement().hasAnnotation("javax.inject.Qualifier")
42 | }?.toAnnotationSpec()
43 |
44 | // Do not wrap a Provider inside another Provider.
45 | val provider = type is ParameterizedTypeName && type.rawType == JAVAX_PROVIDER
46 |
47 | val typeElement = if (type.isPrimitive) null else MoreTypes.asElement(mirror)
48 | // Dagger forbids requesting an @AssistedFactory-annotated type inside of a Provider.
49 | val daggerAssistedFactory = typeElement?.hasAnnotation("dagger.assisted.AssistedFactory") ?: false
50 |
51 | return Key(type, qualifier, !provider && !daggerAssistedFactory)
52 | }
53 |
--------------------------------------------------------------------------------
/inflation-inject/src/test/java/app/cash/inject/inflation/InflationInjectFactory2Test.kt:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import org.junit.Assert.assertNull
8 | import org.junit.Assert.assertSame
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.robolectric.RobolectricTestRunner
12 | import org.robolectric.RuntimeEnvironment
13 | import org.robolectric.annotation.Config
14 |
15 | @Config(sdk = [26])
16 | @RunWith(RobolectricTestRunner::class)
17 | class InflationInjectFactory2Test {
18 | private val context = RuntimeEnvironment.systemContext
19 |
20 | @Test fun viewFactoryInMap() {
21 | val expected = View(context)
22 | val factories = mapOf("com.example.View" to ViewFactory { _, _ -> expected })
23 | val inflater = InflationInjectFactory(factories, ThrowingFactory2)
24 | val actual = inflater.onCreateView(null, "com.example.View", context, null)
25 | assertSame(expected, actual)
26 | }
27 |
28 | @Test fun viewFactoryMissingWithDelegateDelegates() {
29 | val expected = View(context)
30 | val factories = emptyMap()
31 | val delegate = object : LayoutInflater.Factory2 {
32 | override fun onCreateView(parent: View?, name: String, context: Context,
33 | attrs: AttributeSet?) = expected
34 |
35 | override fun onCreateView(name: String, context: Context, attrs: AttributeSet?): View {
36 | throw AssertionError()
37 | }
38 |
39 | }
40 | val inflater = InflationInjectFactory(factories, delegate)
41 | val actual = inflater.onCreateView(null, "com.example.View", context, null)
42 | assertSame(expected, actual)
43 | }
44 |
45 | @Test fun viewFactoryMissingWithoutDelegateReturnsNull() {
46 | val factories = emptyMap()
47 | val inflater = InflationInjectFactory(factories, null)
48 | val actual = inflater.onCreateView(null, "com.example.View", context, null)
49 | assertNull(actual)
50 | }
51 | }
52 |
53 | private object ThrowingFactory2 : LayoutInflater.Factory2 {
54 | override fun onCreateView(parent: View?, name: String, context: Context,
55 | attrs: AttributeSet?): View? {
56 | throw AssertionError()
57 | }
58 |
59 | override fun onCreateView(name: String, context: Context, attrs: AttributeSet?): View? {
60 | throw AssertionError()
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/inflation-inject-processor/src/main/java/app/cash/inject/inflation/processor/InflationInjectionModule.kt:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation.processor
2 |
3 | import app.cash.inject.inflation.processor.internal.applyEach
4 | import app.cash.inject.inflation.processor.internal.peerClassWithReflectionNesting
5 | import app.cash.inject.inflation.ViewFactory
6 | import com.squareup.javapoet.AnnotationSpec
7 | import com.squareup.javapoet.ClassName
8 | import com.squareup.javapoet.MethodSpec
9 | import com.squareup.javapoet.TypeSpec
10 | import javax.lang.model.element.Modifier.ABSTRACT
11 | import javax.lang.model.element.Modifier.PRIVATE
12 | import javax.lang.model.element.Modifier.PUBLIC
13 |
14 | private val MODULE = ClassName.get("dagger", "Module")
15 | private val BINDS = ClassName.get("dagger", "Binds")
16 | private val INTO_MAP = ClassName.get("dagger.multibindings", "IntoMap")
17 | private val STRING_KEY = ClassName.get("dagger.multibindings", "StringKey")
18 |
19 | data class InflationInjectionModule(
20 | val moduleName: ClassName,
21 | val public: Boolean,
22 | val injectedNames: List,
23 | /** An optional `@Generated` annotation marker. */
24 | val generatedAnnotation: AnnotationSpec? = null
25 | ) {
26 | val generatedType = moduleName.inflationInjectModuleName()
27 |
28 | fun brewJava(): TypeSpec {
29 | return TypeSpec.classBuilder(generatedType)
30 | .addAnnotation(MODULE)
31 | .apply {
32 | if (generatedAnnotation != null) {
33 | addAnnotation(generatedAnnotation)
34 | }
35 | }
36 | .addModifiers(ABSTRACT)
37 | .apply {
38 | if (public) {
39 | addModifiers(PUBLIC)
40 | }
41 | }
42 | .addMethod(MethodSpec.constructorBuilder()
43 | .addModifiers(PRIVATE)
44 | .build())
45 | .applyEach(injectedNames.sorted()) { injectedName ->
46 | addMethod(MethodSpec.methodBuilder(injectedName.bindMethodName())
47 | .addAnnotation(BINDS)
48 | .addAnnotation(INTO_MAP)
49 | .addAnnotation(AnnotationSpec.builder(STRING_KEY)
50 | .addMember("value", "\$S", injectedName.reflectionName())
51 | .build())
52 | .addModifiers(ABSTRACT)
53 | .returns(ViewFactory::class.java)
54 | .addParameter(injectedName.assistedInjectFactoryName(), "factory")
55 | .build())
56 | }
57 | .build()
58 | }
59 | }
60 |
61 | private fun ClassName.bindMethodName() = "bind_" + reflectionName().replace('.', '_')
62 |
63 | fun ClassName.inflationInjectModuleName(): ClassName =
64 | peerClassWithReflectionNesting("InflationInject_" + simpleName())
65 |
--------------------------------------------------------------------------------
/inflation-inject/src/main/java/app/cash/inject/inflation/InflationInjectFactory.java:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import androidx.annotation.NonNull;
8 | import androidx.annotation.Nullable;
9 | import java.util.Map;
10 | import javax.inject.Inject;
11 |
12 | /**
13 | * A factory for {@link LayoutInflater} which can create
14 | * {@link InflationInject @InflationInject}-annotated views. This type should be injected rather
15 | * than created manually as it will ask your dependency graph for a map of all the creatable views.
16 | *
17 | * @see InflationInject
18 | * @see InflationModule
19 | */
20 | public final class InflationInjectFactory implements LayoutInflater.Factory2 {
21 | private final Map factories;
22 | private final @Nullable LayoutInflater.Factory delegate;
23 | private final @Nullable LayoutInflater.Factory2 delegate2;
24 |
25 | @Inject
26 | public InflationInjectFactory(@NonNull Map factories) {
27 | this(factories, null);
28 | }
29 |
30 | @SuppressWarnings("ConstantConditions") // Validating API invariants.
31 | public InflationInjectFactory(@NonNull Map factories,
32 | @Nullable LayoutInflater.Factory delegate) {
33 | if (factories == null) throw new NullPointerException("factories == null");
34 | this.factories = factories;
35 | this.delegate = delegate;
36 | this.delegate2 = null;
37 | }
38 |
39 | @SuppressWarnings("ConstantConditions") // Validating API invariants.
40 | public InflationInjectFactory(@NonNull Map factories,
41 | @Nullable LayoutInflater.Factory2 delegate) {
42 | if (factories == null) throw new NullPointerException("factories == null");
43 | this.factories = factories;
44 | this.delegate = null;
45 | this.delegate2 = delegate;
46 | }
47 |
48 | @Nullable @Override public View onCreateView(@NonNull String name, @NonNull Context context,
49 | @Nullable AttributeSet attrs) {
50 | ViewFactory factory = factories.get(name);
51 | if (factory != null) {
52 | return factory.create(context, attrs);
53 | }
54 | if (delegate != null) {
55 | return delegate.onCreateView(name, context, attrs);
56 | }
57 | return null;
58 | }
59 |
60 | @Nullable
61 | @Override
62 | public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
63 | ViewFactory factory = factories.get(name);
64 | if (factory != null) {
65 | return factory.create(context, attrs);
66 | }
67 | if (delegate2 != null) {
68 | return delegate2.onCreateView(parent, name, context, attrs);
69 | }
70 | if (delegate != null) {
71 | return delegate.onCreateView(name, context, attrs);
72 | }
73 | return null;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/inflation-inject-processor/src/main/java/app/cash/inject/inflation/processor/internal/annotationProcessing.kt:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation.processor.internal
2 |
3 | import app.cash.inject.inflation.processor.internal.MirrorValue.Array
4 | import app.cash.inject.inflation.processor.internal.MirrorValue.Error
5 | import app.cash.inject.inflation.processor.internal.MirrorValue.Type
6 | import app.cash.inject.inflation.processor.internal.MirrorValue.Unmapped
7 | import javax.annotation.processing.RoundEnvironment
8 | import javax.lang.model.AnnotatedConstruct
9 | import javax.lang.model.element.AnnotationMirror
10 | import javax.lang.model.element.AnnotationValue
11 | import javax.lang.model.element.Element
12 | import javax.lang.model.element.TypeElement
13 | import javax.lang.model.type.ErrorType
14 | import javax.lang.model.type.TypeMirror
15 | import javax.lang.model.util.Elements
16 | import javax.lang.model.util.SimpleAnnotationValueVisitor6
17 | import javax.lang.model.util.SimpleTypeVisitor6
18 |
19 | /** Return a list of elements annotated with `T`. */
20 | inline fun RoundEnvironment.findElementsAnnotatedWith(): Set
21 | = getElementsAnnotatedWith(T::class.java)
22 |
23 | /** Return true if this [AnnotatedConstruct] is annotated with `T`. */
24 | inline fun AnnotatedConstruct.hasAnnotation()
25 | = getAnnotation(T::class.java) != null
26 |
27 | /** Return true if this [AnnotatedConstruct] is annotated with `qualifiedName`. */
28 | fun AnnotatedConstruct.hasAnnotation(qualifiedName: String) = getAnnotation(qualifiedName) != null
29 |
30 | /** Return the first annotation matching [qualifiedName] or null. */
31 | fun AnnotatedConstruct.getAnnotation(qualifiedName: String) = annotationMirrors
32 | .firstOrNull {
33 | it.annotationType.asElement().cast().qualifiedName.contentEquals(qualifiedName)
34 | }
35 |
36 | fun AnnotationMirror.getValue(property: String, elements: Elements) = elements
37 | .getElementValuesWithDefaults(this)
38 | .entries
39 | .firstOrNull { it.key.simpleName.contentEquals(property) }
40 | ?.value
41 | ?.toMirrorValue()
42 |
43 | fun AnnotationValue.toMirrorValue(): MirrorValue = accept(MirrorValueVisitor, null)
44 |
45 | sealed class MirrorValue {
46 | data class Type(private val value: TypeMirror) : MirrorValue(), TypeMirror by value
47 | data class Array(private val value: List) : MirrorValue(), List by value
48 | object Unmapped : MirrorValue()
49 | object Error : MirrorValue()
50 | }
51 |
52 | private object MirrorValueVisitor : SimpleAnnotationValueVisitor6() {
53 | override fun defaultAction(o: Any, ignored: Nothing?) = Unmapped
54 |
55 | override fun visitType(mirror: TypeMirror, ignored: Nothing?) = mirror.accept(TypeVisitor, null)
56 |
57 | override fun visitArray(values: List, ignored: Nothing?) =
58 | Array(values.map { it.accept(this, null) })
59 | }
60 | private object TypeVisitor : SimpleTypeVisitor6() {
61 | override fun visitError(type: ErrorType, ignored: Nothing?) = Error
62 | override fun defaultAction(type: TypeMirror, ignored: Nothing?) = Type(type)
63 | }
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Inflation Injection
2 |
3 | **Attention**: This tool is now deprecated. Please switch to
4 | [Compose UI](https://developer.android.com/jetpack/compose) or manually pass dependencies
5 | to the legacy view system. Existing versions will also continue to work, but feature
6 | development and bug fixes have stopped.
7 |
8 | ---
9 |
10 | Constructor-inject views during XML layout inflation.
11 |
12 | Looking for Assisted Inject? It's [built in to Dagger now](https://dagger.dev/dev-guide/assisted-injection.html)!
13 |
14 |
15 | ## Usage
16 |
17 | Write your layout XML like normal.
18 |
19 | ```xml
20 |
21 |
22 |
23 |
24 | ```
25 |
26 | Use `@InflationInject` in `CustomView`:
27 |
28 | ```java
29 | public final class CustomView extends View {
30 | private final Picasso picasso;
31 |
32 | @InflationInject
33 | public CustomView(
34 | @Inflated Context context,
35 | @Inflated AttributeSet attrs,
36 | Picasso picasso
37 | ) {
38 | super(context, attrs);
39 | this.picasso = picasso;
40 | }
41 |
42 | // ...
43 | }
44 | ```
45 |
46 | In order to allow Dagger to create your custom views, add `@InflationModule` to a Dagger module and
47 | add the generated module name to its `includes=`.
48 |
49 | ```java
50 | @InflationModule
51 | @Module(includes = InflationInject_PresenterModule.class)
52 | abstract class PresenterModule {}
53 | ```
54 |
55 | The annotation processor will generate the `InflationInject_PresenterModule` for us. It will not be
56 | resolved until the processor runs.
57 |
58 | Finally, inject `InflationInjectFactory` and add it to your `LayoutInflater`.
59 |
60 | ```java
61 | InflationInjectFactory factory = DaggerMainActivity_MainComponent.create().factory();
62 | getLayoutInflater().setFactory(factory);
63 |
64 | setContentView(R.layout.main_view);
65 | ```
66 |
67 |
68 | ## Download
69 |
70 | ```groovy
71 | repositories {
72 | mavenCentral()
73 | }
74 | dependencies {
75 | implementation 'app.cash.inject:inflation-inject:1.0.1'
76 | annotationProcessor 'app.cash.inject:inflation-inject-processor:1.0.1'
77 | }
78 | ```
79 |
80 |
81 | Snapshots of the development version are available in Sonatype's snapshots repository.
82 |
83 |
84 | ```groovy
85 | repositories {
86 | maven {
87 | url 'https://oss.sonatype.org/content/repositories/snapshots/'
88 | }
89 | }
90 | dependencies {
91 | implementation 'app.cash.inject:inflation-inject:1.1.0-SNAPSHOT'
92 | annotationProcessor 'app.cash.inject:inflation-inject-processor:1.1.0-SNAPSHOT'
93 | }
94 | ```
95 |
96 |
97 |
98 |
99 |
100 | # License
101 |
102 | Copyright 2017 Square, Inc.
103 |
104 | Licensed under the Apache License, Version 2.0 (the "License");
105 | you may not use this file except in compliance with the License.
106 | You may obtain a copy of the License at
107 |
108 | http://www.apache.org/licenses/LICENSE-2.0
109 |
110 | Unless required by applicable law or agreed to in writing, software
111 | distributed under the License is distributed on an "AS IS" BASIS,
112 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
113 | See the License for the specific language governing permissions and
114 | limitations under the License.
115 |
116 |
--------------------------------------------------------------------------------
/inflation-inject-processor/src/main/java/app/cash/inject/inflation/processor/AssistedInjection.kt:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation.processor
2 |
3 | import app.cash.inject.inflation.processor.internal.applyEach
4 | import app.cash.inject.inflation.processor.internal.joinToCode
5 | import app.cash.inject.inflation.processor.internal.peerClassWithReflectionNesting
6 | import app.cash.inject.inflation.processor.internal.rawClassName
7 | import com.squareup.javapoet.AnnotationSpec
8 | import com.squareup.javapoet.ClassName
9 | import com.squareup.javapoet.CodeBlock
10 | import com.squareup.javapoet.MethodSpec
11 | import com.squareup.javapoet.ParameterizedTypeName
12 | import com.squareup.javapoet.TypeName
13 | import com.squareup.javapoet.TypeSpec
14 | import com.squareup.javapoet.TypeVariableName
15 | import javax.lang.model.element.Modifier.FINAL
16 | import javax.lang.model.element.Modifier.PRIVATE
17 | import javax.lang.model.element.Modifier.PUBLIC
18 |
19 | private val JAVAX_INJECT = ClassName.get("javax.inject", "Inject")
20 | internal val JAVAX_PROVIDER = ClassName.get("javax.inject", "Provider")
21 |
22 | /** The structure of an assisted injection factory. */
23 | data class AssistedInjection(
24 | /** The type which will be instantiated inside the factory. */
25 | val targetType: TypeName,
26 | /** TODO */
27 | val dependencyRequests: List,
28 | /** The factory interface type. */
29 | val factoryType: TypeName,
30 | /** Name of the factory's only method. */
31 | val factoryMethod: String,
32 | /** The factory method return type. [targetType] must be assignable to this type. */
33 | val returnType: TypeName = targetType,
34 | /**
35 | * The factory method keys. These default to the keys of the assisted [dependencyRequests]
36 | * and when supplied must always match them, but the order is allowed to be different.
37 | */
38 | val assistedKeys: List = dependencyRequests.filter { it.isAssisted }.map { it.key },
39 | /** An optional `@Generated` annotation marker. */
40 | val generatedAnnotation: AnnotationSpec? = null
41 | ) {
42 | private val keyToRequest = dependencyRequests.filter { it.isAssisted }.associateBy { it.key }
43 | init {
44 | check(keyToRequest.keys == assistedKeys.toSet()) {
45 | """
46 | assistedKeys must contain the same elements as the assisted dependencyRequests.
47 |
48 | * assistedKeys:
49 | $assistedKeys
50 | * assisted dependencyRequests:
51 | ${keyToRequest.keys}
52 | """.trimIndent()
53 | }
54 | }
55 |
56 | /** The type generated from [brewJava]. */
57 | val generatedType = targetType.rawClassName().assistedInjectFactoryName()
58 |
59 | private val providedKeys = dependencyRequests.filterNot { it.isAssisted }
60 |
61 | fun brewJava(): TypeSpec {
62 | return TypeSpec.classBuilder(generatedType)
63 | .addModifiers(PUBLIC, FINAL)
64 | .addSuperinterface(factoryType)
65 | .apply {
66 | if (generatedAnnotation != null) {
67 | addAnnotation(generatedAnnotation)
68 | }
69 | }
70 | .applyEach(providedKeys) {
71 | addField(it.providerType.withoutAnnotations(), it.name, PRIVATE, FINAL)
72 | }
73 | .addMethod(MethodSpec.constructorBuilder()
74 | .addModifiers(PUBLIC)
75 | .addAnnotation(JAVAX_INJECT)
76 | .applyEach(providedKeys) {
77 | addParameter(it.providerType, it.name)
78 | addStatement("this.$1N = $1N", it.name)
79 | }
80 | .build())
81 | .addMethod(MethodSpec.methodBuilder(factoryMethod)
82 | .addAnnotation(Override::class.java)
83 | .addModifiers(PUBLIC)
84 | .returns(returnType)
85 | .apply {
86 | if (targetType is ParameterizedTypeName) {
87 | addTypeVariables(targetType.typeArguments.filterIsInstance())
88 | }
89 | }
90 | .applyEach(assistedKeys) { key ->
91 | val parameterName = keyToRequest.getValue(key).name
92 | addParameter(key.type, parameterName)
93 | }
94 | .addStatement("return new \$T(\n\$L)", targetType,
95 | dependencyRequests.map { it.argumentProvider }.joinToCode(",\n"))
96 | .build())
97 | .build()
98 | }
99 | }
100 |
101 | private val DependencyRequest.providerType: TypeName
102 | get() {
103 | val type = if (key.useProvider) {
104 | ParameterizedTypeName.get(JAVAX_PROVIDER, key.type.box())
105 | } else {
106 | key.type
107 | }
108 | key.qualifier?.let {
109 | return type.annotated(it)
110 | }
111 | return type
112 | }
113 |
114 | private val DependencyRequest.argumentProvider
115 | get() = CodeBlock.of(if (isAssisted || !key.useProvider) "\$N" else "\$N.get()", name)
116 |
117 | fun ClassName.assistedInjectFactoryName(): ClassName =
118 | peerClassWithReflectionNesting(simpleName() + "_InflationFactory")
119 |
--------------------------------------------------------------------------------
/gradle/gradle-mvn-push.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Chris Banes
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | apply plugin: 'maven'
18 | apply plugin: 'signing'
19 |
20 | version = VERSION_NAME
21 | group = GROUP
22 |
23 | def isReleaseBuild() {
24 | return VERSION_NAME.contains("SNAPSHOT") == false
25 | }
26 |
27 | def getReleaseRepositoryUrl() {
28 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
29 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
30 | }
31 |
32 | def getSnapshotRepositoryUrl() {
33 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
34 | : "https://oss.sonatype.org/content/repositories/snapshots/"
35 | }
36 |
37 | def getRepositoryUsername() {
38 | return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : ""
39 | }
40 |
41 | def getRepositoryPassword() {
42 | return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : ""
43 | }
44 |
45 | afterEvaluate { project ->
46 | uploadArchives {
47 | repositories {
48 | mavenDeployer {
49 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
50 |
51 | pom.groupId = GROUP
52 | pom.artifactId = POM_ARTIFACT_ID
53 | pom.version = VERSION_NAME
54 |
55 | repository(url: getReleaseRepositoryUrl()) {
56 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
57 | }
58 | snapshotRepository(url: getSnapshotRepositoryUrl()) {
59 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
60 | }
61 |
62 | pom.project {
63 | name POM_NAME
64 | packaging POM_PACKAGING
65 | description POM_DESCRIPTION
66 | url POM_URL
67 |
68 | scm {
69 | url POM_SCM_URL
70 | connection POM_SCM_CONNECTION
71 | developerConnection POM_SCM_DEV_CONNECTION
72 | }
73 |
74 | licenses {
75 | license {
76 | name POM_LICENCE_NAME
77 | url POM_LICENCE_URL
78 | distribution POM_LICENCE_DIST
79 | }
80 | }
81 |
82 | developers {
83 | developer {
84 | id POM_DEVELOPER_ID
85 | name POM_DEVELOPER_NAME
86 | }
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | signing {
94 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
95 | sign configurations.archives
96 | }
97 |
98 | if (project.getPlugins().hasPlugin('com.android.application') ||
99 | project.getPlugins().hasPlugin('com.android.library')) {
100 | task install(type: Upload, dependsOn: assemble) {
101 | repositories.mavenInstaller {
102 | configuration = configurations.archives
103 |
104 | pom.groupId = GROUP
105 | pom.artifactId = POM_ARTIFACT_ID
106 | pom.version = VERSION_NAME
107 |
108 | pom.project {
109 | name POM_NAME
110 | packaging POM_PACKAGING
111 | description POM_DESCRIPTION
112 | url POM_URL
113 |
114 | scm {
115 | url POM_SCM_URL
116 | connection POM_SCM_CONNECTION
117 | developerConnection POM_SCM_DEV_CONNECTION
118 | }
119 |
120 | licenses {
121 | license {
122 | name POM_LICENCE_NAME
123 | url POM_LICENCE_URL
124 | distribution POM_LICENCE_DIST
125 | }
126 | }
127 |
128 | developers {
129 | developer {
130 | id POM_DEVELOPER_ID
131 | name POM_DEVELOPER_NAME
132 | }
133 | }
134 | }
135 | }
136 | }
137 |
138 | task androidJavadocs(type: Javadoc) {
139 | source = android.sourceSets.main.java.source
140 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
141 | }
142 |
143 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
144 | classifier = 'javadoc'
145 | from androidJavadocs.destinationDir
146 | }
147 |
148 | task androidSourcesJar(type: Jar) {
149 | classifier = 'sources'
150 | from android.sourceSets.main.java.source
151 | }
152 | } else {
153 | install {
154 | repositories.mavenInstaller {
155 | pom.groupId = GROUP
156 | pom.artifactId = POM_ARTIFACT_ID
157 | pom.version = VERSION_NAME
158 |
159 | pom.project {
160 | name POM_NAME
161 | packaging POM_PACKAGING
162 | description POM_DESCRIPTION
163 | url POM_URL
164 |
165 | scm {
166 | url POM_SCM_URL
167 | connection POM_SCM_CONNECTION
168 | developerConnection POM_SCM_DEV_CONNECTION
169 | }
170 |
171 | licenses {
172 | license {
173 | name POM_LICENCE_NAME
174 | url POM_LICENCE_URL
175 | distribution POM_LICENCE_DIST
176 | }
177 | }
178 |
179 | developers {
180 | developer {
181 | id POM_DEVELOPER_ID
182 | name POM_DEVELOPER_NAME
183 | }
184 | }
185 | }
186 | }
187 | }
188 |
189 | task sourcesJar(type: Jar, dependsOn:classes) {
190 | classifier = 'sources'
191 | from sourceSets.main.allSource
192 | }
193 |
194 | task javadocJar(type: Jar, dependsOn:javadoc) {
195 | classifier = 'javadoc'
196 | from javadoc.destinationDir
197 | }
198 | }
199 |
200 | if (JavaVersion.current().isJava8Compatible()) {
201 | allprojects {
202 | tasks.withType(Javadoc) {
203 | options.addStringOption('Xdoclint:none', '-quiet')
204 | }
205 | }
206 | }
207 |
208 | artifacts {
209 | if (project.getPlugins().hasPlugin('com.android.application') ||
210 | project.getPlugins().hasPlugin('com.android.library')) {
211 | archives androidSourcesJar
212 | archives androidJavadocsJar
213 | } else {
214 | archives sourcesJar
215 | archives javadocJar
216 | }
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [Unreleased]
4 |
5 | ## [1.0.1] - 2022-01-19
6 |
7 | * Fix: Sort methods in generated module code by name to ensure compatiblity with Gradle remote caching.
8 |
9 |
10 | ## [1.0.0] - 2021-03-27
11 |
12 | Change package name and Maven groupId to `app.cash`.
13 |
14 | No other changes since 0.9.1.
15 |
16 | Looking for Assisted Inject?
17 | It's [built in to Dagger now](https://dagger.dev/dev-guide/assisted-injection.html)!
18 |
19 |
20 | ## [0.9.1] - 2021-03-26
21 |
22 | * Fix: Do not add Kotlin stdlib as a dependency to inflation inject runtime which is written in Java.
23 |
24 |
25 | ## [0.9.0] - 2021-03-25
26 |
27 | Assisted injection has been removed from the project because it is now available upstream
28 | as part of Dagger. See https://dagger.dev/dev-guide/assisted-injection.html
29 |
30 | This project now solely hosts inflation injection, which otherwise had no changes in this version.
31 |
32 |
33 | ## [0.8.1] - 2021-03-22
34 |
35 | * Fix: Allow inflation inject processor to work in modules that do not have assisted inject
36 | annotations present.
37 |
38 |
39 | ## [0.8.0] - 2021-03-22
40 |
41 | * New: Inflation injection now uses its own `@Inflated` annotation for `Context` and `AttributeSet`.
42 | Using `@Assisted` is no longer supported.
43 |
44 |
45 | ## [0.7.0] - 2021-03-21
46 |
47 | * Similar to how Dagger disambiguates two injections of the same type, qualifier annotations are now
48 | required to disambiguate assisted parameters. Parameter names are no longer significant.
49 | * Fix: Do not wrap types annotated with Dagger's `@AssistedFactory` in a `Provider` if injected into
50 | one of our `@AssistedInject` or `@InflationInject` types.
51 |
52 | Note: This first was erroneously tagged as 0.6.1.
53 |
54 |
55 | ## [0.6.0] - 2020-09-14
56 |
57 | * New: Annotate generated module with `@InstallIn` if the `@AssistedModule` is also annotated with
58 | `@InstallIn`. If the `@AssistedModule` is not annotated with `@InstallIn` and Hilt is available on
59 | the classpath, the generated module will be annotated with `@DisableInstallInCheck`.
60 | * Fix: Ignore `copy` method when searching for `@Assisted` annotation. The Kotlin compiler will copy
61 | the annotation to this method on a `data class` which we cannot disable.
62 |
63 |
64 | ## [0.5.2] - 2019-11-22
65 |
66 | * Fix: Properly honor target `-source` version when generating the `@Generated` annotation on sources.
67 |
68 |
69 | ## [0.5.1] - 2019-10-28
70 |
71 | * New: Support incremental annotation proessing in inflation inject (previously it was only supported
72 | by assisted inject).
73 | * Fix: Change annotations to class retention to actually allow incremental annotation processing
74 | to work.
75 |
76 |
77 | ## [0.5.0] - 2019-08-08
78 |
79 | * New: Support incremental annotation processing.
80 | * Fix: Explicitly sort generated module bindings to ensure stable output and avoid recompilation.
81 | * Fix: Support nested `@AssistedModule` and `@InflationModule` classes.
82 |
83 |
84 | ## [0.4.0] - 2019-04-05
85 |
86 | * New: Other annotation processors using this tool as a library can now specify factory superinterfaces
87 | with any type, not just a class.
88 | * Fix: Allow duplicates for provided dependencies. Dagger does not prohibit this so neither should this tool.
89 |
90 |
91 | ## [0.3.3] - 2019-03-07
92 |
93 | * Fix: Support `@AssistedModule`s which are generated by other annotation processors.
94 |
95 |
96 | ## [0.3.2] - 2018-11-27
97 |
98 | * Relax the message for when zero assisted parameters or zero provided parameters were used from being
99 | an error to a warning. While assisted injection isn't needed in either of those cases, nothing actually
100 | prevent it from still working.
101 |
102 |
103 | ## [0.3.1] - 2018-11-19
104 |
105 | * Fix: Honor abstract factory methods which come from supertypes.
106 | * Fix: Support generic use in factory parameters and return type.
107 |
108 |
109 | ## [0.3.0] - 2018-10-16
110 |
111 | * New: Allow multiple assisted parameters of the same type. This also brings validation that all
112 | parameter names in the factory match those of the constructor. Without this, there is no way to
113 | correctly match arguments when multiple of the same type are present.
114 | * New: Validate that modules annotated with `@AssistedModule` or `@InflationModule` actually include
115 | the generated module in their `includes=` list.
116 | * New: Include a `@Generated` annotation which is appropriate for the target JDK on all generated code.
117 | * Fix: Mark invalid usages of `@Assisted` at compile time:
118 | * Cannot be used on a non-constructor parameter.
119 | * Cannot be used on a constructor without `@AssistedInject` or `@InflationInject`.
120 | * Cannot be used on a constructor which is annotated with `@Inject`.
121 | * Fix: Inflation injection now validates that targets are subtypes of View rather than generating
122 | code which fails to compile.
123 | * Fix: Support injection of primitives by boxing them when generating a `Provider<..>` type.
124 | * Fix: Generate correct code for types which are nested inside others.
125 |
126 |
127 | ## [0.2.1] - 2018-09-04
128 |
129 | * Fix: Ensure the generated Dagger 2 module is public if the user-defined module is public.
130 |
131 |
132 | ## [0.2.0] - 2018-08-20
133 |
134 | * New: Android view-inflation injection! Inject dependencies into your custom views as constructor
135 | parameters while still allowing inflation from XML.
136 | * Fix: Factory parameter order is no longer required to match constructor parameter order.
137 | * Fix: Requesting a `Provider` injection now works correctly.
138 | * Fix: Duplicate assisted or provided dependencies now issue a compiler error.
139 | * Fix: Validate visibility of the type, constructor, and factory prior to generating the factory.
140 | This produces a better error message instead of generating code that will fail to compile.
141 |
142 |
143 | ## [0.1.2] - 2017-07-19
144 |
145 | * Fix: Support creating parameterized types.
146 |
147 |
148 | ## [0.1.1] - 2017-07-03
149 |
150 | * Fix: Ensure annotation processors are registered as such in the jar.
151 |
152 |
153 | ## [0.1.0] - 2017-07-02
154 |
155 | Initial preview release.
156 |
157 |
158 |
159 | [Unreleased]: https://github.com/cashapp/InflationInject/compare/1.0.1...HEAD
160 | [1.0.1]: https://github.com/cashapp/InflationInject/releases/tag/1.0.1
161 | [1.0.0]: https://github.com/cashapp/InflationInject/releases/tag/1.0.0
162 | [0.9.1]: https://github.com/cashapp/InflationInject/releases/tag/0.9.1
163 | [0.9.0]: https://github.com/cashapp/InflationInject/releases/tag/0.9.0
164 | [0.8.1]: https://github.com/cashapp/InflationInject/releases/tag/0.8.1
165 | [0.8.0]: https://github.com/cashapp/InflationInject/releases/tag/0.8.0
166 | [0.7.0]: https://github.com/cashapp/InflationInject/releases/tag/0.7.0
167 | [0.6.0]: https://github.com/cashapp/InflationInject/releases/tag/0.6.0
168 | [0.5.2]: https://github.com/cashapp/InflationInject/releases/tag/0.5.2
169 | [0.5.1]: https://github.com/cashapp/InflationInject/releases/tag/0.5.1
170 | [0.5.0]: https://github.com/cashapp/InflationInject/releases/tag/0.5.0
171 | [0.4.0]: https://github.com/cashapp/InflationInject/releases/tag/0.4.0
172 | [0.3.3]: https://github.com/cashapp/InflationInject/releases/tag/0.3.3
173 | [0.3.2]: https://github.com/cashapp/InflationInject/releases/tag/0.3.2
174 | [0.3.1]: https://github.com/cashapp/InflationInject/releases/tag/0.3.1
175 | [0.3.0]: https://github.com/cashapp/InflationInject/releases/tag/0.3.0
176 | [0.2.1]: https://github.com/cashapp/InflationInject/releases/tag/0.2.1
177 | [0.2.0]: https://github.com/cashapp/InflationInject/releases/tag/0.2.0
178 | [0.1.2]: https://github.com/cashapp/InflationInject/releases/tag/0.1.2
179 | [0.1.1]: https://github.com/cashapp/InflationInject/releases/tag/0.1.1
180 | [0.1.0]: https://github.com/cashapp/InflationInject/releases/tag/0.1.0
181 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/inflation-inject-processor/src/main/java/app/cash/inject/inflation/processor/InflationInjectProcessor.kt:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation.processor
2 |
3 | import com.google.auto.service.AutoService
4 | import app.cash.inject.inflation.processor.internal.MirrorValue
5 | import app.cash.inject.inflation.processor.internal.applyEach
6 | import app.cash.inject.inflation.processor.internal.cast
7 | import app.cash.inject.inflation.processor.internal.castEach
8 | import app.cash.inject.inflation.processor.internal.createGeneratedAnnotation
9 | import app.cash.inject.inflation.processor.internal.filterNotNullValues
10 | import app.cash.inject.inflation.processor.internal.findElementsAnnotatedWith
11 | import app.cash.inject.inflation.processor.internal.getAnnotation
12 | import app.cash.inject.inflation.processor.internal.getValue
13 | import app.cash.inject.inflation.processor.internal.hasAnnotation
14 | import app.cash.inject.inflation.processor.internal.toClassName
15 | import app.cash.inject.inflation.processor.internal.toTypeName
16 | import app.cash.inject.inflation.InflationInject
17 | import app.cash.inject.inflation.InflationModule
18 | import app.cash.inject.inflation.ViewFactory
19 | import com.squareup.javapoet.ClassName
20 | import com.squareup.javapoet.JavaFile
21 | import net.ltgt.gradle.incap.IncrementalAnnotationProcessor
22 | import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.AGGREGATING
23 | import javax.annotation.processing.AbstractProcessor
24 | import javax.annotation.processing.Filer
25 | import javax.annotation.processing.Messager
26 | import javax.annotation.processing.ProcessingEnvironment
27 | import javax.annotation.processing.Processor
28 | import javax.annotation.processing.RoundEnvironment
29 | import javax.lang.model.SourceVersion
30 | import javax.lang.model.element.Element
31 | import javax.lang.model.element.ElementKind.CLASS
32 | import javax.lang.model.element.ElementKind.CONSTRUCTOR
33 | import javax.lang.model.element.ExecutableElement
34 | import javax.lang.model.element.Modifier
35 | import javax.lang.model.element.Modifier.PRIVATE
36 | import javax.lang.model.element.Modifier.STATIC
37 | import javax.lang.model.element.TypeElement
38 | import javax.lang.model.type.TypeMirror
39 | import javax.lang.model.util.Elements
40 | import javax.lang.model.util.Types
41 | import javax.tools.Diagnostic.Kind.ERROR
42 | import javax.tools.Diagnostic.Kind.WARNING
43 |
44 | @IncrementalAnnotationProcessor(AGGREGATING)
45 | @AutoService(Processor::class)
46 | class InflationInjectProcessor : AbstractProcessor() {
47 | override fun getSupportedSourceVersion() = SourceVersion.latest()
48 | override fun getSupportedAnnotationTypes() = setOf(
49 | InflationInject::class.java.canonicalName,
50 | InflationModule::class.java.canonicalName)
51 |
52 | override fun init(env: ProcessingEnvironment) {
53 | super.init(env)
54 | sourceVersion = env.sourceVersion
55 | messager = env.messager
56 | filer = env.filer
57 | types = env.typeUtils
58 | elements = env.elementUtils
59 | viewType = elements.getTypeElement("android.view.View").asType()
60 | }
61 |
62 | private lateinit var sourceVersion: SourceVersion
63 | private lateinit var messager: Messager
64 | private lateinit var filer: Filer
65 | private lateinit var types: Types
66 | private lateinit var elements: Elements
67 | private lateinit var viewType: TypeMirror
68 |
69 | private var userModule: String? = null
70 |
71 | override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean {
72 | val inflationInjectElements = roundEnv.findInflationInjectCandidateTypeElements()
73 | .mapNotNull { it.toInflationInjectElementsOrNull() }
74 |
75 | inflationInjectElements
76 | .associateWith { it.toAssistedInjectionOrNull() }
77 | .filterNotNullValues()
78 | .forEach(::writeInflationInject)
79 |
80 | val inflationModuleElements = roundEnv.findInflationModuleTypeElement()
81 | ?.toInflationModuleElementsOrNull(inflationInjectElements)
82 |
83 | if (inflationModuleElements != null) {
84 | val moduleType = inflationModuleElements.moduleType
85 |
86 | val userModuleFqcn = userModule
87 | if (userModuleFqcn != null) {
88 | val userModuleType = elements.getTypeElement(userModuleFqcn)
89 | error("Multiple @InflationModule-annotated modules found.", userModuleType)
90 | error("Multiple @InflationModule-annotated modules found.", moduleType)
91 | userModule = null
92 | } else {
93 | userModule = moduleType.qualifiedName.toString()
94 |
95 | val inflationInjectionModule = inflationModuleElements.toInflationInjectionModule()
96 | writeInflationModule(inflationModuleElements, inflationInjectionModule)
97 | }
98 | }
99 |
100 | // Wait until processing is ending to validate that the @InflationModule's @Module annotation
101 | // includes the generated type.
102 | if (roundEnv.processingOver()) {
103 | val userModuleFqcn = userModule
104 | if (userModuleFqcn != null) {
105 | // In the processing round in which we handle the @InflationModule the @Module annotation's
106 | // includes contain an type because we haven't generated the inflation module yet.
107 | // As a result, we need to re-lookup the element so that its referenced types are available.
108 | val userModule = elements.getTypeElement(userModuleFqcn)
109 |
110 | // Previous validation guarantees this annotation is present.
111 | val moduleAnnotation = userModule.getAnnotation("dagger.Module")!!
112 | // Dagger guarantees this property is present and is an array of types or errors.
113 | val includes = moduleAnnotation.getValue("includes", elements)!!
114 | .cast()
115 | .filterIsInstance()
116 |
117 | val generatedModuleName = userModule.toClassName().inflationInjectModuleName()
118 | val referencesGeneratedModule = includes
119 | .map { it.toTypeName() }
120 | .any { it == generatedModuleName }
121 | if (!referencesGeneratedModule) {
122 | error("@InflationModule's @Module must include ${generatedModuleName.simpleName()}",
123 | userModule)
124 | }
125 | }
126 | }
127 |
128 | return false
129 | }
130 |
131 | /**
132 | * Find [TypeElement]s which are candidates for assisted injection by having a constructor
133 | * annotated with [InflationInject].
134 | */
135 | private fun RoundEnvironment.findInflationInjectCandidateTypeElements(): List {
136 | return findElementsAnnotatedWith()
137 | .map { it.enclosingElement as TypeElement }
138 | }
139 |
140 | /**
141 | * From this [TypeElement] which is a candidate for inflation injection, find and validate the
142 | * syntactical elements required to generate the factory:
143 | * - Non-private, non-inner target type
144 | * - Single non-private target constructor
145 | */
146 | private fun TypeElement.toInflationInjectElementsOrNull(): InflationInjectElements? {
147 | var valid = true
148 |
149 | if (PRIVATE in modifiers) {
150 | error("@InflationInject-using types must not be private", this)
151 | valid = false
152 | }
153 | if (enclosingElement.kind == CLASS && STATIC !in modifiers) {
154 | error("Nested @InflationInject-using types must be static", this)
155 | valid = false
156 | }
157 | if (!types.isSubtype(asType(), viewType)) {
158 | error("@InflationInject-using types must be subtypes of View", this)
159 | valid = false
160 | }
161 |
162 | val constructors = enclosedElements
163 | .filter { it.kind == CONSTRUCTOR }
164 | .filter { it.hasAnnotation() }
165 | .castEach()
166 | if (constructors.size > 1) {
167 | error("Multiple @InflationInject-annotated constructors found.", this)
168 | valid = false
169 | }
170 |
171 | if (!valid) return null
172 |
173 | val constructor = constructors.single()
174 | if (PRIVATE in constructor.modifiers) {
175 | error("@InflationInject constructor must not be private.", constructor)
176 | return null
177 | }
178 |
179 | return InflationInjectElements(this, constructor)
180 | }
181 |
182 | /**
183 | * From this [InflationInjectElements], parse and validate the semantic information of the
184 | * elements which is required to generate the factory:
185 | * - Unqualified assisted parameters of Context and AttributeSet
186 | * - At least one provided parameter and no duplicates
187 | */
188 | private fun InflationInjectElements.toAssistedInjectionOrNull(): AssistedInjection? {
189 | var valid = true
190 |
191 | val requests = targetConstructor.parameters.map { it.asDependencyRequest() }
192 | val (assistedRequests, providedRequests) = requests.partition { it.isAssisted }
193 | val assistedKeys = assistedRequests.map { it.key }
194 | if (assistedKeys.toSet() != FACTORY_KEYS.toSet()) {
195 | error("""
196 | Inflation injection requires Context and AttributeSet @Inflated parameters.
197 | Found:
198 | $assistedKeys
199 | Expected:
200 | $FACTORY_KEYS
201 | """.trimIndent(), targetConstructor)
202 | valid = false
203 | }
204 | if (providedRequests.isEmpty()) {
205 | warn("Inflation injection requires at least one non-@Inflated parameter.", targetConstructor)
206 | } else {
207 | val providedDuplicates = providedRequests.groupBy { it.key }.filterValues { it.size > 1 }
208 | if (providedDuplicates.isNotEmpty()) {
209 | error("Duplicate non-@Inflated parameters declared. Forget a qualifier annotation?"
210 | + providedDuplicates.values.flatten().joinToString("\n * ", prefix = "\n * "),
211 | targetConstructor)
212 | valid = false
213 | }
214 | }
215 |
216 | if (!valid) return null
217 |
218 | val targetType = targetType.asType().toTypeName()
219 | val generatedAnnotation = createGeneratedAnnotation(sourceVersion, elements)
220 | return AssistedInjection(targetType, requests, FACTORY, "create", VIEW,
221 | FACTORY_KEYS, generatedAnnotation)
222 | }
223 |
224 | private fun writeInflationInject(elements: InflationInjectElements, injection: AssistedInjection) {
225 | val generatedTypeSpec = injection.brewJava()
226 | .toBuilder()
227 | .addOriginatingElement(elements.targetType)
228 | .build()
229 | JavaFile.builder(injection.generatedType.packageName(), generatedTypeSpec)
230 | .addFileComment("Generated by @InflationInject. Do not modify!")
231 | .build()
232 | .writeTo(filer)
233 | }
234 |
235 | /**
236 | * Find and validate a [TypeElement] of the inflation module by being annotated
237 | * [InflationModule].
238 | */
239 | private fun RoundEnvironment.findInflationModuleTypeElement(): TypeElement? {
240 | val inflationModules = findElementsAnnotatedWith().castEach()
241 | if (inflationModules.size > 1) {
242 | inflationModules.forEach {
243 | error("Multiple @InflationModule-annotated modules found.", it)
244 | }
245 | return null
246 | }
247 |
248 | return inflationModules.singleOrNull()
249 | }
250 |
251 | private fun TypeElement.toInflationModuleElementsOrNull(
252 | inflationInjectElements: List
253 | ): InflationModuleElements? {
254 | if (!hasAnnotation("dagger.Module")) {
255 | error("@InflationModule must also be annotated as a Dagger @Module", this)
256 | return null
257 | }
258 |
259 | val inflationTargetTypes = inflationInjectElements.map { it.targetType }
260 | return InflationModuleElements(this, inflationTargetTypes)
261 | }
262 |
263 | private fun InflationModuleElements.toInflationInjectionModule(): InflationInjectionModule {
264 | val moduleName = moduleType.toClassName()
265 | val inflationNames = inflationTypes.map { it.toClassName() }
266 | val public = Modifier.PUBLIC in moduleType.modifiers
267 | val generatedAnnotation = createGeneratedAnnotation(sourceVersion, elements)
268 | return InflationInjectionModule(moduleName, public, inflationNames, generatedAnnotation)
269 | }
270 |
271 | private fun writeInflationModule(
272 | elements: InflationModuleElements,
273 | module: InflationInjectionModule
274 | ) {
275 | val generatedTypeSpec = module.brewJava()
276 | .toBuilder()
277 | .addOriginatingElement(elements.moduleType)
278 | .applyEach(elements.inflationTypes) {
279 | addOriginatingElement(it)
280 | }
281 | .build()
282 | JavaFile.builder(module.generatedType.packageName(), generatedTypeSpec)
283 | .addFileComment("Generated by @InflationModule. Do not modify!")
284 | .build()
285 | .writeTo(filer)
286 | }
287 |
288 | private fun warn(message: String, element: Element? = null) {
289 | messager.printMessage(WARNING, message, element)
290 | }
291 |
292 | private fun error(message: String, element: Element? = null) {
293 | messager.printMessage(ERROR, message, element)
294 | }
295 |
296 | private data class InflationInjectElements(
297 | val targetType: TypeElement,
298 | val targetConstructor: ExecutableElement
299 | )
300 |
301 | private data class InflationModuleElements(
302 | val moduleType: TypeElement,
303 | val inflationTypes: List
304 | )
305 | }
306 |
307 | private val VIEW = ClassName.get("android.view", "View")
308 | private val FACTORY = ViewFactory::class.toClassName()
309 | private val FACTORY_KEYS = listOf(
310 | Key(ClassName.get("android.content", "Context")),
311 | Key(ClassName.get("android.util", "AttributeSet")))
312 |
--------------------------------------------------------------------------------
/inflation-inject-processor/src/test/java/app/cash/inject/inflation/processor/InflationInjectProcessorTest.kt:
--------------------------------------------------------------------------------
1 | package app.cash.inject.inflation.processor
2 |
3 | import com.google.common.truth.Truth.assertAbout
4 | import com.google.testing.compile.JavaFileObjects
5 | import com.google.testing.compile.JavaSourceSubjectFactory.javaSource
6 | import com.google.testing.compile.JavaSourcesSubjectFactory.javaSources
7 | import org.junit.Ignore
8 | import org.junit.Test
9 |
10 | private val GENERATED_TYPE = try {
11 | Class.forName("javax.annotation.processing.Generated")
12 | "javax.annotation.processing.Generated"
13 | } catch (_: ClassNotFoundException) {
14 | "javax.annotation.Generated"
15 | }
16 |
17 | private const val GENERATED_ANNOTATION = """
18 | @Generated(
19 | value = "app.cash.inject.inflation.processor.InflationInjectProcessor",
20 | comments = "https://github.com/cashapp/InflationInject"
21 | )
22 | """
23 |
24 | class InflationInjectProcessorTest {
25 | @Test fun simple() {
26 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
27 | package test;
28 |
29 | import android.content.Context;
30 | import android.util.AttributeSet;
31 | import android.view.View;
32 | import app.cash.inject.inflation.Inflated;
33 | import app.cash.inject.inflation.InflationInject;
34 |
35 | class TestView extends View {
36 | @InflationInject
37 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
38 | super(context, attrs);
39 | }
40 | }
41 | """)
42 | val inputModule = JavaFileObjects.forSourceString("test.TestModule", """
43 | package test;
44 |
45 | import app.cash.inject.inflation.InflationModule;
46 | import dagger.Module;
47 |
48 | @InflationModule
49 | @Module(includes = InflationInject_TestModule.class)
50 | abstract class TestModule {}
51 | """)
52 |
53 | val expectedFactory = JavaFileObjects.forSourceString("test.TestView_InflationFactory", """
54 | package test;
55 |
56 | import android.content.Context;
57 | import android.util.AttributeSet;
58 | import android.view.View;
59 | import app.cash.inject.inflation.ViewFactory;
60 | import java.lang.Long;
61 | import java.lang.Override;
62 | import $GENERATED_TYPE;
63 | import javax.inject.Inject;
64 | import javax.inject.Provider;
65 |
66 | $GENERATED_ANNOTATION
67 | public final class TestView_InflationFactory implements ViewFactory {
68 | private final Provider foo;
69 |
70 | @Inject public Test_InflationFactory(Provider foo) {
71 | this.foo = foo;
72 | }
73 |
74 | @Override public View create(Context context, AttributeSet attrs) {
75 | return new TestView(context, attrs, foo.get());
76 | }
77 | }
78 | """)
79 | val expectedModule = JavaFileObjects.forSourceString("test.InflationModule_TestModule", """
80 | package test;
81 |
82 | import app.cash.inject.inflation.ViewFactory;
83 | import dagger.Binds;
84 | import dagger.Module;
85 | import dagger.multibindings.IntoMap;
86 | import dagger.multibindings.StringKey;
87 | import $GENERATED_TYPE;
88 |
89 | @Module
90 | $GENERATED_ANNOTATION
91 | abstract class InflationInject_TestModule {
92 | private InflationInject_TestModule() {}
93 |
94 | @Binds
95 | @IntoMap
96 | @StringKey("test.TestView")
97 | abstract ViewFactory bind_test_TestView(TestView_InflationFactory factory);
98 | }
99 | """)
100 |
101 | assertAbout(javaSources())
102 | .that(listOf(inputView, inputModule))
103 | .processedWith(InflationInjectProcessor())
104 | .compilesWithoutError()
105 | .and()
106 | .generatesSources(expectedFactory, expectedModule)
107 | }
108 |
109 | @Test fun injectDaggerAssistedFactoryDoesNotUseProvider() {
110 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
111 | package test;
112 |
113 | import android.content.Context;
114 | import android.util.AttributeSet;
115 | import android.view.View;
116 | import dagger.assisted.AssistedFactory;
117 | import app.cash.inject.inflation.Inflated;
118 | import app.cash.inject.inflation.InflationInject;
119 |
120 | class TestView extends View {
121 | @InflationInject
122 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, Other.Factory foo) {
123 | super(context, attrs);
124 | }
125 | }
126 |
127 | class Other {
128 | Other(String a, String b) {}
129 |
130 | @AssistedFactory
131 | interface Factory {
132 | Other create(String b);
133 | }
134 | }
135 | """)
136 | val inputModule = JavaFileObjects.forSourceString("test.TestModule", """
137 | package test;
138 |
139 | import app.cash.inject.inflation.InflationModule;
140 | import dagger.Module;
141 |
142 | @InflationModule
143 | @Module(includes = InflationInject_TestModule.class)
144 | abstract class TestModule {}
145 | """)
146 |
147 | val expectedFactory = JavaFileObjects.forSourceString("test.TestView_InflationFactory", """
148 | package test;
149 |
150 | import android.content.Context;
151 | import android.util.AttributeSet;
152 | import android.view.View;
153 | import app.cash.inject.inflation.ViewFactory;
154 | import java.lang.Override;
155 | import $GENERATED_TYPE;
156 | import javax.inject.Inject;
157 |
158 | $GENERATED_ANNOTATION
159 | public final class TestView_InflationFactory implements ViewFactory {
160 | private final Other.Factory foo;
161 |
162 | @Inject public TestView_InflationFactory(Other.Factory foo) {
163 | this.foo = foo;
164 | }
165 |
166 | @Override public View create(Context context, AttributeSet attrs) {
167 | return new TestView(context, attrs, foo);
168 | }
169 | }
170 | """)
171 | val expectedModule = JavaFileObjects.forSourceString("test.InflationModule_TestModule", """
172 | package test;
173 |
174 | import app.cash.inject.inflation.ViewFactory;
175 | import dagger.Binds;
176 | import dagger.Module;
177 | import dagger.multibindings.IntoMap;
178 | import dagger.multibindings.StringKey;
179 | import $GENERATED_TYPE;
180 |
181 | @Module
182 | $GENERATED_ANNOTATION
183 | abstract class InflationInject_TestModule {
184 | private InflationInject_TestModule() {}
185 |
186 | @Binds
187 | @IntoMap
188 | @StringKey("test.TestView")
189 | abstract ViewFactory bind_test_TestView(TestView_InflationFactory factory);
190 | }
191 | """)
192 |
193 | assertAbout(javaSources())
194 | .that(listOf(inputView, inputModule))
195 | .processedWith(InflationInjectProcessor())
196 | .compilesWithoutError()
197 | .and()
198 | .generatesSources(expectedFactory, expectedModule)
199 | }
200 |
201 | @Test fun public() {
202 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
203 | package test;
204 |
205 | import android.content.Context;
206 | import android.util.AttributeSet;
207 | import android.view.View;
208 | import app.cash.inject.inflation.Inflated;
209 | import app.cash.inject.inflation.InflationInject;
210 |
211 | class TestView extends View {
212 | @InflationInject
213 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
214 | super(context, attrs);
215 | }
216 | }
217 | """)
218 | val inputModule = JavaFileObjects.forSourceString("test.TestModule", """
219 | package test;
220 |
221 | import app.cash.inject.inflation.InflationModule;
222 | import dagger.Module;
223 |
224 | @InflationModule
225 | @Module(includes = InflationInject_TestModule.class)
226 | public abstract class TestModule {}
227 | """)
228 |
229 | val expectedModule = JavaFileObjects.forSourceString("test.InflationModule_TestModule", """
230 | package test;
231 |
232 | import app.cash.inject.inflation.ViewFactory;
233 | import dagger.Binds;
234 | import dagger.Module;
235 | import dagger.multibindings.IntoMap;
236 | import dagger.multibindings.StringKey;
237 | import $GENERATED_TYPE;
238 |
239 | @Module
240 | $GENERATED_ANNOTATION
241 | public abstract class InflationInject_TestModule {
242 | private InflationInject_TestModule() {}
243 |
244 | @Binds
245 | @IntoMap
246 | @StringKey("test.TestView")
247 | abstract ViewFactory bind_test_TestView(TestView_InflationFactory factory);
248 | }
249 | """)
250 |
251 | assertAbout(javaSources())
252 | .that(listOf(inputView, inputModule))
253 | .processedWith(InflationInjectProcessor())
254 | .compilesWithoutError()
255 | .and()
256 | .generatesSources(expectedModule)
257 | }
258 |
259 | @Test fun nested() {
260 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
261 | package test;
262 |
263 | import android.content.Context;
264 | import android.util.AttributeSet;
265 | import android.view.View;
266 | import app.cash.inject.inflation.Inflated;
267 | import app.cash.inject.inflation.InflationInject;
268 |
269 | class Outer {
270 | static class TestView extends View {
271 | @InflationInject
272 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
273 | super(context, attrs);
274 | }
275 | }
276 | }
277 | """)
278 | val inputModule = JavaFileObjects.forSourceString("test.TestModule", """
279 | package test;
280 |
281 | import app.cash.inject.inflation.InflationModule;
282 | import dagger.Module;
283 |
284 | @InflationModule
285 | @Module(includes = InflationInject_TestModule.class)
286 | abstract class TestModule {}
287 | """)
288 |
289 | val expectedFactory = JavaFileObjects.forSourceString("test.TestView_InflationFactory", """
290 | package test;
291 |
292 | import android.content.Context;
293 | import android.util.AttributeSet;
294 | import android.view.View;
295 | import app.cash.inject.inflation.ViewFactory;
296 | import java.lang.Long;
297 | import java.lang.Override;
298 | import $GENERATED_TYPE;
299 | import javax.inject.Inject;
300 | import javax.inject.Provider;
301 |
302 | $GENERATED_ANNOTATION
303 | public final class Outer${'$'}TestView_InflationFactory implements ViewFactory {
304 | private final Provider foo;
305 |
306 | @Inject public Test_InflationFactory(Provider foo) {
307 | this.foo = foo;
308 | }
309 |
310 | @Override public View create(Context context, AttributeSet attrs) {
311 | return new Outer.TestView(context, attrs, foo.get());
312 | }
313 | }
314 | """)
315 | val expectedModule = JavaFileObjects.forSourceString("test.InflationModule_TestModule", """
316 | package test;
317 |
318 | import app.cash.inject.inflation.ViewFactory;
319 | import dagger.Binds;
320 | import dagger.Module;
321 | import dagger.multibindings.IntoMap;
322 | import dagger.multibindings.StringKey;
323 | import $GENERATED_TYPE;
324 |
325 | @Module
326 | $GENERATED_ANNOTATION
327 | abstract class InflationInject_TestModule {
328 | private InflationInject_TestModule() {}
329 |
330 | @Binds
331 | @IntoMap
332 | @StringKey("test.Outer${'$'}TestView")
333 | abstract ViewFactory bind_test_Outer${'$'}TestView(Outer${'$'}TestView_InflationFactory factory);
334 | }
335 | """)
336 |
337 | assertAbout(javaSources())
338 | .that(listOf(inputView, inputModule))
339 | .processedWith(InflationInjectProcessor())
340 | .compilesWithoutError()
341 | .and()
342 | .generatesSources(expectedFactory, expectedModule)
343 | }
344 |
345 | @Ignore("Not handled properly. https://github.com/cashapp/InflationInject/issues/64")
346 | @Test fun parameterized() {
347 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
348 | package test;
349 |
350 | import android.content.Context;
351 | import android.util.AttributeSet;
352 | import android.view.View;
353 | import app.cash.inject.inflation.Inflated;
354 | import app.cash.inject.inflation.InflationInject;
355 |
356 | class TestView extends View {
357 | @InflationInject
358 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
359 | super(context, attrs);
360 | }
361 | }
362 | """)
363 | val inputModule = JavaFileObjects.forSourceString("test.TestModule", """
364 | package test;
365 |
366 | import app.cash.inject.inflation.InflationModule;
367 | import dagger.Module;
368 |
369 | @InflationModule
370 | @Module(includes = InflationInject_TestModule.class)
371 | abstract class TestModule {}
372 | """)
373 |
374 | val expectedFactory = JavaFileObjects.forSourceString("test.TestView_InflationFactory", """
375 | package test;
376 |
377 | import android.content.Context;
378 | import android.util.AttributeSet;
379 | import android.view.View;
380 | import app.cash.inject.inflation.ViewFactory;
381 | import java.lang.Long;
382 | import java.lang.Override;
383 | import $GENERATED_TYPE;
384 | import javax.inject.Inject;
385 | import javax.inject.Provider;
386 |
387 | $GENERATED_ANNOTATION
388 | public final class TestView_InflationFactory implements ViewFactory {
389 | private final Provider foo;
390 |
391 | @Inject public Test_InflationFactory(Provider foo) {
392 | this.foo = foo;
393 | }
394 |
395 | @Override public View create(Context context, AttributeSet attrs) {
396 | return new TestView>(context, attrs, foo.get());
397 | }
398 | }
399 | """)
400 | val expectedModule = JavaFileObjects.forSourceString("test.InflationModule_TestModule", """
401 | package test;
402 |
403 | import app.cash.inject.inflation.ViewFactory;
404 | import dagger.Binds;
405 | import dagger.Module;
406 | import dagger.multibindings.IntoMap;
407 | import dagger.multibindings.StringKey;
408 | import $GENERATED_TYPE;
409 |
410 | @Module
411 | $GENERATED_ANNOTATION
412 | abstract class InflationInject_TestModule {
413 | private InflationInject_TestModule() {}
414 |
415 | @Binds
416 | @IntoMap
417 | @StringKey("test.TestView")
418 | abstract ViewFactory bind_test_TestView(TestView_InflationFactory factory);
419 | }
420 | """)
421 |
422 | assertAbout(javaSources())
423 | .that(listOf(inputView, inputModule))
424 | .processedWith(InflationInjectProcessor())
425 | .compilesWithoutError()
426 | .and()
427 | .generatesSources(expectedFactory, expectedModule)
428 | }
429 |
430 |
431 | @Test fun assistedParametersLast() {
432 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
433 | package test;
434 |
435 | import android.content.Context;
436 | import android.util.AttributeSet;
437 | import android.view.View;
438 | import app.cash.inject.inflation.Inflated;
439 | import app.cash.inject.inflation.InflationInject;
440 |
441 | class TestView extends View {
442 | @InflationInject
443 | TestView(Long foo, @Inflated Context context, @Inflated AttributeSet attrs) {
444 | super(context, attrs);
445 | }
446 | }
447 | """)
448 |
449 | val expectedFactory = JavaFileObjects.forSourceString("test.TestView_InflationFactory", """
450 | package test;
451 |
452 | import android.content.Context;
453 | import android.util.AttributeSet;
454 | import android.view.View;
455 | import app.cash.inject.inflation.ViewFactory;
456 | import java.lang.Long;
457 | import java.lang.Override;
458 | import $GENERATED_TYPE;
459 | import javax.inject.Inject;
460 | import javax.inject.Provider;
461 |
462 | $GENERATED_ANNOTATION
463 | public final class TestView_InflationFactory implements ViewFactory {
464 | private final Provider foo;
465 |
466 | @Inject public Test_InflationFactory(Provider foo) {
467 | this.foo = foo;
468 | }
469 |
470 | @Override public View create(Context context, AttributeSet attrs) {
471 | return new TestView(foo.get(), context, attrs);
472 | }
473 | }
474 | """)
475 |
476 | assertAbout(javaSource())
477 | .that(inputView)
478 | .processedWith(InflationInjectProcessor())
479 | .compilesWithoutError()
480 | .and()
481 | .generatesSources(expectedFactory)
482 | }
483 |
484 | @Test fun contextAndAttributeSetSwapped() {
485 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
486 | package test;
487 |
488 | import android.content.Context;
489 | import android.util.AttributeSet;
490 | import android.view.View;
491 | import app.cash.inject.inflation.Inflated;
492 | import app.cash.inject.inflation.InflationInject;
493 |
494 | class TestView extends View {
495 | @InflationInject
496 | TestView(@Inflated AttributeSet attrs, @Inflated Context context, Long foo) {
497 | super(context, attrs);
498 | }
499 | }
500 | """)
501 |
502 | val expectedFactory = JavaFileObjects.forSourceString("test.TestView_InflationFactory", """
503 | package test;
504 |
505 | import android.content.Context;
506 | import android.util.AttributeSet;
507 | import android.view.View;
508 | import app.cash.inject.inflation.ViewFactory;
509 | import java.lang.Long;
510 | import java.lang.Override;
511 | import $GENERATED_TYPE;
512 | import javax.inject.Inject;
513 | import javax.inject.Provider;
514 |
515 | $GENERATED_ANNOTATION
516 | public final class TestView_InflationFactory implements ViewFactory {
517 | private final Provider foo;
518 |
519 | @Inject public Test_InflationFactory(Provider foo) {
520 | this.foo = foo;
521 | }
522 |
523 | @Override public View create(Context context, AttributeSet attrs) {
524 | return new TestView(attrs, context, foo.get());
525 | }
526 | }
527 | """)
528 |
529 | assertAbout(javaSource())
530 | .that(inputView)
531 | .processedWith(InflationInjectProcessor())
532 | .compilesWithoutError()
533 | .and()
534 | .generatesSources(expectedFactory)
535 | }
536 |
537 | @Test fun typeDoesNotExtendView() {
538 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
539 | package test;
540 |
541 | import android.content.Context;
542 | import android.util.AttributeSet;
543 | import app.cash.inject.inflation.Inflated;
544 | import app.cash.inject.inflation.InflationInject;
545 |
546 | class TestView {
547 | @InflationInject
548 | TestView(@Inflated AttributeSet attrs, @Inflated Context context, Long foo) {
549 | super(context, attrs);
550 | }
551 | }
552 | """)
553 |
554 | assertAbout(javaSource())
555 | .that(inputView)
556 | .processedWith(InflationInjectProcessor())
557 | .failsToCompile()
558 | .withErrorContaining("@InflationInject-using types must be subtypes of View")
559 | .`in`(inputView).onLine(9)
560 | }
561 |
562 | @Test fun typeExtendsViewSubclass() {
563 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
564 | package test;
565 |
566 | import android.content.Context;
567 | import android.util.AttributeSet;
568 | import android.widget.LinearLayout;
569 | import app.cash.inject.inflation.Inflated;
570 | import app.cash.inject.inflation.InflationInject;
571 |
572 | class TestView extends LinearLayout {
573 | @InflationInject
574 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
575 | super(context, attrs);
576 | }
577 | }
578 | """)
579 |
580 | val expectedFactory = JavaFileObjects.forSourceString("test.TestView_InflationFactory", """
581 | package test;
582 |
583 | import android.content.Context;
584 | import android.util.AttributeSet;
585 | import android.view.View;
586 | import app.cash.inject.inflation.ViewFactory;
587 | import java.lang.Long;
588 | import java.lang.Override;
589 | import $GENERATED_TYPE;
590 | import javax.inject.Inject;
591 | import javax.inject.Provider;
592 |
593 | $GENERATED_ANNOTATION
594 | public final class TestView_InflationFactory implements ViewFactory {
595 | private final Provider foo;
596 |
597 | @Inject public Test_InflationFactory(Provider foo) {
598 | this.foo = foo;
599 | }
600 |
601 | @Override public View create(Context context, AttributeSet attrs) {
602 | return new TestView(context, attrs, foo.get());
603 | }
604 | }
605 | """)
606 |
607 | assertAbout(javaSource())
608 | .that(inputView)
609 | .processedWith(InflationInjectProcessor())
610 | .compilesWithoutError()
611 | .and()
612 | .generatesSources(expectedFactory)
613 | }
614 |
615 | @Test fun baseAndSubtypeInjection() {
616 | val longView = JavaFileObjects.forSourceString("test.LongView", """
617 | package test;
618 |
619 | import android.content.Context;
620 | import android.util.AttributeSet;
621 | import android.widget.LinearLayout;
622 | import app.cash.inject.inflation.Inflated;
623 | import app.cash.inject.inflation.InflationInject;
624 |
625 | class LongView extends LinearLayout {
626 | @InflationInject
627 | LongView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
628 | super(context, attrs);
629 | }
630 | }
631 | """)
632 | val stringView = JavaFileObjects.forSourceString("test.StringView", """
633 | package test;
634 |
635 | import android.content.Context;
636 | import android.util.AttributeSet;
637 | import android.widget.LinearLayout;
638 | import app.cash.inject.inflation.Inflated;
639 | import app.cash.inject.inflation.InflationInject;
640 |
641 | class StringView extends LongView {
642 | @InflationInject
643 | StringView(@Inflated Context context, @Inflated AttributeSet attrs, String foo) {
644 | super(context, attrs, Long.parseLong(foo));
645 | }
646 | }
647 | """)
648 |
649 | val expectedLongFactory = JavaFileObjects.forSourceString("test.LongView_InflationFactory", """
650 | package test;
651 |
652 | import android.content.Context;
653 | import android.util.AttributeSet;
654 | import android.view.View;
655 | import app.cash.inject.inflation.ViewFactory;
656 | import java.lang.Long;
657 | import java.lang.Override;
658 | import $GENERATED_TYPE;
659 | import javax.inject.Inject;
660 | import javax.inject.Provider;
661 |
662 | $GENERATED_ANNOTATION
663 | public final class LongView_InflationFactory implements ViewFactory {
664 | private final Provider foo;
665 |
666 | @Inject public LongView_InflationFactory(Provider foo) {
667 | this.foo = foo;
668 | }
669 |
670 | @Override public View create(Context context, AttributeSet attrs) {
671 | return new LongView(context, attrs, foo.get());
672 | }
673 | }
674 | """)
675 | val expectedStringFactory = JavaFileObjects.forSourceString("test.StringView_InflationFactory", """
676 | package test;
677 |
678 | import android.content.Context;
679 | import android.util.AttributeSet;
680 | import android.view.View;
681 | import app.cash.inject.inflation.ViewFactory;
682 | import java.lang.Override;
683 | import java.lang.String;
684 | import $GENERATED_TYPE;
685 | import javax.inject.Inject;
686 | import javax.inject.Provider;
687 |
688 | $GENERATED_ANNOTATION
689 | public final class StringView_InflationFactory implements ViewFactory {
690 | private final Provider foo;
691 |
692 | @Inject public LongView_InflationFactory(Provider foo) {
693 | this.foo = foo;
694 | }
695 |
696 | @Override public View create(Context context, AttributeSet attrs) {
697 | return new StringView(context, attrs, foo.get());
698 | }
699 | }
700 | """)
701 |
702 | assertAbout(javaSources())
703 | .that(listOf(longView, stringView))
704 | .processedWith(InflationInjectProcessor())
705 | .compilesWithoutError()
706 | .and()
707 | .generatesSources(expectedLongFactory, expectedStringFactory)
708 | }
709 |
710 |
711 | @Test fun constructorMissingAssistedParametersFails() {
712 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
713 | package test;
714 |
715 | import android.view.View;
716 | import app.cash.inject.inflation.InflationInject;
717 |
718 | class TestView extends View {
719 | @InflationInject
720 | TestView(Long foo) {
721 | super(null);
722 | }
723 | }
724 | """)
725 |
726 | assertAbout(javaSource())
727 | .that(inputView)
728 | .processedWith(InflationInjectProcessor())
729 | .failsToCompile()
730 | .withErrorContaining("""
731 | Inflation injection requires Context and AttributeSet @Inflated parameters.
732 | Found:
733 | []
734 | Expected:
735 | [android.content.Context, android.util.AttributeSet]
736 | """.trimIndent())
737 | .`in`(inputView).onLine(9)
738 | }
739 |
740 | @Test fun constructorExtraAssistedParameterFails() {
741 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
742 | package test;
743 |
744 | import android.content.Context;
745 | import android.util.AttributeSet;
746 | import android.view.View;
747 | import app.cash.inject.inflation.Inflated;
748 | import app.cash.inject.inflation.InflationInject;
749 |
750 | class TestView extends View {
751 | @InflationInject
752 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, @Inflated String hey, Long foo) {
753 | super(context, attrs);
754 | }
755 | }
756 | """)
757 |
758 | assertAbout(javaSource())
759 | .that(inputView)
760 | .processedWith(InflationInjectProcessor())
761 | .failsToCompile()
762 | .withErrorContaining("""
763 | Inflation injection requires Context and AttributeSet @Inflated parameters.
764 | Found:
765 | [android.content.Context, android.util.AttributeSet, java.lang.String]
766 | Expected:
767 | [android.content.Context, android.util.AttributeSet]
768 | """.trimIndent())
769 | .`in`(inputView).onLine(12)
770 | }
771 |
772 | @Test fun constructorMissingContextFails() {
773 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
774 | package test;
775 |
776 | import android.util.AttributeSet;
777 | import android.view.View;
778 | import app.cash.inject.inflation.Inflated;
779 | import app.cash.inject.inflation.InflationInject;
780 |
781 | class TestView extends View {
782 | @InflationInject
783 | TestView(@Inflated AttributeSet attrs, Long foo) {
784 | super(null, attrs);
785 | }
786 | }
787 | """)
788 |
789 | assertAbout(javaSource())
790 | .that(inputView)
791 | .processedWith(InflationInjectProcessor())
792 | .failsToCompile()
793 | .withErrorContaining("""
794 | Inflation injection requires Context and AttributeSet @Inflated parameters.
795 | Found:
796 | [android.util.AttributeSet]
797 | Expected:
798 | [android.content.Context, android.util.AttributeSet]
799 | """.trimIndent())
800 | .`in`(inputView).onLine(11)
801 | }
802 |
803 | @Test fun constructorMissingAttributeSetFails() {
804 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
805 | package test;
806 |
807 | import android.content.Context;
808 | import android.view.View;
809 | import app.cash.inject.inflation.Inflated;
810 | import app.cash.inject.inflation.InflationInject;
811 |
812 | class TestView extends View {
813 | @InflationInject
814 | TestView(@Inflated Context context, Long foo) {
815 | super(context, null);
816 | }
817 | }
818 | """)
819 |
820 | assertAbout(javaSource())
821 | .that(inputView)
822 | .processedWith(InflationInjectProcessor())
823 | .failsToCompile()
824 | .withErrorContaining("""
825 | Inflation injection requires Context and AttributeSet @Inflated parameters.
826 | Found:
827 | [android.content.Context]
828 | Expected:
829 | [android.content.Context, android.util.AttributeSet]
830 | """.trimIndent())
831 | .`in`(inputView).onLine(11)
832 | }
833 |
834 | @Test fun constructorMissingProvidedParametersWarns() {
835 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
836 | package test;
837 |
838 | import android.content.Context;
839 | import android.util.AttributeSet;
840 | import android.view.View;
841 | import app.cash.inject.inflation.Inflated;
842 | import app.cash.inject.inflation.InflationInject;
843 |
844 | class TestView extends View {
845 | @InflationInject
846 | TestView(@Inflated Context context, @Inflated AttributeSet attrs) {
847 | super(context, attrs);
848 | }
849 | }
850 | """)
851 |
852 | assertAbout(javaSource())
853 | .that(inputView)
854 | .processedWith(InflationInjectProcessor())
855 | .compilesWithoutError()
856 | .withWarningContaining("Inflation injection requires at least one non-@Inflated parameter.")
857 | .`in`(inputView).onLine(12)
858 | // .and().generatesNoFiles()
859 | }
860 |
861 | @Test fun privateConstructorFails() {
862 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
863 | package test;
864 |
865 | import android.view.View;
866 | import app.cash.inject.inflation.Inflated;
867 | import app.cash.inject.inflation.InflationInject;
868 |
869 | class TestView extends View {
870 | @InflationInject
871 | private TestView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
872 | super(context, attrs);
873 | }
874 | }
875 | """)
876 |
877 | assertAbout(javaSource())
878 | .that(inputView)
879 | .processedWith(InflationInjectProcessor())
880 | .failsToCompile()
881 | .withErrorContaining("@InflationInject constructor must not be private.")
882 | .`in`(inputView).onLine(10)
883 | }
884 |
885 | @Test fun nestedPrivateTypeFails() {
886 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
887 | package test;
888 |
889 | import android.view.View;
890 | import app.cash.inject.inflation.Inflated;
891 | import app.cash.inject.inflation.InflationInject;
892 |
893 | class Outer {
894 | private static class TestView extends View {
895 | @InflationInject
896 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
897 | super(context, attrs);
898 | }
899 | }
900 | }
901 | """)
902 |
903 | assertAbout(javaSource())
904 | .that(inputView)
905 | .processedWith(InflationInjectProcessor())
906 | .failsToCompile()
907 | .withErrorContaining("@InflationInject-using types must not be private")
908 | .`in`(inputView).onLine(9)
909 | }
910 |
911 | @Test fun nestedNonStaticFails() {
912 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
913 | package test;
914 |
915 | import android.view.View;
916 | import app.cash.inject.inflation.Inflated;
917 | import app.cash.inject.inflation.InflationInject;
918 |
919 | class Outer {
920 | class TestView extends View {
921 | @InflationInject
922 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
923 | super(context, attrs);
924 | }
925 | }
926 | }
927 | """)
928 |
929 | assertAbout(javaSource())
930 | .that(inputView)
931 | .processedWith(InflationInjectProcessor())
932 | .failsToCompile()
933 | .withErrorContaining("Nested @InflationInject-using types must be static")
934 | .`in`(inputView).onLine(9)
935 | }
936 |
937 | @Test fun multipleInflationInjectConstructorsFails() {
938 | val inputView = JavaFileObjects.forSourceString("test.TestView", """
939 | package test;
940 |
941 | import android.view.View;
942 | import app.cash.inject.inflation.Inflated;
943 | import app.cash.inject.inflation.InflationInject;
944 |
945 | class TestView extends View {
946 | @InflationInject
947 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
948 | super(context, attrs);
949 | }
950 |
951 | @InflationInject
952 | TestView(@Inflated Context context, @Inflated AttributeSet attrs, String foo) {
953 | super(context, attrs);
954 | }
955 | }
956 | """)
957 |
958 | assertAbout(javaSource())
959 | .that(inputView)
960 | .processedWith(InflationInjectProcessor())
961 | .failsToCompile()
962 | .withErrorContaining("Multiple @InflationInject-annotated constructors found.")
963 | .`in`(inputView).onLine(8)
964 | }
965 |
966 | @Test fun moduleWithoutModuleAnnotationFails() {
967 | val moduleOne = JavaFileObjects.forSourceString("test.OneModule", """
968 | package test;
969 |
970 | import app.cash.inject.inflation.InflationModule;
971 |
972 | @InflationModule
973 | abstract class OneModule {}
974 | """)
975 |
976 | assertAbout(javaSource())
977 | .that(moduleOne)
978 | .processedWith(InflationInjectProcessor())
979 | .failsToCompile()
980 | .withErrorContaining("@InflationModule must also be annotated as a Dagger @Module")
981 | .`in`(moduleOne).onLine(7)
982 | }
983 |
984 | @Test fun moduleWithNoIncludesFails() {
985 | val moduleOne = JavaFileObjects.forSourceString("test.OneModule", """
986 | package test;
987 |
988 | import app.cash.inject.inflation.InflationModule;
989 | import dagger.Module;
990 |
991 | @InflationModule
992 | @Module
993 | abstract class OneModule {}
994 | """)
995 |
996 | assertAbout(javaSource())
997 | .that(moduleOne)
998 | .processedWith(InflationInjectProcessor())
999 | .failsToCompile()
1000 | .withErrorContaining("@InflationModule's @Module must include InflationInject_OneModule")
1001 | .`in`(moduleOne).onLine(9)
1002 | }
1003 |
1004 | @Test fun moduleWithoutIncludeFails() {
1005 | val moduleOne = JavaFileObjects.forSourceString("test.OneModule", """
1006 | package test;
1007 |
1008 | import app.cash.inject.inflation.InflationModule;
1009 | import dagger.Module;
1010 |
1011 | @InflationModule
1012 | @Module(includes = TwoModule.class)
1013 | abstract class OneModule {}
1014 |
1015 | @Module
1016 | abstract class TwoModule {}
1017 | """)
1018 |
1019 | assertAbout(javaSource())
1020 | .that(moduleOne)
1021 | .processedWith(InflationInjectProcessor())
1022 | .failsToCompile()
1023 | .withErrorContaining("@InflationModule's @Module must include InflationInject_OneModule")
1024 | .`in`(moduleOne).onLine(9)
1025 | }
1026 |
1027 | @Test fun multipleModulesFails() {
1028 | val moduleOne = JavaFileObjects.forSourceString("test.OneModule", """
1029 | package test;
1030 |
1031 | import app.cash.inject.inflation.InflationModule;
1032 | import dagger.Module;
1033 |
1034 | @InflationModule
1035 | @Module(includes = InflationInject_OneModule.class)
1036 | abstract class OneModule {}
1037 | """)
1038 | val moduleTwo = JavaFileObjects.forSourceString("test.TwoModule", """
1039 | package test;
1040 |
1041 | import app.cash.inject.inflation.InflationModule;
1042 | import dagger.Module;
1043 |
1044 | @InflationModule
1045 | @Module(includes = InflationInject_TwoModule.class)
1046 | abstract class TwoModule {}
1047 | """)
1048 |
1049 | assertAbout(javaSources())
1050 | .that(listOf(moduleOne, moduleTwo))
1051 | .processedWith(InflationInjectProcessor())
1052 | .failsToCompile()
1053 | .withErrorContaining("Multiple @InflationModule-annotated modules found.")
1054 | .`in`(moduleOne).onLine(9)
1055 | .and()
1056 | .withErrorContaining("Multiple @InflationModule-annotated modules found.")
1057 | .`in`(moduleTwo).onLine(9)
1058 | }
1059 |
1060 | @Ignore("No easy way to test this")
1061 | @Test fun multipleModulesAcrossRoundsFails() {
1062 | }
1063 |
1064 | @Test fun multipleViewsStableOrder() {
1065 | val inputViewA = JavaFileObjects.forSourceString("test.TestViewA", """
1066 | package test;
1067 |
1068 | import android.content.Context;
1069 | import android.util.AttributeSet;
1070 | import android.view.View;
1071 | import app.cash.inject.inflation.Inflated;
1072 | import app.cash.inject.inflation.InflationInject;
1073 |
1074 | class TestViewA extends View {
1075 | @InflationInject
1076 | TestViewA(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
1077 | super(context, attrs);
1078 | }
1079 | }
1080 | """)
1081 | val inputViewB = JavaFileObjects.forSourceString("test.TestViewA", """
1082 | package test;
1083 |
1084 | import android.content.Context;
1085 | import android.util.AttributeSet;
1086 | import android.view.View;
1087 | import app.cash.inject.inflation.Inflated;
1088 | import app.cash.inject.inflation.InflationInject;
1089 |
1090 | class TestViewB extends View {
1091 | @InflationInject
1092 | TestViewB(@Inflated Context context, @Inflated AttributeSet attrs, Long foo) {
1093 | super(context, attrs);
1094 | }
1095 | }
1096 | """)
1097 | val inputModule = JavaFileObjects.forSourceString("test.TestModule", """
1098 | package test;
1099 |
1100 | import app.cash.inject.inflation.InflationModule;
1101 | import dagger.Module;
1102 |
1103 | @InflationModule
1104 | @Module(includes = InflationInject_TestModule.class)
1105 | abstract class TestModule {}
1106 | """)
1107 |
1108 | val expectedFactoryA = JavaFileObjects.forSourceString("test.TestViewA_InflationFactory", """
1109 | package test;
1110 |
1111 | import android.content.Context;
1112 | import android.util.AttributeSet;
1113 | import android.view.View;
1114 | import app.cash.inject.inflation.ViewFactory;
1115 | import java.lang.Long;
1116 | import java.lang.Override;
1117 | import $GENERATED_TYPE;
1118 | import javax.inject.Inject;
1119 | import javax.inject.Provider;
1120 |
1121 | $GENERATED_ANNOTATION
1122 | public final class TestViewA_InflationFactory implements ViewFactory {
1123 | private final Provider foo;
1124 |
1125 | @Inject public TestViewA_InflationFactory(Provider foo) {
1126 | this.foo = foo;
1127 | }
1128 |
1129 | @Override public View create(Context context, AttributeSet attrs) {
1130 | return new TestViewA(context, attrs, foo.get());
1131 | }
1132 | }
1133 | """)
1134 | val expectedFactoryB = JavaFileObjects.forSourceString("test.TestViewB_InflationFactory", """
1135 | package test;
1136 |
1137 | import android.content.Context;
1138 | import android.util.AttributeSet;
1139 | import android.view.View;
1140 | import app.cash.inject.inflation.ViewFactory;
1141 | import java.lang.Long;
1142 | import java.lang.Override;
1143 | import $GENERATED_TYPE;
1144 | import javax.inject.Inject;
1145 | import javax.inject.Provider;
1146 |
1147 | $GENERATED_ANNOTATION
1148 | public final class TestViewB_InflationFactory implements ViewFactory {
1149 | private final Provider foo;
1150 |
1151 | @Inject public TestViewB_InflationFactory(Provider foo) {
1152 | this.foo = foo;
1153 | }
1154 |
1155 | @Override public View create(Context context, AttributeSet attrs) {
1156 | return new TestViewB(context, attrs, foo.get());
1157 | }
1158 | }
1159 | """)
1160 | val expectedModule = JavaFileObjects.forSourceString("test.InflationModule_TestModule", """
1161 | package test;
1162 |
1163 | import app.cash.inject.inflation.ViewFactory;
1164 | import dagger.Binds;
1165 | import dagger.Module;
1166 | import dagger.multibindings.IntoMap;
1167 | import dagger.multibindings.StringKey;
1168 | import $GENERATED_TYPE;
1169 |
1170 | @Module
1171 | $GENERATED_ANNOTATION
1172 | abstract class InflationInject_TestModule {
1173 | private InflationInject_TestModule() {}
1174 |
1175 | @Binds
1176 | @IntoMap
1177 | @StringKey("test.TestViewA")
1178 | abstract ViewFactory bind_test_TestViewA(TestViewA_InflationFactory factory);
1179 |
1180 | @Binds
1181 | @IntoMap
1182 | @StringKey("test.TestViewB")
1183 | abstract ViewFactory bind_test_TestViewB(TestViewB_InflationFactory factory);
1184 | }
1185 | """)
1186 |
1187 | assertAbout(javaSources())
1188 | .that(listOf(inputViewA, inputViewB, inputModule))
1189 | .processedWith(InflationInjectProcessor())
1190 | .compilesWithoutError()
1191 | .and()
1192 | .generatesSources(expectedFactoryA, expectedFactoryB, expectedModule)
1193 |
1194 | assertAbout(javaSources())
1195 | .that(listOf(inputViewB, inputViewA, inputModule)) // Inputs passed in reverse order.
1196 | .processedWith(InflationInjectProcessor())
1197 | .compilesWithoutError()
1198 | .and()
1199 | .generatesSources(expectedFactoryA, expectedFactoryB, expectedModule)
1200 | }
1201 |
1202 | // TODO module and no inflation injects (what do we do here? bind empty map? fail?)
1203 | }
1204 |
--------------------------------------------------------------------------------