diff --git a/compiler/src/main/java/io/jbock/simple/processor/binding/ComponentElement.java b/compiler/src/main/java/io/jbock/simple/processor/binding/ComponentElement.java index 34682b1..f9f4ff5 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/binding/ComponentElement.java +++ b/compiler/src/main/java/io/jbock/simple/processor/binding/ComponentElement.java @@ -3,11 +3,23 @@ import io.jbock.javapoet.ClassName; import io.jbock.simple.Component; import io.jbock.simple.Inject; +import io.jbock.simple.processor.util.Visitors; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.function.Supplier; +import java.util.stream.Collectors; import static io.jbock.simple.processor.util.Suppliers.memoize; +import static io.jbock.simple.processor.util.Visitors.ANNOTATION_VALUE_AS_TYPE; +import static io.jbock.simple.processor.util.Visitors.DECLARED_TYPE_VISITOR; +import static io.jbock.simple.processor.util.Visitors.TYPE_ELEMENT_VISITOR; public final class ComponentElement { @@ -49,4 +61,38 @@ public boolean mockBuilder() { } return annotation.mockBuilder(); } + + public List modules() { + return modules.get(); + } + + private final Supplier> modules = memoize(() -> { + List annotationMirrors = element().getAnnotationMirrors(); + AnnotationMirror annotationMirror = annotationMirrors.stream() + .filter(mirror -> { + TypeElement tel = Visitors.TYPE_ELEMENT_VISITOR.visit(mirror.getAnnotationType().asElement()); + if (tel == null) { + return false; + } + return tel.getQualifiedName().contentEquals(Component.class.getCanonicalName()); + }) + .findFirst() + .orElseThrow(AssertionError::new); + Map elementValues = + annotationMirror.getElementValues(); + return elementValues.entrySet().stream() + .filter(e -> "modules".contentEquals(e.getKey().getSimpleName())) + .map(Map.Entry::getValue) + .flatMap(m -> Visitors.ANNOTATION_VALUE_AS_ARRAY.visit(m, null).stream()) + .map(ANNOTATION_VALUE_AS_TYPE::visit) + .filter(Objects::nonNull) + .map(DECLARED_TYPE_VISITOR::visit) + .filter(Objects::nonNull) + .map(DeclaredType::asElement) + .filter(Objects::nonNull) + .map(TYPE_ELEMENT_VISITOR::visit) + .collect(Collectors.toList()); + }); + + } diff --git a/compiler/src/main/java/io/jbock/simple/processor/binding/KeyFactory.java b/compiler/src/main/java/io/jbock/simple/processor/binding/KeyFactory.java index aead4e2..af2e0e4 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/binding/KeyFactory.java +++ b/compiler/src/main/java/io/jbock/simple/processor/binding/KeyFactory.java @@ -14,6 +14,7 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -119,10 +120,14 @@ public Collection parameterBindings() { }); private final Supplier> providesBindings = memoize(() -> { - List methods = methodsIn(componentElement().element().getEnclosedElements()); + List methods = new ArrayList<>(); + methods.addAll(methodsIn(componentElement().element().getEnclosedElements())); + for (TypeElement module : componentElement().modules()) { + methods.addAll(methodsIn(module.getEnclosedElements())); + } Map result = new LinkedHashMap<>(); for (ExecutableElement method : methods) { - if (method.getAnnotation(Provides.class) == null && !tool().hasInjectAnnotation(method)) { + if (!hasProvidesOrInjectAnnotation(method)) { continue; // ignore } Key key = keyCache().getKey(method); @@ -139,7 +144,7 @@ public Collection parameterBindings() { if (method.getModifiers().contains(Modifier.STATIC)) { continue; // ignore } - if (method.getAnnotation(Provides.class) != null || tool().hasInjectAnnotation(method)) { + if (hasProvidesOrInjectAnnotation(method)) { continue; // ignore } if (!method.getParameters().isEmpty()) { @@ -157,6 +162,10 @@ public Collection parameterBindings() { return result; }); + private boolean hasProvidesOrInjectAnnotation(ExecutableElement method) { + return method.getAnnotation(Provides.class) != null || tool().hasInjectAnnotation(method); + } + public Optional builderElement() { return builderElement.get(); } diff --git a/compiler/src/main/java/io/jbock/simple/processor/util/Visitors.java b/compiler/src/main/java/io/jbock/simple/processor/util/Visitors.java index 2d80b59..87f0ad1 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/util/Visitors.java +++ b/compiler/src/main/java/io/jbock/simple/processor/util/Visitors.java @@ -1,14 +1,19 @@ package io.jbock.simple.processor.util; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.AnnotationValueVisitor; import javax.lang.model.element.ElementVisitor; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVisitor; +import javax.lang.model.util.SimpleAnnotationValueVisitor9; import javax.lang.model.util.SimpleElementVisitor9; import javax.lang.model.util.SimpleTypeVisitor9; +import java.util.List; public final class Visitors { @@ -49,6 +54,27 @@ public DeclaredType visitDeclared(DeclaredType declaredType, Void unused) { } }; + public static final AnnotationValueVisitor ANNOTATION_VALUE_AS_TYPE = new SimpleAnnotationValueVisitor9<>() { + + @Override + public TypeMirror visitType(TypeMirror mirror, Void unused) { + return mirror; + } + }; + + public static final AnnotationValueVisitor, Void> ANNOTATION_VALUE_AS_ARRAY = new SimpleAnnotationValueVisitor9<>() { + + @Override + public List visitArray(List array, Void unused) { + return array; + } + + @Override + protected List defaultAction(Object o, Void unused) { + return List.of(); + } + }; + private Visitors() { } } diff --git a/compiler/src/main/java/io/jbock/simple/processor/validation/InjectBindingValidator.java b/compiler/src/main/java/io/jbock/simple/processor/validation/InjectBindingValidator.java index f4de321..b2b4dbf 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/validation/InjectBindingValidator.java +++ b/compiler/src/main/java/io/jbock/simple/processor/validation/InjectBindingValidator.java @@ -2,6 +2,7 @@ import io.jbock.simple.Component; import io.jbock.simple.Inject; +import io.jbock.simple.Modulus; import io.jbock.simple.processor.binding.InjectBindingScanner; import io.jbock.simple.processor.util.TypeTool; import io.jbock.simple.processor.util.Util; @@ -44,7 +45,7 @@ public void validateStaticMethod(ExecutableElement method) { if (method.getReturnType().getKind() == TypeKind.VOID) { throw new ValidationFailure("The factory method may not return void", method); } - if (!isSibling(method) && !isInComponent(method)) { + if (!isSibling(method) && !isInComponent(method) && !isInModule(method)) { throw new ValidationFailure("The factory method must return the type of its enclosing class", method); } } @@ -54,6 +55,11 @@ private boolean isInComponent(ExecutableElement method) { return typeElement.getAnnotation(Component.class) != null; } + private boolean isInModule(ExecutableElement method) { + TypeElement typeElement = Visitors.TYPE_ELEMENT_VISITOR.visit(method.getEnclosingElement()); + return typeElement.getAnnotation(Modulus.class) != null; + } + private boolean isSibling(ExecutableElement method) { TypeElement typeElement = Visitors.TYPE_ELEMENT_VISITOR.visit(method.getEnclosingElement()); List hierarchyRt = tool.types().asElement(method.getReturnType()) diff --git a/compiler/src/test/java/io/jbock/simple/processor/StaticInjectionTest.java b/compiler/src/test/java/io/jbock/simple/processor/StaticInjectionTest.java index cfa607e..dd7299c 100644 --- a/compiler/src/test/java/io/jbock/simple/processor/StaticInjectionTest.java +++ b/compiler/src/test/java/io/jbock/simple/processor/StaticInjectionTest.java @@ -19,6 +19,7 @@ void clashResolvedByQualifiers() { "import io.jbock.simple.Component;", "import io.jbock.simple.Inject;", "import io.jbock.simple.Named;", + "import io.jbock.simple.Modulus;", "", "final class TestClass {", "", @@ -27,11 +28,16 @@ void clashResolvedByQualifiers() { " }", "", " static class B {", - " @Inject @Named(\"1\") static B create1() { return null; }", + " @Inject @Named(\"1\") static B create1(String s) { return null; }", " @Inject @Named(\"2\") static B create2() { return null; }", " }", "", - " @Component", + " @Modulus", + " static class M {", + " @Inject static String createString() { return \"\"; }", + " }", + "", + " @Component(modules = M.class)", " interface AComponent {", " A getA();", " }", @@ -55,7 +61,8 @@ void clashResolvedByQualifiers() { " }", "", " static TestClass.AComponent create() {", - " TestClass.B testClassB = TestClass.B.create1();", + " String mString = TestClass.M.createString();", + " TestClass.B testClassB = TestClass.B.create1(mString);", " TestClass.B testClassB2 = TestClass.B.create2();", " TestClass.A testClassA = new TestClass.A(testClassB, testClassB2);", " return new TestClass_AComponent_Impl(testClassA);", diff --git a/simple-component/src/main/java/io/jbock/simple/Component.java b/simple-component/src/main/java/io/jbock/simple/Component.java index b5980bd..2b9909f 100644 --- a/simple-component/src/main/java/io/jbock/simple/Component.java +++ b/simple-component/src/main/java/io/jbock/simple/Component.java @@ -31,7 +31,7 @@ public @interface Component { /** - * A list of classes annotated with {@link Module} whose bindings are used to generate the + * A list of classes annotated with {@link Modulus} whose bindings are used to generate the * component implementation. */ Class[] modules() default {}; diff --git a/simple-component/src/main/java/io/jbock/simple/Module.java b/simple-component/src/main/java/io/jbock/simple/Module.java deleted file mode 100644 index a7dd8a4..0000000 --- a/simple-component/src/main/java/io/jbock/simple/Module.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.jbock.simple; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** Annotates a class that contributes to the object graph. */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface Module { -} \ No newline at end of file diff --git a/simple-component/src/main/java/io/jbock/simple/Modulus.java b/simple-component/src/main/java/io/jbock/simple/Modulus.java new file mode 100644 index 0000000..2616f02 --- /dev/null +++ b/simple-component/src/main/java/io/jbock/simple/Modulus.java @@ -0,0 +1,23 @@ +package io.jbock.simple; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a class that contributes to the object graph. + * A binding is a {@code static} methods that is annotated with either + * {@code @Provides} or {@code @Inject}. + * + *

The annotated class must also be referenced in the + * {@code modules} of a {@link Component}. + * + *

This class is called {@code Modulus}, not {@code Module} + * because there is a class {@code Module} in {@code java.lang} + * since Java 9. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Modulus { +} \ No newline at end of file