Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check for overriding event annotations #90

Merged
merged 28 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
592c987
refactor(processor): Rename variable
LeStegii Apr 29, 2024
5b7603d
feat(framework): Begin working on checks for overridden methods
LeStegii May 6, 2024
bbdeb17
feat(framework): Optimize checks and remove unused code
LeStegii May 6, 2024
b6377b3
test(framework): Adjust reflection tests
LeStegii May 6, 2024
b537147
docs(framework): Add error code
LeStegii May 6, 2024
1b7970e
docs: Update ERROR_CODES.md
LeStegii May 6, 2024
681659b
refactor(framework): Optimize imports
LeStegii May 6, 2024
8804987
docs(framework): Fix error codes
LeStegii May 6, 2024
254e676
feat(processor): Add processor checks
LeStegii May 7, 2024
8323a7c
docs: Update ERROR_CODES.md
LeStegii May 7, 2024
f359b9d
Merge branch 'master' into fix/override-annotations
LeStegii May 7, 2024
9c359a5
Merge remote-tracking branch 'origin/fix/override-annotations' into f…
LeStegii May 7, 2024
f469c95
feat(processor): Change error location to overriding method
LeStegii May 7, 2024
5533271
feat(processor): Adjust checks and add checks for `@OnKey`
LeStegii May 7, 2024
8556ed4
feat(processor): Fix checks and make them more efficient
LeStegii May 7, 2024
e3dfad4
refactor(processor): Move internals and finalize checks
LeStegii May 7, 2024
39116d4
docs: Update ERROR_CODES.md
LeStegii May 7, 2024
337f0cd
refactor(processor): Refactor and reformat code
LeStegii May 8, 2024
4b1b594
refactor(processor): Cleanup and rename processor classes
LeStegii May 8, 2024
14b3b2e
refactor(processor): Move modifier check
LeStegii May 8, 2024
2f8ebae
docs: Update ERROR_CODES.md
LeStegii May 8, 2024
fda73aa
refactor(processor): Precompute event methods for super classes
LeStegii May 8, 2024
74fcf03
docs: Add inheritance docs
LeStegii May 8, 2024
fab3596
refactor(processor): Remove indents
LeStegii May 8, 2024
ba812a8
refactor(processor): Change map structure
LeStegii May 8, 2024
57e4b60
refactor(processor): Change logic and remove unused code
LeStegii May 8, 2024
4387150
refactor(processor): Little tweaks
LeStegii May 8, 2024
7228b32
refactor(processor): Use variable instead of repeating call
LeStegii May 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public class MyComponent extends ImageView { // Wrong, should extend Parent (or
show(new MyComponent()); // Wrong, should not be able to show a controller that does not provide a parent as its view
```

### 1012: `Cannot access private * '*' in class '*' annotated with '*'.`
### 1012: `Cannot access private * '*' annotated with an event annotation in class '*'.`

- Runtime: ✅
- Annotation Processor: ✅
Expand All @@ -200,6 +200,36 @@ public class MyController {
}
```

### 1013: `Method '*' annotated with an event annotation in class '*' overrides event method in class '*'.`

- Runtime: ✅
- Annotation Processor: ✅

This error if an event method overrides another event method as this would lead to the overriding method being called twice.

```java
public class MyController extends BaseController {

@Override
@OnInit()
private void init() {
}

// ...
}
```

```java
public class BaseController {

@OnInit() // Wrong, event methods shouldn't be overridden in sub classes
private void init() {
}

// ...
}
```

## Resources

### 2000: `Could not find resource '*'.`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,55 @@

import com.google.auto.service.AutoService;
import org.fulib.fx.annotation.Route;
import org.fulib.fx.annotation.controller.Component;
import org.fulib.fx.annotation.controller.Controller;
import org.fulib.fx.annotation.controller.Resource;
import org.fulib.fx.annotation.controller.SubComponent;
import org.fulib.fx.annotation.controller.Title;
import org.fulib.fx.annotation.controller.*;
import org.fulib.fx.annotation.event.OnKey;
import org.fulib.fx.annotation.param.Params;
import org.fulib.fx.annotation.param.ParamsMap;
import org.fulib.fx.util.ControllerUtil;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import static java.util.stream.Collectors.groupingBy;
import static org.fulib.fx.util.FrameworkUtil.error;
import static org.fulib.fx.util.FrameworkUtil.note;

@SupportedAnnotationTypes({
"org.fulib.fx.annotation.controller.*",
"org.fulib.fx.annotation.Route",
"org.fulib.fx.annotation.param.*",
"org.fulib.fx.annotation.controller.*",
"org.fulib.fx.annotation.Route",
"org.fulib.fx.annotation.param.*",
})
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(Processor.class)
@SuppressWarnings("unused")
public class ControllerAnnotationProcessor extends AbstractProcessor {
public class FulibFxProcessor extends AbstractProcessor {

private FxClassGenerator generator;
private ProcessingHelper helper;

public ControllerAnnotationProcessor() {
public FulibFxProcessor() {
}

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.generator = new FxClassGenerator(processingEnv);
this.helper = new ProcessingHelper(processingEnv);
this.generator = new FxClassGenerator(helper, processingEnv);
}

@Override
Expand All @@ -58,15 +61,19 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
checkComponent(element);
checkDoubleAnnotation(element); // Check if a class is annotated with both @Controller and @Component
if (element instanceof TypeElement typeElement) {
checkEventModifiers(typeElement);
generator.generateSidecar(typeElement);
checkOverrides(typeElement);
}
}

// Check if the element is a valid controller
for (Element element : roundEnv.getElementsAnnotatedWith(Controller.class)) {
checkController(element);
if (element instanceof TypeElement typeElement) {
checkEventModifiers(typeElement);
generator.generateSidecar(typeElement);
checkOverrides(typeElement);
}
}

Expand Down Expand Up @@ -101,6 +108,17 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
return true;
}

private void checkOverrides(TypeElement typeElement) {
Map<String, List<ExecutableElement>> eventMethods = helper.streamAllMethods((TypeElement) processingEnv.getTypeUtils().asElement(typeElement.getSuperclass()))
.filter(this::isEventElement)
.collect(groupingBy(method -> method.getSimpleName().toString()));
typeElement.getEnclosedElements().stream()
.filter(e -> e.getKind() == ElementKind.METHOD)
.map(e -> (ExecutableElement) e)
.filter(this::isEventElement)
.forEach(e -> checkOverrides(e, eventMethods));
}

private void checkOnKey(Element element) {
if (element instanceof ExecutableElement method) {
if (!method.getParameters().isEmpty() && !(method.getParameters().size() == 1 && processingEnv.getTypeUtils().isAssignable(method.getParameters().get(0).asType(), processingEnv.getElementUtils().getTypeElement("javafx.scene.input.KeyEvent").asType()))) {
Expand Down Expand Up @@ -222,24 +240,27 @@ private void checkViewResource(Element element, String view) {
private void checkViewMethod(Element element, String view) {
String methodName = view.substring(1); // remove the leading '#'
processingEnv.getElementUtils().getAllMembers((TypeElement) element).stream()
.filter(elem -> elem.getKind() == ElementKind.METHOD)
.filter(elem -> elem.getSimpleName().toString().equals(methodName))
.map(elem -> (ExecutableElement) elem)
.findFirst()
.ifPresentOrElse(
method -> {
if (!method.getParameters().isEmpty()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error(1008).formatted(method.getSimpleName(), method.getEnclosingElement().asType().toString()), method);
}

TypeMirror parent = processingEnv.getElementUtils().getTypeElement("javafx.scene.Parent").asType();

if (!processingEnv.getTypeUtils().isAssignable(method.getReturnType(), parent)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error(1002), method);
}
},
() -> processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error(1003).formatted(methodName), element)
);
.filter(elem -> elem.getKind() == ElementKind.METHOD)
.filter(elem -> elem.getSimpleName().toString().equals(methodName))
.map(elem -> (ExecutableElement) elem)
.findFirst()
.ifPresentOrElse(
method -> {
if (!method.getParameters().isEmpty()) {
Name methodSimpleName = method.getSimpleName();
String className = method.getEnclosingElement().asType().toString();
String error = error(1008).formatted(methodSimpleName, className);
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error, method);
}

TypeMirror parent = processingEnv.getElementUtils().getTypeElement("javafx.scene.Parent").asType();

if (!processingEnv.getTypeUtils().isAssignable(method.getReturnType(), parent)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error(1002), method);
}
},
() -> processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error(1003).formatted(methodName), element)
);
}

