blob: 58ab2af449b4e7c36c375f9112e088f5b6865ffb [file] [log] [blame]
/*
* 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 static com.google.testing.compile.CompilationSubject.assertThat;
import static dagger.internal.codegen.Compilers.compilerWithOptions;
import static dagger.internal.codegen.Compilers.daggerCompiler;
import static dagger.internal.codegen.TestUtils.message;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import javax.tools.JavaFileObject;
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() {
JavaFileObject componentFile =
JavaFileObjects.forSourceLines(
"test.MyComponent",
"package test;",
"",
"import dagger.Component;",
"import javax.inject.Singleton;",
"",
"@Component(modules = ScopedModule.class)",
"interface MyComponent {",
" ScopedType string();",
"}");
JavaFileObject typeFile =
JavaFileObjects.forSourceLines(
"test.ScopedType",
"package test;",
"",
"import javax.inject.Inject;",
"import javax.inject.Singleton;",
"",
"@Singleton",
"class ScopedType {",
" @Inject ScopedType(String s, long l, float f) {}",
"}");
JavaFileObject moduleFile =
JavaFileObjects.forSourceLines(
"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; }",
"}");
Compilation compilation = daggerCompiler().compile(componentFile, typeFile, moduleFile);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining(
message(
"MyComponent (unscoped) may not reference scoped bindings:",
" @Singleton class ScopedType",
" @Provides @Singleton String ScopedModule.string()"));
}
@Test // b/79859714
public void bindsWithChildScope_inParentModule_notAllowed() {
JavaFileObject childScope =
JavaFileObjects.forSourceLines(
"test.ChildScope",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope",
"@interface ChildScope {}");
JavaFileObject foo =
JavaFileObjects.forSourceLines(
"test.Foo",
"package test;",
"", //
"interface Foo {}");
JavaFileObject fooImpl =
JavaFileObjects.forSourceLines(
"test.ChildModule",
"package test;",
"",
"import javax.inject.Inject;",
"",
"class FooImpl implements Foo {",
" @Inject FooImpl() {}",
"}");
JavaFileObject parentModule =
JavaFileObjects.forSourceLines(
"test.ParentModule",
"package test;",
"",
"import dagger.Binds;",
"import dagger.Module;",
"",
"@Module",
"interface ParentModule {",
" @Binds @ChildScope Foo bind(FooImpl fooImpl);",
"}");
JavaFileObject parent =
JavaFileObjects.forSourceLines(
"test.ParentComponent",
"package test;",
"",
"import dagger.Component;",
"import javax.inject.Singleton;",
"",
"@Singleton",
"@Component(modules = ParentModule.class)",
"interface Parent {",
" Child child();",
"}");
JavaFileObject child =
JavaFileObjects.forSourceLines(
"test.Child",
"package test;",
"",
"import dagger.Subcomponent;",
"",
"@ChildScope",
"@Subcomponent",
"interface Child {",
" Foo foo();",
"}");
Compilation compilation =
daggerCompiler().compile(childScope, foo, fooImpl, parentModule, parent, child);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining(
message(
"Parent scoped with @Singleton may not reference bindings with different "
+ "scopes:",
" @Binds @ChildScope Foo ParentModule.bind(FooImpl)"));
}
@Test
public void componentWithScopeIncludesIncompatiblyScopedBindings_Fail() {
JavaFileObject componentFile =
JavaFileObjects.forSourceLines(
"test.MyComponent",
"package test;",
"",
"import dagger.Component;",
"import javax.inject.Singleton;",
"",
"@Singleton",
"@Component(modules = ScopedModule.class)",
"interface MyComponent {",
" ScopedType string();",
"}");
JavaFileObject scopeFile =
JavaFileObjects.forSourceLines(
"test.PerTest",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope",
"@interface PerTest {}");
JavaFileObject scopeWithAttribute =
JavaFileObjects.forSourceLines(
"test.Per",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope",
"@interface Per {",
" Class<?> value();",
"}");
JavaFileObject typeFile =
JavaFileObjects.forSourceLines(
"test.ScopedType",
"package test;",
"",
"import javax.inject.Inject;",
"",
"@PerTest", // incompatible scope
"class ScopedType {",
" @Inject ScopedType(String s, long l, float f, boolean b) {}",
"}");
JavaFileObject moduleFile =
JavaFileObjects.forSourceLines(
"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
"}");
Compilation compilation =
daggerCompiler()
.compile(componentFile, scopeFile, scopeWithAttribute, typeFile, moduleFile);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining(
message(
"MyComponent scoped with @Singleton "
+ "may not reference bindings with different scopes:",
" @PerTest class ScopedType",
" @Provides @PerTest String ScopedModule.string()",
" @Provides @Per(MyComponent.class) boolean "
+ "ScopedModule.bool()"))
.inFile(componentFile)
.onLineContaining("interface MyComponent");
compilation =
compilerWithOptions("-Adagger.fullBindingGraphValidation=ERROR")
.compile(componentFile, scopeFile, scopeWithAttribute, typeFile, moduleFile);
// The @Inject binding for ScopedType should not appear here, but the @Singleton binding should.
assertThat(compilation)
.hadErrorContaining(
message(
"ScopedModule contains bindings with different scopes:",
" @Provides @PerTest String ScopedModule.string()",
" @Provides @Singleton float ScopedModule.floatingPoint()",
" @Provides @Per(MyComponent.class) boolean "
+ "ScopedModule.bool()"))
.inFile(moduleFile)
.onLineContaining("class ScopedModule");
}
@Test
public void fullBindingGraphValidationDoesNotReportForOneScope() {
Compilation compilation =
compilerWithOptions(
"-Adagger.fullBindingGraphValidation=ERROR",
"-Adagger.moduleHasDifferentScopesValidation=ERROR")
.compile(
JavaFileObjects.forSourceLines(
"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; }",
"}"));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
public void fullBindingGraphValidationDoesNotReportInjectBindings() {
Compilation compilation =
compilerWithOptions(
"-Adagger.fullBindingGraphValidation=ERROR",
"-Adagger.moduleHasDifferentScopesValidation=ERROR")
.compile(
JavaFileObjects.forSourceLines(
"test.UsedInRootRedScoped",
"package test;",
"",
"import javax.inject.Inject;",
"",
"@RedScope",
"final class UsedInRootRedScoped {",
" @Inject UsedInRootRedScoped() {}",
"}"),
JavaFileObjects.forSourceLines(
"test.UsedInRootBlueScoped",
"package test;",
"",
"import javax.inject.Inject;",
"",
"@BlueScope",
"final class UsedInRootBlueScoped {",
" @Inject UsedInRootBlueScoped() {}",
"}"),
JavaFileObjects.forSourceLines(
"test.RedScope",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope",
"@interface RedScope {}"),
JavaFileObjects.forSourceLines(
"test.BlueScope",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope",
"@interface BlueScope {}"),
JavaFileObjects.forSourceLines(
"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\";",
" }",
"}"),
JavaFileObjects.forSourceLines(
"test.Child",
"package test;",
"",
"import dagger.Subcomponent;",
"",
"@Subcomponent",
"interface Child {",
" UsedInChildRedScoped usedInChildRedScoped();",
" UsedInChildBlueScoped usedInChildBlueScoped();",
"",
" @Subcomponent.Builder",
" interface Builder {",
" Child child();",
" }",
"}"),
JavaFileObjects.forSourceLines(
"test.UsedInChildRedScoped",
"package test;",
"",
"import javax.inject.Inject;",
"",
"@RedScope",
"final class UsedInChildRedScoped {",
" @Inject UsedInChildRedScoped() {}",
"}"),
JavaFileObjects.forSourceLines(
"test.UsedInChildBlueScoped",
"package test;",
"",
"import javax.inject.Inject;",
"",
"@BlueScope",
"final class UsedInChildBlueScoped {",
" @Inject UsedInChildBlueScoped() {}",
"}"));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
public void componentWithScopeCanDependOnMultipleScopedComponents() {
// If a scoped component will have dependencies, they can include multiple scoped component
JavaFileObject type =
JavaFileObjects.forSourceLines(
"test.SimpleType",
"package test;",
"",
"import javax.inject.Inject;",
"",
"class SimpleType {",
" @Inject SimpleType() {}",
" static class A { @Inject A() {} }",
" static class B { @Inject B() {} }",
"}");
JavaFileObject simpleScope =
JavaFileObjects.forSourceLines(
"test.SimpleScope",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope @interface SimpleScope {}");
JavaFileObject singletonScopedA =
JavaFileObjects.forSourceLines(
"test.SingletonComponentA",
"package test;",
"",
"import dagger.Component;",
"import javax.inject.Singleton;",
"",
"@Singleton",
"@Component",
"interface SingletonComponentA {",
" SimpleType.A type();",
"}");
JavaFileObject singletonScopedB =
JavaFileObjects.forSourceLines(
"test.SingletonComponentB",
"package test;",
"",
"import dagger.Component;",
"import javax.inject.Singleton;",
"",
"@Singleton",
"@Component",
"interface SingletonComponentB {",
" SimpleType.B type();",
"}");
JavaFileObject scopeless =
JavaFileObjects.forSourceLines(
"test.ScopelessComponent",
"package test;",
"",
"import dagger.Component;",
"",
"@Component",
"interface ScopelessComponent {",
" SimpleType type();",
"}");
JavaFileObject simpleScoped =
JavaFileObjects.forSourceLines(
"test.SimpleScopedComponent",
"package test;",
"",
"import dagger.Component;",
"",
"@SimpleScope",
"@Component(dependencies = {SingletonComponentA.class, SingletonComponentB.class})",
"interface SimpleScopedComponent {",
" SimpleType.A type();",
"}");
Compilation compilation =
daggerCompiler()
.compile(
type, simpleScope, simpleScoped, singletonScopedA, singletonScopedB, scopeless);
assertThat(compilation).succeededWithoutWarnings();
}
// Tests the following component hierarchy:
//
// @ScopeA
// ComponentA
// [SimpleType getSimpleType()]
// / \
// / \
// @ScopeB @ScopeB
// ComponentB1 ComponentB2
// \ [SimpleType getSimpleType()]
// \ /
// \ /
// @ScopeC
// ComponentC
// [SimpleType getSimpleType()]
@Test
public void componentWithScopeCanDependOnMultipleScopedComponentsEvenDoingADiamond() {
JavaFileObject type =
JavaFileObjects.forSourceLines(
"test.SimpleType",
"package test;",
"",
"import javax.inject.Inject;",
"",
"class SimpleType {",
" @Inject SimpleType() {}",
"}");
JavaFileObject simpleScope =
JavaFileObjects.forSourceLines(
"test.SimpleScope",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope @interface SimpleScope {}");
JavaFileObject scopeA =
JavaFileObjects.forSourceLines(
"test.ScopeA",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope @interface ScopeA {}");
JavaFileObject scopeB =
JavaFileObjects.forSourceLines(
"test.ScopeB",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope @interface ScopeB {}");
JavaFileObject componentA =
JavaFileObjects.forSourceLines(
"test.ComponentA",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeA",
"@Component",
"interface ComponentA {",
" SimpleType type();",
"}");
JavaFileObject componentB1 =
JavaFileObjects.forSourceLines(
"test.ComponentB1",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeB",
"@Component(dependencies = ComponentA.class)",
"interface ComponentB1 {",
" SimpleType type();",
"}");
JavaFileObject componentB2 =
JavaFileObjects.forSourceLines(
"test.ComponentB2",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeB",
"@Component(dependencies = ComponentA.class)",
"interface ComponentB2 {",
"}");
JavaFileObject componentC =
JavaFileObjects.forSourceLines(
"test.ComponentC",
"package test;",
"",
"import dagger.Component;",
"",
"@SimpleScope",
"@Component(dependencies = {ComponentB1.class, ComponentB2.class})",
"interface ComponentC {",
" SimpleType type();",
"}");
Compilation compilation =
daggerCompiler()
.compile(
type, simpleScope, scopeA, scopeB,
componentA, componentB1, componentB2, componentC);
assertThat(compilation).succeededWithoutWarnings();
}
@Test
public void componentWithoutScopeCannotDependOnScopedComponent() {
JavaFileObject type =
JavaFileObjects.forSourceLines(
"test.SimpleType",
"package test;",
"",
"import javax.inject.Inject;",
"",
"class SimpleType {",
" @Inject SimpleType() {}",
"}");
JavaFileObject scopedComponent =
JavaFileObjects.forSourceLines(
"test.ScopedComponent",
"package test;",
"",
"import dagger.Component;",
"import javax.inject.Singleton;",
"",
"@Singleton",
"@Component",
"interface ScopedComponent {",
" SimpleType type();",
"}");
JavaFileObject unscopedComponent =
JavaFileObjects.forSourceLines(
"test.UnscopedComponent",
"package test;",
"",
"import dagger.Component;",
"import javax.inject.Singleton;",
"",
"@Component(dependencies = ScopedComponent.class)",
"interface UnscopedComponent {",
" SimpleType type();",
"}");
Compilation compilation = daggerCompiler().compile(type, scopedComponent, unscopedComponent);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining(
message(
"test.UnscopedComponent (unscoped) cannot depend on scoped components:",
" @Singleton test.ScopedComponent"));
}
@Test
public void componentWithSingletonScopeMayNotDependOnOtherScope() {
// Singleton must be the widest lifetime of present scopes.
JavaFileObject type =
JavaFileObjects.forSourceLines(
"test.SimpleType",
"package test;",
"",
"import javax.inject.Inject;",
"",
"class SimpleType {",
" @Inject SimpleType() {}",
"}");
JavaFileObject simpleScope =
JavaFileObjects.forSourceLines(
"test.SimpleScope",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope @interface SimpleScope {}");
JavaFileObject simpleScoped =
JavaFileObjects.forSourceLines(
"test.SimpleScopedComponent",
"package test;",
"",
"import dagger.Component;",
"",
"@SimpleScope",
"@Component",
"interface SimpleScopedComponent {",
" SimpleType type();",
"}");
JavaFileObject singletonScoped =
JavaFileObjects.forSourceLines(
"test.SingletonComponent",
"package test;",
"",
"import dagger.Component;",
"import javax.inject.Singleton;",
"",
"@Singleton",
"@Component(dependencies = SimpleScopedComponent.class)",
"interface SingletonComponent {",
" SimpleType type();",
"}");
Compilation compilation =
daggerCompiler().compile(type, simpleScope, simpleScoped, singletonScoped);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining(
message(
"This @Singleton component cannot depend on scoped components:",
" @test.SimpleScope test.SimpleScopedComponent"));
}
@Test
public void componentScopeWithMultipleScopedDependenciesMustNotCycle() {
JavaFileObject type =
JavaFileObjects.forSourceLines(
"test.SimpleType",
"package test;",
"",
"import javax.inject.Inject;",
"",
"class SimpleType {",
" @Inject SimpleType() {}",
"}");
JavaFileObject scopeA =
JavaFileObjects.forSourceLines(
"test.ScopeA",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope @interface ScopeA {}");
JavaFileObject scopeB =
JavaFileObjects.forSourceLines(
"test.ScopeB",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope @interface ScopeB {}");
JavaFileObject longLifetime =
JavaFileObjects.forSourceLines(
"test.ComponentLong",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeA",
"@Component",
"interface ComponentLong {",
" SimpleType type();",
"}");
JavaFileObject mediumLifetime1 =
JavaFileObjects.forSourceLines(
"test.ComponentMedium1",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeB",
"@Component(dependencies = ComponentLong.class)",
"interface ComponentMedium1 {",
" SimpleType type();",
"}");
JavaFileObject mediumLifetime2 =
JavaFileObjects.forSourceLines(
"test.ComponentMedium2",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeB",
"@Component",
"interface ComponentMedium2 {",
"}");
JavaFileObject shortLifetime =
JavaFileObjects.forSourceLines(
"test.ComponentShort",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeA",
"@Component(dependencies = {ComponentMedium1.class, ComponentMedium2.class})",
"interface ComponentShort {",
" SimpleType type();",
"}");
Compilation compilation =
daggerCompiler()
.compile(
type, scopeA, scopeB,
longLifetime, mediumLifetime1, mediumLifetime2, shortLifetime);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining(
message(
"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.
JavaFileObject type =
JavaFileObjects.forSourceLines(
"test.SimpleType",
"package test;",
"",
"import javax.inject.Inject;",
"",
"class SimpleType {",
" @Inject SimpleType() {}",
"}");
JavaFileObject scopeA =
JavaFileObjects.forSourceLines(
"test.ScopeA",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope @interface ScopeA {}");
JavaFileObject scopeB =
JavaFileObjects.forSourceLines(
"test.ScopeB",
"package test;",
"",
"import javax.inject.Scope;",
"",
"@Scope @interface ScopeB {}");
JavaFileObject longLifetime =
JavaFileObjects.forSourceLines(
"test.ComponentLong",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeA",
"@Component",
"interface ComponentLong {",
" SimpleType type();",
"}");
JavaFileObject mediumLifetime =
JavaFileObjects.forSourceLines(
"test.ComponentMedium",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeB",
"@Component(dependencies = ComponentLong.class)",
"interface ComponentMedium {",
" SimpleType type();",
"}");
JavaFileObject shortLifetime =
JavaFileObjects.forSourceLines(
"test.ComponentShort",
"package test;",
"",
"import dagger.Component;",
"",
"@ScopeA",
"@Component(dependencies = ComponentMedium.class)",
"interface ComponentShort {",
" SimpleType type();",
"}");
Compilation compilation =
daggerCompiler().compile(type, scopeA, scopeB, longLifetime, mediumLifetime, shortLifetime);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining(
message(
"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.
compilation =
compilerWithOptions("-Adagger.validateTransitiveComponentDependencies=DISABLED")
.compile(type, scopeA, scopeB, longLifetime, mediumLifetime, shortLifetime);
assertThat(compilation).succeeded();
}
@Test
public void reusableNotAllowedOnComponent() {
JavaFileObject someComponent =
JavaFileObjects.forSourceLines(
"test.SomeComponent",
"package test;",
"",
"import dagger.Component;",
"import dagger.Reusable;",
"",
"@Reusable",
"@Component",
"interface SomeComponent {}");
Compilation compilation = daggerCompiler().compile(someComponent);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining("@Reusable cannot be applied to components or subcomponents")
.inFile(someComponent)
.onLine(6);
}
@Test
public void reusableNotAllowedOnSubcomponent() {
JavaFileObject someSubcomponent =
JavaFileObjects.forSourceLines(
"test.SomeComponent",
"package test;",
"",
"import dagger.Reusable;",
"import dagger.Subcomponent;",
"",
"@Reusable",
"@Subcomponent",
"interface SomeSubcomponent {}");
Compilation compilation = daggerCompiler().compile(someSubcomponent);
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining("@Reusable cannot be applied to components or subcomponents")
.inFile(someSubcomponent)
.onLine(6);
}
}