Skip to content

Commit

Permalink
feat: add support for NonNullApi package annotation (#368)
Browse files Browse the repository at this point in the history
Adds support for a new NonNullApi package annotation from quarkus-hilla
and replaces org.springframework.lang.NonNullApi with the new one
at build time.

Fixes #326
  • Loading branch information
mcollovati authored Nov 9, 2023
1 parent ad6b6ca commit 55dfbf5
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@
import com.github.mcollovati.quarkus.hilla.HillaFormAuthenticationMechanism;
import com.github.mcollovati.quarkus.hilla.HillaSecurityPolicy;
import com.github.mcollovati.quarkus.hilla.HillaSecurityRecorder;
import com.github.mcollovati.quarkus.hilla.NonNullApi;
import com.github.mcollovati.quarkus.hilla.QuarkusEndpointConfiguration;
import com.github.mcollovati.quarkus.hilla.QuarkusEndpointController;
import com.github.mcollovati.quarkus.hilla.QuarkusEndpointProperties;
import com.github.mcollovati.quarkus.hilla.QuarkusViewAccessChecker;
import com.github.mcollovati.quarkus.hilla.deployment.asm.NonnullPluginConfigClassVisitor;
import com.github.mcollovati.quarkus.hilla.deployment.asm.PushEndpointClassVisitor;
import com.github.mcollovati.quarkus.hilla.deployment.asm.SpringReplacementsClassVisitor;

Expand Down Expand Up @@ -202,6 +204,9 @@ void replaceCallsToSpring(
producer.produce(new BytecodeTransformerBuildItem(
PushMessageHandler.class.getName(),
(s, classVisitor) -> new SpringReplacementsClassVisitor(classVisitor, "handleBrowserSubscribe")));
producer.produce(new BytecodeTransformerBuildItem(
"dev.hilla.parser.plugins.nonnull.NonnullPluginConfig$Processor",
(s, classVisitor) -> new NonnullPluginConfigClassVisitor(classVisitor)));
}

@BuildStep
Expand Down Expand Up @@ -232,6 +237,30 @@ public void transform(TransformationContext ctx) {
}));
}

@BuildStep
void replacePackageNonNullApiAnnotations(BuildProducer<AnnotationsTransformerBuildItem> producer) {
DotName sourceAnnotation = DotName.createSimple("org.springframework.lang.NonNullApi");
DotName targetAnnotation = DotName.createSimple(NonNullApi.class);
Predicate<AnnotationInstance> isAnnotatedPredicate = ann -> ann.name().equals(sourceAnnotation);
producer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {

@Override
public boolean appliesTo(AnnotationTarget.Kind kind) {
return AnnotationTarget.Kind.CLASS == kind;
}

@Override
public void transform(TransformationContext ctx) {
if (ctx.getAnnotations().stream().anyMatch(isAnnotatedPredicate)) {
ctx.transform()
.remove(isAnnotatedPredicate)
.add(targetAnnotation)
.done();
}
}
}));
}

@BuildStep
void registerHillaSecurityPolicy(AuthFormBuildItem authFormEnabled, BuildProducer<AdditionalBeanBuildItem> beans) {
if (authFormEnabled.isEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2023 Marco Collovati, Dario Götze
*
* 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 com.github.mcollovati.quarkus.hilla.deployment.asm;

import java.util.ListIterator;

import io.quarkus.gizmo.Gizmo;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;

public class NonnullPluginConfigClassVisitor extends ClassVisitor {
public NonnullPluginConfigClassVisitor(ClassVisitor classVisitor) {
super(Gizmo.ASM_API_VERSION, classVisitor);
}

@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor superVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("<clinit>".equals(name)) {
return new MethodNode(Gizmo.ASM_API_VERSION, access, name, descriptor, signature, exceptions) {
@Override
public void visitEnd() {
var iterator = instructions.iterator();
IntInsnNode setSize = findSetSizeNode(iterator);
if (setSize == null) {
return;
}
setSize.operand = setSize.operand + 1;
AbstractInsnNode instruction = findSetOfInstruction(iterator);
if (instruction != null) {
int nextArrayIndex = setSize.operand - 1;
instructions.insertBefore(instruction, new InsnNode(Opcodes.DUP));
instructions.insertBefore(instruction, new IntInsnNode(Opcodes.BIPUSH, nextArrayIndex));
instructions.insertBefore(
instruction,
new TypeInsnNode(Opcodes.NEW, "dev/hilla/parser/plugins/nonnull/AnnotationMatcher"));
instructions.insertBefore(instruction, new InsnNode(Opcodes.DUP));
instructions.insertBefore(
instruction, new LdcInsnNode("com.github.mcollovati.quarkus.hilla.NonNullApi"));
instructions.insertBefore(instruction, new InsnNode(Opcodes.ICONST_0));
instructions.insertBefore(instruction, new IntInsnNode(Opcodes.BIPUSH, 10));
instructions.insertBefore(
instruction,
new MethodInsnNode(
Opcodes.INVOKESPECIAL,
"dev/hilla/parser/plugins/nonnull/AnnotationMatcher",
"<init>",
"(Ljava/lang/String;ZI)V"));
instructions.insertBefore(instruction, new InsnNode(Opcodes.AASTORE));
}
accept(superVisitor);
}

private static IntInsnNode findSetSizeNode(ListIterator<AbstractInsnNode> iterator) {
while (iterator.hasNext()) {
if (iterator.next() instanceof IntInsnNode intInsnNode) {
return intInsnNode;
}
}
return null;
}

private static AbstractInsnNode findSetOfInstruction(ListIterator<AbstractInsnNode> iterator) {
while (iterator.hasNext()) {
var instruction = iterator.next();
if (instruction.getOpcode() == Opcodes.INVOKESTATIC
&& instruction instanceof MethodInsnNode methodInsnNode
&& "java/util/Set".equals(methodInsnNode.owner)
&& "of".equals(methodInsnNode.name)) {
return instruction;
}
}
return null;
}
};
}
return superVisitor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.example.application;

import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;

import com.vaadin.flow.server.auth.AnonymousAllowed;
Expand All @@ -26,7 +25,6 @@
@ApplicationScoped
public class HelloWorldService {

@Nonnull
public String sayHello(String name) {
if (name.isEmpty()) {
return "Hello stranger";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NonNullApi
package com.example.application;

import org.springframework.lang.NonNullApi;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2023 Marco Collovati, Dario Götze
*
* 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 com.github.mcollovati.quarkus.hilla;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierDefault;

@Target({ElementType.PACKAGE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Nonnull
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
public @interface NonNullApi {}

0 comments on commit 55dfbf5

Please sign in to comment.