From 706158186edb936f3015cce07dae0cc4e6513a86 Mon Sep 17 00:00:00 2001 From: jbock <jbock-java@gmx.de> Date: Sat, 2 Dec 2023 16:42:52 +0100 Subject: [PATCH] ISSUES-17 add BindingRegistry --- .../simple/processor/ContextComponent.java | 26 ++++++++++ .../processor/binding/ComponentElement.java | 34 ++++++++++--- .../processor/binding/InjectBinding.java | 20 ++++++++ .../binding/InjectBindingFactory.java | 25 ++++----- .../processor/step/BindingRegistry.java | 30 +++++++++++ .../simple/processor/step/ComponentStep.java | 19 +++---- .../simple/processor/step/InjectStep.java | 7 ++- .../simple/processor/step/ProvidesStep.java | 6 ++- .../validation/InjectBindingValidator.java | 48 ++++++++++------- .../processor/BindingValidationTest.java | 2 +- .../processor/DuplicateBindingTest.java | 51 +++++++++++++++++++ 11 files changed, 214 insertions(+), 54 deletions(-) create mode 100644 compiler/src/main/java/io/jbock/simple/processor/step/BindingRegistry.java diff --git a/compiler/src/main/java/io/jbock/simple/processor/ContextComponent.java b/compiler/src/main/java/io/jbock/simple/processor/ContextComponent.java index ed9fa9e..9dc2d98 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/ContextComponent.java +++ b/compiler/src/main/java/io/jbock/simple/processor/ContextComponent.java @@ -1,6 +1,7 @@ package io.jbock.simple.processor; import io.jbock.simple.Component; +import io.jbock.simple.Inject; import io.jbock.simple.processor.binding.ComponentElement; import io.jbock.simple.processor.binding.InjectBindingFactory; import io.jbock.simple.processor.binding.KeyFactory; @@ -40,4 +41,29 @@ static ContextComponent create( Generator generator(); TopologicalSorter topologicalSorter(); + + final class Factory { + private final TypeTool tool; + private final InjectBindingFactory injectBindingFactory; + private final KeyFactory keyFactory; + + @Inject + public Factory( + TypeTool tool, + InjectBindingFactory injectBindingFactory, + KeyFactory keyFactory) { + this.tool = tool; + this.injectBindingFactory = injectBindingFactory; + this.keyFactory = keyFactory; + } + + public ContextComponent create(ComponentElement component) { + return ContextComponent_Impl.builder() + .componentElement(component) + .tool(tool) + .injectBindingFactory(injectBindingFactory) + .keyFactory(keyFactory) + .build(); + } + } } 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 a229899..1f63361 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 @@ -2,6 +2,7 @@ import io.jbock.javapoet.ClassName; import io.jbock.simple.Component; +import io.jbock.simple.Inject; import io.jbock.simple.Provides; import io.jbock.simple.processor.util.ValidationFailure; import io.jbock.simple.processor.util.Visitors; @@ -25,6 +26,7 @@ public final class ComponentElement { private final TypeElement element; private final KeyFactory keyFactory; + private final InjectBinding.Factory injectBindingFactory; private final Supplier<ClassName> generatedClass = memoize(() -> { ClassName className = ClassName.get(element()); @@ -72,7 +74,7 @@ public final class ComponentElement { continue; // ignore } Key key = keyFactory().getKey(method); - InjectBinding b = InjectBinding.create(keyFactory(), method); + InjectBinding b = injectBindingFactory().create(method); result.put(key, b); } return result; @@ -122,15 +124,11 @@ public final class ComponentElement { private ComponentElement( TypeElement element, - KeyFactory keyFactory) { + KeyFactory keyFactory, + InjectBinding.Factory injectBindingFactory) { this.element = element; this.keyFactory = keyFactory; - } - - public static ComponentElement create( - TypeElement element, - KeyFactory keyFactory) { - return new ComponentElement(element, keyFactory); + this.injectBindingFactory = injectBindingFactory; } public TypeElement element() { @@ -165,6 +163,10 @@ private KeyFactory keyFactory() { return keyFactory; } + private InjectBinding.Factory injectBindingFactory() { + return injectBindingFactory; + } + public Optional<Binding> parameterBinding(Key key) { return Optional.ofNullable(parameterBindings.get().get(key)); } @@ -188,4 +190,20 @@ public boolean omitMockBuilder() { } return annotation.omitMockBuilder(); } + + public static final class Factory { + private final KeyFactory keyFactory; + private final InjectBinding.Factory injectBindingFactory; + + @Inject + public Factory(KeyFactory keyFactory, InjectBinding.Factory injectBindingFactory) { + this.keyFactory = keyFactory; + this.injectBindingFactory = injectBindingFactory; + } + + public ComponentElement create( + TypeElement element) { + return new ComponentElement(element, keyFactory, injectBindingFactory); + } + } } diff --git a/compiler/src/main/java/io/jbock/simple/processor/binding/InjectBinding.java b/compiler/src/main/java/io/jbock/simple/processor/binding/InjectBinding.java index 349213d..7687f29 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/binding/InjectBinding.java +++ b/compiler/src/main/java/io/jbock/simple/processor/binding/InjectBinding.java @@ -4,6 +4,7 @@ import io.jbock.javapoet.ParameterSpec; import io.jbock.javapoet.ParameterizedTypeName; import io.jbock.javapoet.TypeName; +import io.jbock.simple.Inject; import io.jbock.simple.Provides; import io.jbock.simple.processor.util.ValidationFailure; @@ -141,4 +142,23 @@ public String toString() { private KeyFactory keyFactory() { return keyFactory; } + + public static final class Factory { + private final KeyFactory keyFactory; + + @Inject + public Factory(KeyFactory keyFactory) { + this.keyFactory = keyFactory; + } + + InjectBinding create(ExecutableElement m) { + Key key = keyFactory.getKey(m); + if (m.getKind() == ElementKind.CONSTRUCTOR) { + if (key.qualifier().isPresent()) { + throw new ValidationFailure("Constructors can't have qualifiers", m); + } + } + return new InjectBinding(key, keyFactory, m); + } + } } diff --git a/compiler/src/main/java/io/jbock/simple/processor/binding/InjectBindingFactory.java b/compiler/src/main/java/io/jbock/simple/processor/binding/InjectBindingFactory.java index c91bf3a..0bb1abc 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/binding/InjectBindingFactory.java +++ b/compiler/src/main/java/io/jbock/simple/processor/binding/InjectBindingFactory.java @@ -3,7 +3,6 @@ import io.jbock.simple.Inject; import io.jbock.simple.processor.util.ClearableCache; import io.jbock.simple.processor.util.TypeTool; -import io.jbock.simple.processor.util.ValidationFailure; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; @@ -15,8 +14,6 @@ import java.util.Optional; import java.util.stream.Collectors; -import static io.jbock.simple.processor.util.Printing.INDENT; -import static io.jbock.simple.processor.util.Printing.bindingElementToString; import static io.jbock.simple.processor.util.Visitors.EXECUTABLE_ELEMENT_VISITOR; import static io.jbock.simple.processor.util.Visitors.TYPE_ELEMENT_VISITOR; @@ -26,11 +23,16 @@ public final class InjectBindingFactory implements ClearableCache { private final TypeTool tool; private final KeyFactory keyFactory; + private final InjectBinding.Factory injectBindingFactory; @Inject - public InjectBindingFactory(TypeTool tool, KeyFactory keyFactory) { + public InjectBindingFactory( + TypeTool tool, + KeyFactory keyFactory, + InjectBinding.Factory injectBindingFactory) { this.tool = tool; this.keyFactory = keyFactory; + this.injectBindingFactory = injectBindingFactory; } public Map<Key, InjectBinding> injectBindings(TypeElement typeElement) { @@ -47,18 +49,11 @@ public Map<Key, InjectBinding> injectBindings(TypeElement typeElement) { return Map.of(); } result = new LinkedHashMap<>(); - for (ExecutableElement m : allMembers) { - InjectBinding b = InjectBinding.create(keyFactory, m); - InjectBinding previous = result.put(b.key(), b); - if (previous != null) { - throw new ValidationFailure("This binding clashes with:\n" - + INDENT - + bindingElementToString(previous.element()) - + ".\n" - + "Consider a (different) qualifier", b.element()); - } - injectBindingCache.put(typeElement, result); + for (ExecutableElement method : allMembers) { + InjectBinding b = injectBindingFactory.create(method); + result.put(b.key(), b); // duplicates handled elsewhere } + injectBindingCache.put(typeElement, result); return result; } diff --git a/compiler/src/main/java/io/jbock/simple/processor/step/BindingRegistry.java b/compiler/src/main/java/io/jbock/simple/processor/step/BindingRegistry.java new file mode 100644 index 0000000..7eeba18 --- /dev/null +++ b/compiler/src/main/java/io/jbock/simple/processor/step/BindingRegistry.java @@ -0,0 +1,30 @@ +package io.jbock.simple.processor.step; + +import io.jbock.simple.Inject; +import io.jbock.simple.processor.binding.Key; +import io.jbock.simple.processor.binding.KeyFactory; +import io.jbock.simple.processor.util.ValidationFailure; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import java.util.HashMap; +import java.util.Map; + +public class BindingRegistry { + + private final Map<Key, Element> bindings = new HashMap<>(); + private final KeyFactory keyFactory; + + @Inject + public BindingRegistry(KeyFactory keyFactory) { + this.keyFactory = keyFactory; + } + + void register(ExecutableElement method) { + Key key = keyFactory.getKey(method); + Element previous = bindings.put(key, method); + if (previous != null && !previous.equals(method)) { + throw new ValidationFailure("Duplicate binding for " + key, method); + } + } +} diff --git a/compiler/src/main/java/io/jbock/simple/processor/step/ComponentStep.java b/compiler/src/main/java/io/jbock/simple/processor/step/ComponentStep.java index b1fba97..1c18f7b 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/step/ComponentStep.java +++ b/compiler/src/main/java/io/jbock/simple/processor/step/ComponentStep.java @@ -7,8 +7,6 @@ import io.jbock.simple.processor.ContextComponent; import io.jbock.simple.processor.binding.Binding; import io.jbock.simple.processor.binding.ComponentElement; -import io.jbock.simple.processor.binding.InjectBindingFactory; -import io.jbock.simple.processor.binding.KeyFactory; import io.jbock.simple.processor.util.SpecWriter; import io.jbock.simple.processor.util.TypeTool; import io.jbock.simple.processor.util.ValidationFailure; @@ -31,28 +29,28 @@ public class ComponentStep implements Step { private final Messager messager; private final TypeTool tool; - private final KeyFactory keyFactory; private final TypeElementValidator typeElementValidator; private final ExecutableElementValidator executableElementValidator; private final SpecWriter specWriter; - private final InjectBindingFactory injectBindingFactory; + private final ComponentElement.Factory componentElementFactory; + private final ContextComponent.Factory contextComponentFactory; @Inject public ComponentStep( Messager messager, TypeTool tool, - KeyFactory keyFactory, TypeElementValidator typeElementValidator, ExecutableElementValidator executableElementValidator, SpecWriter specWriter, - InjectBindingFactory injectBindingFactory) { + ComponentElement.Factory componentElementFactory, + ContextComponent.Factory contextComponentFactory) { this.messager = messager; this.tool = tool; - this.keyFactory = keyFactory; this.typeElementValidator = typeElementValidator; this.executableElementValidator = executableElementValidator; this.specWriter = specWriter; - this.injectBindingFactory = injectBindingFactory; + this.componentElementFactory = componentElementFactory; + this.contextComponentFactory = contextComponentFactory; } @Override @@ -76,7 +74,7 @@ public Set<? extends Element> process(Map<String, Set<Element>> elementsByAnnota private void process(TypeElement typeElement) { typeElementValidator.validate(typeElement); - ComponentElement component = ComponentElement.create(typeElement, keyFactory); + ComponentElement component = componentElementFactory.create(typeElement); component.factoryElement().ifPresent(factory -> { ExecutableElement method = factory.singleAbstractMethod(); if (!tool.types().isSameType(method.getReturnType(), typeElement.asType())) { @@ -88,8 +86,7 @@ private void process(TypeElement typeElement) { executableElementValidator.validate(m); } } - ContextComponent componentComponent = ContextComponent.create( - component, tool, injectBindingFactory, keyFactory); + ContextComponent componentComponent = contextComponentFactory.create(component); Generator generator = componentComponent.generator(); List<Binding> bindings = componentComponent.topologicalSorter().sortedBindings(); TypeSpec typeSpec = generator.generate(bindings); diff --git a/compiler/src/main/java/io/jbock/simple/processor/step/InjectStep.java b/compiler/src/main/java/io/jbock/simple/processor/step/InjectStep.java index e3a050b..8894227 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/step/InjectStep.java +++ b/compiler/src/main/java/io/jbock/simple/processor/step/InjectStep.java @@ -25,15 +25,18 @@ public class InjectStep implements Step { private final InjectBindingValidator validator; private final ExecutableElementValidator executableElementValidator; private final Messager messager; + private final BindingRegistry bindingRegistry; @Inject public InjectStep( InjectBindingValidator validator, ExecutableElementValidator executableElementValidator, - Messager messager) { + Messager messager, + BindingRegistry bindingRegistry) { this.validator = validator; this.executableElementValidator = executableElementValidator; this.messager = messager; + this.bindingRegistry = bindingRegistry; } @Override @@ -55,10 +58,12 @@ public Set<? extends Element> process(Map<String, Set<Element>> elementsByAnnota for (ExecutableElement constructor : constructors) { executableElementValidator.validate(constructor); validator.validateConstructor(constructor); + bindingRegistry.register(constructor); } for (ExecutableElement method : methods) { executableElementValidator.validate(method); validator.validateStaticMethod(method); + bindingRegistry.register(method); } checkFields(elements); } catch (ValidationFailure f) { diff --git a/compiler/src/main/java/io/jbock/simple/processor/step/ProvidesStep.java b/compiler/src/main/java/io/jbock/simple/processor/step/ProvidesStep.java index 69cf19c..99ade03 100644 --- a/compiler/src/main/java/io/jbock/simple/processor/step/ProvidesStep.java +++ b/compiler/src/main/java/io/jbock/simple/processor/step/ProvidesStep.java @@ -20,11 +20,14 @@ public class ProvidesStep implements Step { private final Messager messager; + private final BindingRegistry bindingRegistry; @Inject public ProvidesStep( - Messager messager) { + Messager messager, + BindingRegistry bindingRegistry) { this.messager = messager; + this.bindingRegistry = bindingRegistry; } @Override @@ -50,6 +53,7 @@ public Set<? extends Element> process(Map<String, Set<Element>> elementsByAnnota if (enclosing.getAnnotation(Component.class) == null) { throw new ValidationFailure("The @Provides method must be nested inside a @Component", m); } + bindingRegistry.register(m); } } catch (ValidationFailure f) { f.writeTo(messager); 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 e4e265f..b632edb 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 @@ -8,7 +8,6 @@ import io.jbock.simple.processor.util.ValidationFailure; import io.jbock.simple.processor.util.Visitors; -import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; @@ -37,13 +36,39 @@ public void validateConstructor(ExecutableElement element) { validate(element); } - public void validateStaticMethod(ExecutableElement element) { - validate(element); - if (!tool.types().isSameType(element.getReturnType(), element.getEnclosingElement().asType())) { - throw new ValidationFailure("Static method binding must return the enclosing type", - element); + public void validateStaticMethod(ExecutableElement method) { + validate(method); + TypeElement typeElement = Visitors.TYPE_ELEMENT_VISITOR.visit(method.getEnclosingElement()); +// List<TypeElement> hierarchyMethod = getEnclosingElements(typeElement); +// List<TypeElement> hierarchyRt = tool.types().asElement(method.getReturnType()) +// .map(Visitors.TYPE_ELEMENT_VISITOR::visit) +// .map(this::getEnclosingElements) +// .orElse(List.of()); + if (!method.getModifiers().contains(Modifier.STATIC)) { + throw new ValidationFailure("The factory method must be static", method); + } + if (method.getReturnType().getKind() == TypeKind.VOID) { + throw new ValidationFailure("The factory method may not return void", method); + } + if (!tool.types().isSameType(method.getReturnType(), typeElement.asType())) { + throw new ValidationFailure("The factory method must return the type of its enclosing class", method); + } + } + +/* + private List<TypeElement> getEnclosingElements(TypeElement typeElement) { + if (typeElement == null) { + return List.of(); } + List<TypeElement> acc = new ArrayList<>(2); + acc.add(typeElement); + TypeElement el = typeElement; + if ((el = Visitors.TYPE_ELEMENT_VISITOR.visit(el.getEnclosingElement())) != null) { + acc.add(el); + } + return acc; } +*/ private void validate(ExecutableElement element) { TypeElement typeElement = Visitors.TYPE_ELEMENT_VISITOR.visit(element.getEnclosingElement()); @@ -52,17 +77,6 @@ private void validate(ExecutableElement element) { } Map<Key, InjectBinding> m = injectBindingFactory.injectBindings(typeElement); for (InjectBinding b : m.values()) { - if (b.element().getKind() == ElementKind.METHOD) { - if (!b.element().getModifiers().contains(Modifier.STATIC)) { - throw new ValidationFailure("The factory method must be static", b.element()); - } - if (b.element().getReturnType().getKind() == TypeKind.VOID) { - throw new ValidationFailure("The factory method may not return void", b.element()); - } - if (!tool.types().isSameType(b.element().getReturnType(), typeElement.asType())) { - throw new ValidationFailure("The factory method must return the type of its enclosing class", b.element()); - } - } if (b.element().getAnnotationMirrors().stream().filter(mirror -> { DeclaredType annotationType = mirror.getAnnotationType(); return tool.isSameType(annotationType, JAVAX_INJECT) diff --git a/compiler/src/test/java/io/jbock/simple/processor/BindingValidationTest.java b/compiler/src/test/java/io/jbock/simple/processor/BindingValidationTest.java index b4f9105..040c457 100644 --- a/compiler/src/test/java/io/jbock/simple/processor/BindingValidationTest.java +++ b/compiler/src/test/java/io/jbock/simple/processor/BindingValidationTest.java @@ -26,7 +26,7 @@ void multipleStaticBindings() { "}"); Compilation compilation = simpleCompiler().compile(component); assertThat(compilation).failed(); - assertThat(compilation).hadErrorContaining("Consider a (different) qualifier"); + assertThat(compilation).hadErrorContaining("Duplicate binding for test.TestClass"); } @Test diff --git a/compiler/src/test/java/io/jbock/simple/processor/DuplicateBindingTest.java b/compiler/src/test/java/io/jbock/simple/processor/DuplicateBindingTest.java index 0acd9a3..671871c 100644 --- a/compiler/src/test/java/io/jbock/simple/processor/DuplicateBindingTest.java +++ b/compiler/src/test/java/io/jbock/simple/processor/DuplicateBindingTest.java @@ -79,4 +79,55 @@ void parameterBindingTakesPrecedence() { " }", "}"); } + + @Test + void staticMethodConflict() { + JavaFileObject component = forSourceLines("test.TestClass", + "package test;", + "", + "import io.jbock.simple.Component;", + "import io.jbock.simple.Inject;", + "import io.jbock.simple.Named;", + "", + "final class TestClass {", + "", + " static class A {", + " @Inject static A a() { return null; }", + " @Inject static A b() { return null; }", + " }", + "", + " @Component", + " interface AComponent {", + " A getA();", + " }", + "}"); + Compilation compilation = simpleCompiler().compile(component); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorContaining("Duplicate binding for test.TestClass.A"); + } + + @Test + void staticMethodNoConflict() { + JavaFileObject component = forSourceLines("test.TestClass", + "package test;", + "", + "import io.jbock.simple.Component;", + "import io.jbock.simple.Inject;", + "import io.jbock.simple.Named;", + "", + "final class TestClass {", + "", + " static class A {", + " @Inject static A a() { return null; }", + " @Inject static @Named(\"b\") A b() { return null; }", + " }", + "", + " @Component", + " interface AComponent {", + " A getA();", + " }", + "}"); + Compilation compilation = simpleCompiler().compile(component); + assertThat(compilation).succeeded(); + } } \ No newline at end of file