private void checkSubComponentElement(Element element) {
Expand All @@ -249,7 +270,7 @@ private void checkSubComponentElement(Element element) {
}

// Check if the field is of a provider type
if (isProvider(element.asType())) {
if (helper.isProvider(element.asType())) {
// Check if the provided class is of a component type
if (element.asType() instanceof DeclaredType type) {
final TypeMirror componentType = type.getTypeArguments().get(0);
Expand All @@ -273,8 +294,73 @@ private boolean isController(TypeMirror typeMirror) {
return declaredType.asElement().getAnnotation(Controller.class) != null;
}

private boolean isProvider(TypeMirror typeMirror) {
return typeMirror.toString().startsWith("javax.inject.Provider");
/**
* Checks if an element is an event element (annotated with {@link ControllerUtil#EVENT_ANNOTATIONS}).
*
* @param element The element to check
* @return True if the element is an event element
*/
private boolean isEventElement(Element element) {
for (Class<? extends Annotation> eventAnnotation : ControllerUtil.EVENT_ANNOTATIONS) {
if (element.getAnnotation(eventAnnotation) != null) {
return true;
}
}
return false;
}

/**
* Checks if the given method overrides another event method.
*
* @param method The method to check
* @param eventMethods A map of all event methods of the superclasses
*/
private void checkOverrides(ExecutableElement method, Map<String, List<ExecutableElement>> eventMethods) {
TypeElement element = (TypeElement) processingEnv.getTypeUtils().asElement(method.getEnclosingElement().asType());

// If no parent class is found, the method cannot override anything
if (element.getSuperclass().getKind() == TypeKind.NONE) {
return;
}

String methodName = method.getSimpleName().toString();

// Check if one of the parent classes has the method
if (!eventMethods.containsKey(methodName)) {
return;
}

for (ExecutableElement parentMethod : eventMethods.get(methodName)) {
if (sameMethodSignature(method, parentMethod)) {
Name className = element.getQualifiedName();
Name parentClassName = ((TypeElement) parentMethod.getEnclosingElement()).getQualifiedName();
String error = error(1013).formatted(method, className, parentClassName);
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error, method);
break;
}
}

}

private boolean sameMethodSignature(ExecutableElement method, ExecutableElement otherMethod) {
ExecutableType methodType = (ExecutableType) method.asType();
ExecutableType otherMethodType = (ExecutableType) otherMethod.asType();
return method.getSimpleName().equals(otherMethod.getSimpleName()) &&
method.getParameters().size() == otherMethod.getParameters().size() &&
processingEnv.getTypeUtils().isSubsignature(methodType, otherMethodType);
}

private void checkEventModifiers(TypeElement clazz) {
Stream.concat(helper.streamAllMethods(clazz), helper.streamAllFields(clazz))
.filter(this::isEventElement)
.filter(element -> element.getModifiers().contains(Modifier.PRIVATE))
.forEach(element -> {
String kind = element.getKind() == ElementKind.METHOD ? "method" : "field";
Name name = element.getSimpleName();
Name clazzName = clazz.getQualifiedName();
String error = error(1012).formatted(kind, name, clazzName);
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error, element);
});
}

}
Loading