/* * Copyright (C) 2014 The Dagger Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dagger.internal.codegen; import androidx.room.compiler.processing.XProcessingEnv; import androidx.room.compiler.processing.util.Source; import com.google.common.collect.ImmutableMap; import dagger.testing.compile.CompilerTests; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ScopingValidationTest { @Test public void componentWithoutScopeIncludesScopedBindings_Fail() { Source componentFile = CompilerTests.javaSource( "test.MyComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Component(modules = ScopedModule.class)", "interface MyComponent {", " ScopedType string();", "}"); Source typeFile = CompilerTests.javaSource( "test.ScopedType", "package test;", "", "import javax.inject.Inject;", "import javax.inject.Singleton;", "", "@Singleton", "class ScopedType {", " @Inject ScopedType(String s, long l, float f) {}", "}"); Source moduleFile = CompilerTests.javaSource( "test.ScopedModule", "package test;", "", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Singleton;", "", "@Module", "class ScopedModule {", " @Provides @Singleton String string() { return \"a string\"; }", " @Provides long integer() { return 0L; }", " @Provides float floatingPoint() { return 0.0f; }", "}"); CompilerTests.daggerCompiler(componentFile, typeFile, moduleFile) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "MyComponent (unscoped) may not reference scoped bindings:", " @Singleton class ScopedType", " ScopedType is requested at", " MyComponent.string()", "", " @Provides @Singleton String ScopedModule.string()")); }); } @Test // b/79859714 public void bindsWithChildScope_inParentModule_notAllowed() { Source childScope = CompilerTests.javaSource( "test.ChildScope", "package test;", "", "import javax.inject.Scope;", "", "@Scope", "@interface ChildScope {}"); Source foo = CompilerTests.javaSource( "test.Foo", "package test;", "", // "interface Foo {}"); Source fooImpl = CompilerTests.javaSource( "test.FooImpl", "package test;", "", "import javax.inject.Inject;", "", "class FooImpl implements Foo {", " @Inject FooImpl() {}", "}"); Source parentModule = CompilerTests.javaSource( "test.ParentModule", "package test;", "", "import dagger.Binds;", "import dagger.Module;", "", "@Module", "interface ParentModule {", " @Binds @ChildScope Foo bind(FooImpl fooImpl);", "}"); Source parent = CompilerTests.javaSource( "test.Parent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component(modules = ParentModule.class)", "interface Parent {", " Child child();", "}"); Source child = CompilerTests.javaSource( "test.Child", "package test;", "", "import dagger.Subcomponent;", "", "@ChildScope", "@Subcomponent", "interface Child {", " Foo foo();", "}"); CompilerTests.daggerCompiler(childScope, foo, fooImpl, parentModule, parent, child) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "Parent scoped with @Singleton may not reference bindings with different " + "scopes:", " @Binds @ChildScope Foo ParentModule.bind(FooImpl)")); }); } @Test public void componentWithScopeIncludesIncompatiblyScopedBindings_Fail() { Source componentFile = CompilerTests.javaSource( "test.MyComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component(modules = ScopedModule.class)", "interface MyComponent {", " ScopedType string();", "}"); Source scopeFile = CompilerTests.javaSource( "test.PerTest", "package test;", "", "import javax.inject.Scope;", "", "@Scope", "@interface PerTest {}"); Source scopeWithAttribute = CompilerTests.javaSource( "test.Per", "package test;", "", "import javax.inject.Scope;", "", "@Scope", "@interface Per {", " Class value();", "}"); Source typeFile = CompilerTests.javaSource( "test.ScopedType", "package test;", "", "import javax.inject.Inject;", "", "@PerTest", // incompatible scope "class ScopedType {", " @Inject ScopedType(String s, long l, float f, boolean b) {}", "}"); Source moduleFile = CompilerTests.javaSource( "test.ScopedModule", "package test;", "", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Singleton;", "", "@Module", "class ScopedModule {", " @Provides @PerTest String string() { return \"a string\"; }", // incompatible scope " @Provides long integer() { return 0L; }", // unscoped - valid " @Provides @Singleton float floatingPoint() { return 0.0f; }", // same scope - valid " @Provides @Per(MyComponent.class) boolean bool() { return false; }", // incompatible "}"); CompilerTests.daggerCompiler(componentFile, scopeFile, scopeWithAttribute, typeFile, moduleFile) .compile( subject -> { subject.hasErrorCount(1); subject .hasErrorContaining( String.join( "\n", "MyComponent scoped with @Singleton may not reference bindings with " + "different scopes:", " @PerTest class ScopedType", " ScopedType is requested at", " MyComponent.string()", "", " @Provides @PerTest String ScopedModule.string()", "", // TODO(b/241293838): Remove dependency on backend once this bug is fixed. CompilerTests.backend(subject).equals(XProcessingEnv.Backend.JAVAC) ? " @Provides @Per(MyComponent.class) boolean ScopedModule.bool()" : " @Provides @Per(MyComponent) boolean ScopedModule.bool()")) .onSource(componentFile) .onLineContaining("interface MyComponent"); }); // The @Inject binding for ScopedType should not appear here, but the @Singleton binding should. CompilerTests.daggerCompiler(componentFile, scopeFile, scopeWithAttribute, typeFile, moduleFile) .withProcessingOptions(ImmutableMap.of("dagger.fullBindingGraphValidation", "ERROR")) .compile( subject -> { subject.hasErrorCount(2); subject.hasErrorContaining( String.join( "\n", "ScopedModule contains bindings with different scopes:", " @Provides @PerTest String ScopedModule.string()", "", " @Provides @Singleton float ScopedModule.floatingPoint()", "", // TODO(b/241293838): Remove dependency on backend once this bug is fixed. CompilerTests.backend(subject).equals(XProcessingEnv.Backend.JAVAC) ? " @Provides @Per(MyComponent.class) boolean ScopedModule.bool()" : " @Provides @Per(MyComponent) boolean ScopedModule.bool()")) .onSource(moduleFile) .onLineContaining("class ScopedModule"); }); } @Test public void fullBindingGraphValidationDoesNotReportForOneScope() { CompilerTests.daggerCompiler( CompilerTests.javaSource( "test.TestModule", "package test;", "", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Singleton;", "", "@Module", "interface TestModule {", " @Provides @Singleton static Object object() { return \"object\"; }", " @Provides @Singleton static String string() { return \"string\"; }", " @Provides static int integer() { return 4; }", "}")) .withProcessingOptions( ImmutableMap.builder() .put("dagger.fullBindingGraphValidation", "ERROR") .put("dagger.moduleHasDifferentScopesValidation", "ERROR") .buildOrThrow()) .compile( subject -> { subject.hasErrorCount(0); subject.hasWarningCount(0); }); } @Test public void fullBindingGraphValidationDoesNotReportInjectBindings() { CompilerTests.daggerCompiler( CompilerTests.javaSource( "test.UsedInRootRedScoped", "package test;", "", "import javax.inject.Inject;", "", "@RedScope", "final class UsedInRootRedScoped {", " @Inject UsedInRootRedScoped() {}", "}"), CompilerTests.javaSource( "test.UsedInRootBlueScoped", "package test;", "", "import javax.inject.Inject;", "", "@BlueScope", "final class UsedInRootBlueScoped {", " @Inject UsedInRootBlueScoped() {}", "}"), CompilerTests.javaSource( "test.RedScope", "package test;", "", "import javax.inject.Scope;", "", "@Scope", "@interface RedScope {}"), CompilerTests.javaSource( "test.BlueScope", "package test;", "", "import javax.inject.Scope;", "", "@Scope", "@interface BlueScope {}"), CompilerTests.javaSource( "test.TestModule", "package test;", "", "import dagger.Module;", "import dagger.Provides;", "import javax.inject.Singleton;", "", "@Module(subcomponents = Child.class)", "interface TestModule {", " @Provides @Singleton", " static Object object(", " UsedInRootRedScoped usedInRootRedScoped,", " UsedInRootBlueScoped usedInRootBlueScoped) {", " return \"object\";", " }", "}"), CompilerTests.javaSource( "test.Child", "package test;", "", "import dagger.Subcomponent;", "", "@Subcomponent", "interface Child {", " UsedInChildRedScoped usedInChildRedScoped();", " UsedInChildBlueScoped usedInChildBlueScoped();", "", " @Subcomponent.Builder", " interface Builder {", " Child child();", " }", "}"), CompilerTests.javaSource( "test.UsedInChildRedScoped", "package test;", "", "import javax.inject.Inject;", "", "@RedScope", "final class UsedInChildRedScoped {", " @Inject UsedInChildRedScoped() {}", "}"), CompilerTests.javaSource( "test.UsedInChildBlueScoped", "package test;", "", "import javax.inject.Inject;", "", "@BlueScope", "final class UsedInChildBlueScoped {", " @Inject UsedInChildBlueScoped() {}", "}")) .withProcessingOptions( ImmutableMap.builder() .put("dagger.fullBindingGraphValidation", "ERROR") .put("dagger.moduleHasDifferentScopesValidation", "ERROR") .buildOrThrow()) .compile( subject -> { subject.hasErrorCount(0); subject.hasWarningCount(0); }); } @Test public void componentWithScopeCanDependOnMultipleScopedComponents() { // If a scoped component will have dependencies, they can include multiple scoped component Source type = CompilerTests.javaSource( "test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", " static class A { @Inject A() {} }", " static class B { @Inject B() {} }", "}"); Source simpleScope = CompilerTests.javaSource( "test.SimpleScope", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface SimpleScope {}"); Source singletonScopedA = CompilerTests.javaSource( "test.SingletonComponentA", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component", "interface SingletonComponentA {", " SimpleType.A type();", "}"); Source singletonScopedB = CompilerTests.javaSource( "test.SingletonComponentB", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component", "interface SingletonComponentB {", " SimpleType.B type();", "}"); Source scopeless = CompilerTests.javaSource( "test.ScopelessComponent", "package test;", "", "import dagger.Component;", "", "@Component", "interface ScopelessComponent {", " SimpleType type();", "}"); Source simpleScoped = CompilerTests.javaSource( "test.SimpleScopedComponent", "package test;", "", "import dagger.Component;", "", "@SimpleScope", "@Component(dependencies = {SingletonComponentA.class, SingletonComponentB.class})", "interface SimpleScopedComponent {", " SimpleType.A type();", "}"); CompilerTests.daggerCompiler( type, simpleScope, simpleScoped, singletonScopedA, singletonScopedB, scopeless) .compile( subject -> { subject.hasErrorCount(0); subject.hasWarningCount(0); }); } // Tests the following component hierarchy: // // @ScopeA // ComponentA // [SimpleType getSimpleType()] // / \ // / \ // @ScopeB @ScopeB // ComponentB1 ComponentB2 // \ [SimpleType getSimpleType()] // \ / // \ / // @ScopeC // ComponentC // [SimpleType getSimpleType()] @Test public void componentWithScopeCanDependOnMultipleScopedComponentsEvenDoingADiamond() { Source type = CompilerTests.javaSource( "test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", "}"); Source simpleScope = CompilerTests.javaSource( "test.SimpleScope", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface SimpleScope {}"); Source scopeA = CompilerTests.javaSource( "test.ScopeA", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface ScopeA {}"); Source scopeB = CompilerTests.javaSource( "test.ScopeB", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface ScopeB {}"); Source componentA = CompilerTests.javaSource( "test.ComponentA", "package test;", "", "import dagger.Component;", "", "@ScopeA", "@Component", "interface ComponentA {", " SimpleType type();", "}"); Source componentB1 = CompilerTests.javaSource( "test.ComponentB1", "package test;", "", "import dagger.Component;", "", "@ScopeB", "@Component(dependencies = ComponentA.class)", "interface ComponentB1 {", " SimpleType type();", "}"); Source componentB2 = CompilerTests.javaSource( "test.ComponentB2", "package test;", "", "import dagger.Component;", "", "@ScopeB", "@Component(dependencies = ComponentA.class)", "interface ComponentB2 {", "}"); Source componentC = CompilerTests.javaSource( "test.ComponentC", "package test;", "", "import dagger.Component;", "", "@SimpleScope", "@Component(dependencies = {ComponentB1.class, ComponentB2.class})", "interface ComponentC {", " SimpleType type();", "}"); CompilerTests.daggerCompiler( type, simpleScope, scopeA, scopeB, componentA, componentB1, componentB2, componentC) .compile( subject -> { subject.hasErrorCount(0); subject.hasWarningCount(0); }); } @Test public void componentWithoutScopeCannotDependOnScopedComponent() { Source type = CompilerTests.javaSource( "test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", "}"); Source scopedComponent = CompilerTests.javaSource( "test.ScopedComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component", "interface ScopedComponent {", " SimpleType type();", "}"); Source unscopedComponent = CompilerTests.javaSource( "test.UnscopedComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Component(dependencies = ScopedComponent.class)", "interface UnscopedComponent {", " SimpleType type();", "}"); CompilerTests.daggerCompiler(type, scopedComponent, unscopedComponent) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "test.UnscopedComponent (unscoped) cannot depend on scoped components:", " @Singleton test.ScopedComponent")); }); } @Test public void componentWithSingletonScopeMayNotDependOnOtherScope() { // Singleton must be the widest lifetime of present scopes. Source type = CompilerTests.javaSource( "test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", "}"); Source simpleScope = CompilerTests.javaSource( "test.SimpleScope", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface SimpleScope {}"); Source simpleScoped = CompilerTests.javaSource( "test.SimpleScopedComponent", "package test;", "", "import dagger.Component;", "", "@SimpleScope", "@Component", "interface SimpleScopedComponent {", " SimpleType type();", "}"); Source singletonScoped = CompilerTests.javaSource( "test.SingletonComponent", "package test;", "", "import dagger.Component;", "import javax.inject.Singleton;", "", "@Singleton", "@Component(dependencies = SimpleScopedComponent.class)", "interface SingletonComponent {", " SimpleType type();", "}"); CompilerTests.daggerCompiler(type, simpleScope, simpleScoped, singletonScoped) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "This @Singleton component cannot depend on scoped components:", " @test.SimpleScope test.SimpleScopedComponent")); }); } @Test public void componentScopeWithMultipleScopedDependenciesMustNotCycle() { Source type = CompilerTests.javaSource( "test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", "}"); Source scopeA = CompilerTests.javaSource( "test.ScopeA", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface ScopeA {}"); Source scopeB = CompilerTests.javaSource( "test.ScopeB", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface ScopeB {}"); Source longLifetime = CompilerTests.javaSource( "test.ComponentLong", "package test;", "", "import dagger.Component;", "", "@ScopeA", "@Component", "interface ComponentLong {", " SimpleType type();", "}"); Source mediumLifetime1 = CompilerTests.javaSource( "test.ComponentMedium1", "package test;", "", "import dagger.Component;", "", "@ScopeB", "@Component(dependencies = ComponentLong.class)", "interface ComponentMedium1 {", " SimpleType type();", "}"); Source mediumLifetime2 = CompilerTests.javaSource( "test.ComponentMedium2", "package test;", "", "import dagger.Component;", "", "@ScopeB", "@Component", "interface ComponentMedium2 {", "}"); Source shortLifetime = CompilerTests.javaSource( "test.ComponentShort", "package test;", "", "import dagger.Component;", "", "@ScopeA", "@Component(dependencies = {ComponentMedium1.class, ComponentMedium2.class})", "interface ComponentShort {", " SimpleType type();", "}"); CompilerTests.daggerCompiler( type, scopeA, scopeB, longLifetime, mediumLifetime1, mediumLifetime2, shortLifetime) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "test.ComponentShort depends on scoped components in a non-hierarchical " + "scope ordering:", " @test.ScopeA test.ComponentLong", " @test.ScopeB test.ComponentMedium1", " @test.ScopeA test.ComponentShort")); }); } @Test public void componentScopeAncestryMustNotCycle() { // The dependency relationship of components is necessarily from shorter lifetimes to // longer lifetimes. The scoping annotations must reflect this, and so one cannot declare // scopes on components such that they cycle. Source type = CompilerTests.javaSource( "test.SimpleType", "package test;", "", "import javax.inject.Inject;", "", "class SimpleType {", " @Inject SimpleType() {}", "}"); Source scopeA = CompilerTests.javaSource( "test.ScopeA", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface ScopeA {}"); Source scopeB = CompilerTests.javaSource( "test.ScopeB", "package test;", "", "import javax.inject.Scope;", "", "@Scope @interface ScopeB {}"); Source longLifetime = CompilerTests.javaSource( "test.ComponentLong", "package test;", "", "import dagger.Component;", "", "@ScopeA", "@Component", "interface ComponentLong {", " SimpleType type();", "}"); Source mediumLifetime = CompilerTests.javaSource( "test.ComponentMedium", "package test;", "", "import dagger.Component;", "", "@ScopeB", "@Component(dependencies = ComponentLong.class)", "interface ComponentMedium {", " SimpleType type();", "}"); Source shortLifetime = CompilerTests.javaSource( "test.ComponentShort", "package test;", "", "import dagger.Component;", "", "@ScopeA", "@Component(dependencies = ComponentMedium.class)", "interface ComponentShort {", " SimpleType type();", "}"); CompilerTests.daggerCompiler(type, scopeA, scopeB, longLifetime, mediumLifetime, shortLifetime) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( String.join( "\n", "test.ComponentShort depends on scoped components in a non-hierarchical " + "scope ordering:", " @test.ScopeA test.ComponentLong", " @test.ScopeB test.ComponentMedium", " @test.ScopeA test.ComponentShort")); }); // Test that compilation succeeds when transitive validation is disabled because the scope cycle // cannot be detected. CompilerTests.daggerCompiler(type, scopeA, scopeB, longLifetime, mediumLifetime, shortLifetime) .withProcessingOptions( ImmutableMap.of("dagger.validateTransitiveComponentDependencies", "DISABLED")) .compile(subject -> subject.hasErrorCount(0)); } @Test public void reusableNotAllowedOnComponent() { Source someComponent = CompilerTests.javaSource( "test.SomeComponent", "package test;", "", "import dagger.Component;", "import dagger.Reusable;", "", "@Reusable", "@Component", "interface SomeComponent {}"); CompilerTests.daggerCompiler(someComponent) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( "@Reusable cannot be applied to components or subcomponents") .onSource(someComponent) .onLine(6); }); } @Test public void reusableNotAllowedOnSubcomponent() { Source someSubcomponent = CompilerTests.javaSource( "test.SomeComponent", "package test;", "", "import dagger.Reusable;", "import dagger.Subcomponent;", "", "@Reusable", "@Subcomponent", "interface SomeSubcomponent {}"); CompilerTests.daggerCompiler(someSubcomponent) .compile( subject -> { subject.hasErrorCount(1); subject.hasErrorContaining( "@Reusable cannot be applied to components or subcomponents") .onSource(someSubcomponent) .onLine(6); }); } }