Skip to content

Commit

Permalink
Detect empty annotation name at build time
Browse files Browse the repository at this point in the history
  • Loading branch information
gwenneg committed Mar 11, 2024
1 parent 4436ddd commit f884954
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkiverse.unleash;

import org.jboss.jandex.ClassInfo;

public class EmptyAnnotationNameException extends RuntimeException {

private static final String MESSAGE = "@" + FeatureToggle.class.getName() + " and @" + FeatureVariant.class.getName()
+ " annotations must have a non empty name attribute [class=%s]";

private final ClassInfo classInfo;

public EmptyAnnotationNameException(ClassInfo classInfo) {
super(String.format(MESSAGE, classInfo.name()));
this.classInfo = classInfo;
}

public ClassInfo getClassInfo() {
return classInfo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,25 @@
import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.inject.Inject;

import org.jboss.jandex.*;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodParameterInfo;
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.Type;

import io.getunleash.Unleash;
import io.getunleash.Variant;
import io.quarkiverse.unleash.runtime.*;
import io.quarkiverse.unleash.runtime.AbstractVariantProducer;
import io.quarkiverse.unleash.runtime.FeatureToggleProducer;
import io.quarkiverse.unleash.runtime.ToggleVariantProducer;
import io.quarkiverse.unleash.runtime.ToggleVariantStringProducer;
import io.quarkiverse.unleash.runtime.UnleashLifecycleManager;
import io.quarkiverse.unleash.runtime.UnleashRecorder;
import io.quarkiverse.unleash.runtime.UnleashResourceProducer;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
Expand Down Expand Up @@ -115,29 +128,60 @@ AdditionalBeanBuildItem additionalBeans() {
}

@BuildStep
void validateFeatureToggleAnnotations(CombinedIndexBuildItem combinedIndex,
void validateAnnotations(CombinedIndexBuildItem combinedIndex,
BuildProducer<ValidationErrorBuildItem> validationErrors) {
List<Throwable> throwables = new ArrayList<>();

for (AnnotationInstance annotation : combinedIndex.getIndex().getAnnotations(DN_FEATURE_TOGGLE)) {
AnnotationTarget target = annotation.target();
String name = annotation.value("name").asString();
switch (target.kind()) {
case FIELD -> {
ClassInfo declaringClass = target.asField().declaringClass();
if (DN_PRIMITIVE_BOOLEAN.equals(target.asField().type().name())) {
ClassInfo declaringClass = target.asField().declaringClass();
throwables.add(new BooleanFeatureToggleException(declaringClass));
}
if (name == null || name.isEmpty()) {
throwables.add(new EmptyAnnotationNameException(declaringClass));
}
}
case METHOD_PARAMETER -> {
ClassInfo declaringClass = target.asMethodParameter().method().declaringClass();
if (DN_PRIMITIVE_BOOLEAN.equals(target.asMethodParameter().type().name())) {
ClassInfo declaringClass = target.asMethodParameter().method().declaringClass();
throwables.add(new BooleanFeatureToggleException(declaringClass));
}
if (name == null || name.isEmpty()) {
throwables.add(new EmptyAnnotationNameException(declaringClass));
}
}
default -> {
// No validation required for all other target kinds.
}
}
}

for (AnnotationInstance annotation : combinedIndex.getIndex().getAnnotations(DN_FEATURE_VARIANT)) {
AnnotationTarget target = annotation.target();
String name = annotation.value("name").asString();
switch (target.kind()) {
case FIELD -> {
if (name == null || name.isEmpty()) {
ClassInfo declaringClass = target.asField().declaringClass();
throwables.add(new EmptyAnnotationNameException(declaringClass));
}
}
case METHOD_PARAMETER -> {
if (name == null || name.isEmpty()) {
ClassInfo declaringClass = target.asMethodParameter().method().declaringClass();
throwables.add(new EmptyAnnotationNameException(declaringClass));
}
}
default -> {
// No validation required for all other target kinds.
}
}
}

validationErrors.produce(new ValidationErrorBuildItem(throwables.toArray(new Throwable[0])));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.quarkiverse.unleash;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.Arrays;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.spi.DeploymentException;
import jakarta.inject.Singleton;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.getunleash.Variant;
import io.quarkus.test.QuarkusUnitTest;

public class EmptyAnnotationNameExceptionTest {

@RegisterExtension
static final QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot(
(jar) -> jar.addClasses(EmptyFeatureToggleNameField.class, EmptyFeatureToggleNameConstructorArgument.class,
EmptyFeatureVariantNameField.class, EmptyFeatureVariantNameConstructorArgument.class))
.assertException(t -> {
assertEquals(DeploymentException.class, t.getClass());
assertEquals(4, t.getSuppressed().length);
assertEmptyAnnotationNameException(t, EmptyFeatureToggleNameField.class);
assertEmptyAnnotationNameException(t, EmptyFeatureToggleNameConstructorArgument.class);
assertEmptyAnnotationNameException(t, EmptyFeatureVariantNameField.class);
assertEmptyAnnotationNameException(t, EmptyFeatureVariantNameConstructorArgument.class);
});

private static void assertEmptyAnnotationNameException(Throwable t, Class<?> expectedClassName) {
assertEquals(1, Arrays.stream(t.getSuppressed()).filter(throwable -> {
if (throwable instanceof EmptyAnnotationNameException e) {
return expectedClassName.getName().equals(e.getClassInfo().name().toString());
}
return false;
}).count());
}

@Test
void shouldNotBeInvoked() {
fail("A deployment exception should be thrown before this method is ever invoked");
}

@ApplicationScoped
static class EmptyFeatureToggleNameField {

@FeatureToggle(name = "")
Instance<Boolean> toggle;
}

@Singleton
static class EmptyFeatureToggleNameConstructorArgument {

public EmptyFeatureToggleNameConstructorArgument(@FeatureToggle(name = "") Instance<Boolean> toggle) {
}
}

@RequestScoped
static class EmptyFeatureVariantNameField {

@FeatureVariant(name = "")
Instance<Variant> variant;
}

@Singleton
static class EmptyFeatureVariantNameConstructorArgument {

public EmptyFeatureVariantNameConstructorArgument(@FeatureVariant(name = "") Instance<Variant> variant) {
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ protected FeatureVariant getFeatureVariant(InjectionPoint injectionPoint) {
break;
}
}
if (ft == null || ft.name().isEmpty()) {
throw new IllegalStateException("No feature toggle name of the variant specified");
}
return ft;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ boolean getFeatureToggle(InjectionPoint injectionPoint) {
break;
}
}
if (ft == null || ft.name().isEmpty()) {
throw new IllegalStateException("No feature toggle name specified");
}
return unleash.isEnabled(ft.name(), ft.defaultValue());
}

Expand Down

0 comments on commit f884954

Please sign in to comment.