diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 5fb4085..f09e33f 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -10,17 +10,17 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: maven - - name: Build with Maven - run: mvn clean verify package - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true + - uses: actions/checkout@v4 + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + java-version: '25' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn clean verify package + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/pom.xml b/pom.xml index b7534dd..4539f6f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,154 +1,122 @@ - 4.0.0 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - com.garciat.typeclasses - java-type-classes - 1.0-SNAPSHOT + com.garciat.typeclasses + java-type-classes + 1.0-SNAPSHOT - java-type-classes - https://github.com/Garciat/java-type-classes + java-type-classes + https://github.com/Garciat/java-type-classes - - UTF-8 - 21 - 21 - + + UTF-8 + 25 + 25 + - - - org.jspecify - jspecify - 1.0.0 - - - org.junit.jupiter - junit-jupiter - 6.0.0 - test - - - org.assertj - assertj-core - 3.27.3 - test - - + + + org.jspecify + jspecify + 1.0.0 + + + org.junit.jupiter + junit-jupiter + 6.0.0 + test + + + org.assertj + assertj-core + 3.27.3 + test + + - - - - - maven-clean-plugin - 3.5.0 - - - maven-resources-plugin - 3.4.0 - - - maven-compiler-plugin - 3.14.1 - - true - 128m - 2048m - - -XDcompilePolicy=simple - --should-stop=ifError=FLOW - -Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages=com.garciat.typeclasses - -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED - - - - com.google.errorprone - error_prone_core - 2.45.0 - - - com.uber.nullaway - nullaway - 0.12.14 - - - - - - maven-surefire-plugin - 3.5.4 - - - org.jacoco - jacoco-maven-plugin - 0.8.14 - - - - prepare-agent - - - - report - verify - - report - - - - - - maven-jar-plugin - 3.5.0 - - - maven-install-plugin - 3.1.4 - - - com.spotify.fmt - fmt-maven-plugin - 2.27 - - - - check - - - - - - com.google.googlejavaformat - google-java-format - 1.27.0 - - - - - - - - com.spotify.fmt - fmt-maven-plugin - - - maven-compiler-plugin - - - org.jacoco - jacoco-maven-plugin - - - + + + + + maven-clean-plugin + 3.5.0 + + + maven-resources-plugin + 3.4.0 + + + maven-compiler-plugin + 3.14.1 + + + maven-surefire-plugin + 3.5.4 + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + + prepare-agent + + + + report + verify + + report + + + + + + maven-jar-plugin + 3.5.0 + + + maven-install-plugin + 3.1.4 + + + com.spotify.fmt + fmt-maven-plugin + 2.27 + + + + check + + + + + + com.google.googlejavaformat + google-java-format + 1.27.0 + + + + + + + + com.spotify.fmt + fmt-maven-plugin + + + maven-compiler-plugin + + + org.jacoco + jacoco-maven-plugin + + + \ No newline at end of file diff --git a/src/main/java/com/garciat/typeclasses/processor/TreeParser.java b/src/main/java/com/garciat/typeclasses/processor/TreeParser.java new file mode 100644 index 0000000..87cee5b --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/processor/TreeParser.java @@ -0,0 +1,146 @@ +package com.garciat.typeclasses.processor; + +import com.garciat.typeclasses.types.Maybe; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.TreePath; +import com.sun.source.util.Trees; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +interface TreeParser { + Maybe parse(Trees trees, TreePath current, T input); + + default TreeParser flatMap(TreeParser next) { + return (trees, current, input) -> + this.parse(trees, current, input).flatMap(r -> next.parse(trees, current, r)); + } + + default TreeParser map(Function mapper) { + return flatMap(mapping(mapper)); + } + + default TreeParser filter(Predicate predicate) { + return flatMap(filtering(predicate)); + } + + default

TreeParser guard(TreeParser predicate) { + return (trees, current, input) -> + this.parse(trees, current, input) + .flatMap(r -> predicate.parse(trees, current, r).map(_ -> r)); + } + + static TreeParser identity() { + return (_, _, input) -> Maybe.just(input); + } + + static TreeParser mapping(Function mapper) { + return (_, _, input) -> Maybe.just(mapper.apply(input)); + } + + static TreeParser filtering(Predicate predicate) { + return (_, _, input) -> { + if (predicate.test(input)) { + return Maybe.just(input); + } else { + return Maybe.nothing(); + } + }; + } + + static TreeParser notNull() { + return filtering(Objects::nonNull); + } + + static TreeParser as(Class cls) { + return (_, _, input) -> { + if (cls.isInstance(input)) { + return Maybe.just(cls.cast(input)); + } else { + return Maybe.nothing(); + } + }; + } + + static TreeParser currentElement() { + return (trees, current, _) -> { + Element element = trees.getElement(current); + if (element != null) { + return Maybe.just(element); + } else { + return Maybe.nothing(); + } + }; + } + + static TreeParser methodMatches(Method target) { + return TreeParser.as(ExecutableElement.class) + .filter(m -> m.getSimpleName().contentEquals(target.getName())) + .guard( + mapping(ExecutableElement::getEnclosingElement) + .flatMap(as(TypeElement.class)) + .map(TypeElement::getQualifiedName) + .filter(name -> name.contentEquals(target.getDeclaringClass().getName()))); + } + + static TreeParser unaryCallArgument() { + return mapping(MethodInvocationTree::getArguments) + .filter(list -> list.size() == 1) + .map(List::getFirst); + } + + static TreeParser newAnonymousClassBody() { + return TreeParser.as(NewClassTree.class) + .map(NewClassTree::getClassBody) + .flatMap(notNull()); + } + + static TreeParser singleImplementsClause() { + return mapping(ClassTree::getImplementsClause) + .flatMap(notNull()) + .filter(list -> list.size() == 1) + .map(List::getFirst); + } + + static TreeParser treeTypeMirror() { + return (trees, current, input) -> { + try { + TypeMirror typeMirror = + trees.getTypeMirror(trees.getPath(current.getCompilationUnit(), input)); + return Maybe.just(typeMirror); + } catch (IllegalArgumentException e) { + return Maybe.nothing(); + } + }; + } + + static TreeParser rawTypeMatches(Class cls) { + return TreeParser.as(DeclaredType.class) + .guard( + declaredTypeElement() + .flatMap(as(TypeElement.class)) + .map(TypeElement::getQualifiedName) + .filter(name -> name.contentEquals(cls.getName()))); + } + + static TreeParser unaryTypeArgument() { + return mapping(DeclaredType::getTypeArguments) + .filter(list -> list.size() == 1) + .map(List::getFirst); + } + + static TreeParser declaredTypeElement() { + return mapping(DeclaredType::asElement).flatMap(notNull()); + } +} diff --git a/src/main/java/com/garciat/typeclasses/processor/WitnessResolutionChecker.java b/src/main/java/com/garciat/typeclasses/processor/WitnessResolutionChecker.java index a367f43..ba57600 100644 --- a/src/main/java/com/garciat/typeclasses/processor/WitnessResolutionChecker.java +++ b/src/main/java/com/garciat/typeclasses/processor/WitnessResolutionChecker.java @@ -4,33 +4,21 @@ import com.garciat.typeclasses.TypeClasses; import com.garciat.typeclasses.api.Ty; -import com.garciat.typeclasses.types.Maybe; import com.garciat.typeclasses.types.Unit; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.Tree; -import com.sun.source.util.JavacTask; -import com.sun.source.util.Plugin; -import com.sun.source.util.TaskEvent; -import com.sun.source.util.TaskListener; -import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; import com.sun.source.util.Trees; import java.lang.reflect.Method; -import java.util.List; -import java.util.Objects; -import java.util.function.Function; -import java.util.function.Predicate; +import java.util.Set; +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; -public final class WitnessResolutionChecker implements Plugin { +@SupportedAnnotationTypes("*") +@SupportedSourceVersion(SourceVersion.RELEASE_25) +public final class WitnessResolutionChecker extends AbstractProcessor { private static final Method WITNESS_METHOD; static { @@ -41,28 +29,19 @@ public final class WitnessResolutionChecker implements Plugin { } } + private Trees trees; + @Override - public String getName() { - return "WitnessResolutionChecker"; + public synchronized void init(ProcessingEnvironment processingEnv) { + this.trees = Trees.instance(processingEnv); } @Override - public void init(JavacTask task, String... args) { - task.addTaskListener( - new TaskListener() { - @Override - public void finished(TaskEvent e) { - if (e.getKind() != TaskEvent.Kind.ANALYZE) { - return; - } - - if (e.getCompilationUnit() == null) { - return; - } - - new WitnessCallScanner(Trees.instance(task)).scan(e.getCompilationUnit(), null); - } - }); + public boolean process(Set annotations, RoundEnvironment roundEnv) { + for (Element rootElement : roundEnv.getRootElements()) { + new WitnessCallScanner(trees).scan(trees.getPath(rootElement), null); + } + return false; } /** Scanner that finds calls to TypeClasses.witness() and validates them. */ @@ -77,16 +56,16 @@ private WitnessCallScanner(Trees trees) { @Override public Void visitMethodInvocation(MethodInvocationTree node, Void arg) { - Parser.identity() + TreeParser.identity() .guard( - Parser.currentElement() - .flatMap(Parser.methodMatches(WITNESS_METHOD))) - .flatMap(Parser.unaryCallArgument()) - .flatMap(Parser.newAnonymousClassBody()) - .flatMap(Parser.singleImplementsClause()) - .flatMap(Parser.treeTypeMirror()) - .flatMap(Parser.rawTypeMatches(Ty.class)) - .flatMap(Parser.unaryTypeArgument()) + TreeParser.currentElement() + .flatMap(TreeParser.methodMatches(WITNESS_METHOD))) + .flatMap(TreeParser.unaryCallArgument()) + .flatMap(TreeParser.newAnonymousClassBody()) + .flatMap(TreeParser.singleImplementsClause()) + .flatMap(TreeParser.treeTypeMirror()) + .flatMap(TreeParser.rawTypeMatches(Ty.class)) + .flatMap(TreeParser.unaryTypeArgument()) .parse(trees, getCurrentPath(), node) .fold( Unit::unit, @@ -104,135 +83,9 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void arg) { getCurrentPath().getCompilationUnit()); return unit(); }, - plan -> unit())); + _ -> unit())); return super.visitMethodInvocation(node, arg); } } } - -interface Parser { - Maybe parse(Trees trees, TreePath current, T input); - - default Parser flatMap(Parser next) { - return (trees, current, input) -> - this.parse(trees, current, input).flatMap(r -> next.parse(trees, current, r)); - } - - default Parser map(Function mapper) { - return flatMap(mapping(mapper)); - } - - default Parser filter(Predicate predicate) { - return flatMap(filtering(predicate)); - } - - default Parser guard(Parser predicate) { - return (trees, current, input) -> - this.parse(trees, current, input) - .flatMap(r -> predicate.parse(trees, current, r).map(x -> r)); - } - - static Parser identity() { - return (trees, current, input) -> Maybe.just(input); - } - - static Parser mapping(Function mapper) { - return (trees, current, input) -> Maybe.just(mapper.apply(input)); - } - - static Parser filtering(Predicate predicate) { - return (trees, current, input) -> { - if (predicate.test(input)) { - return Maybe.just(input); - } else { - return Maybe.nothing(); - } - }; - } - - static Parser notNull() { - return filtering(Objects::nonNull); - } - - static Parser as(Class cls) { - return (trees, current, input) -> { - if (cls.isInstance(input)) { - return Maybe.just(cls.cast(input)); - } else { - return Maybe.nothing(); - } - }; - } - - static Parser currentElement() { - return (trees, current, input) -> { - Element element = trees.getElement(current); - if (element != null) { - return Maybe.just(element); - } else { - return Maybe.nothing(); - } - }; - } - - static Parser methodMatches(Method target) { - return Parser.as(ExecutableElement.class) - .filter(m -> m.getSimpleName().contentEquals(target.getName())) - .guard( - mapping(ExecutableElement::getEnclosingElement) - .flatMap(as(TypeElement.class)) - .map(TypeElement::getQualifiedName) - .filter(name -> name.contentEquals(target.getDeclaringClass().getName()))); - } - - static Parser unaryCallArgument() { - return mapping(MethodInvocationTree::getArguments) - .filter(list -> list.size() == 1) - .map(List::getFirst); - } - - static Parser newAnonymousClassBody() { - return Parser.as(NewClassTree.class) - .map(NewClassTree::getClassBody) - .flatMap(notNull()); - } - - static Parser singleImplementsClause() { - return mapping(ClassTree::getImplementsClause) - .flatMap(notNull()) - .filter(list -> list.size() == 1) - .map(List::getFirst); - } - - static Parser treeTypeMirror() { - return (trees, current, input) -> { - try { - TypeMirror typeMirror = - trees.getTypeMirror(trees.getPath(current.getCompilationUnit(), input)); - return Maybe.just(typeMirror); - } catch (IllegalArgumentException e) { - return Maybe.nothing(); - } - }; - } - - static Parser rawTypeMatches(Class cls) { - return Parser.as(DeclaredType.class) - .guard( - declaredTypeElement() - .flatMap(as(TypeElement.class)) - .map(TypeElement::getQualifiedName) - .filter(name -> name.contentEquals(cls.getName()))); - } - - static Parser unaryTypeArgument() { - return mapping(DeclaredType::getTypeArguments) - .filter(list -> list.size() == 1) - .map(List::getFirst); - } - - static Parser declaredTypeElement() { - return mapping(DeclaredType::asElement).flatMap(notNull()); - } -} diff --git a/src/main/resources/META-INF/services/com.sun.source.util.Plugin b/src/main/resources/META-INF/services/javax.annotation.processing.Processor similarity index 100% rename from src/main/resources/META-INF/services/com.sun.source.util.Plugin rename to src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/src/test/java/com/garciat/typeclasses/processor/WitnessResolutionProcessorTest.java b/src/test/java/com/garciat/typeclasses/processor/WitnessResolutionCheckerTest.java similarity index 79% rename from src/test/java/com/garciat/typeclasses/processor/WitnessResolutionProcessorTest.java rename to src/test/java/com/garciat/typeclasses/processor/WitnessResolutionCheckerTest.java index cb44185..e31726e 100644 --- a/src/test/java/com/garciat/typeclasses/processor/WitnessResolutionProcessorTest.java +++ b/src/test/java/com/garciat/typeclasses/processor/WitnessResolutionCheckerTest.java @@ -7,15 +7,12 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaFileObject; -import javax.tools.StandardLocation; -import javax.tools.ToolProvider; +import javax.tools.*; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -public class WitnessResolutionProcessorTest { +public class WitnessResolutionCheckerTest { @Nullable @TempDir Path tempDir; @Test @@ -41,12 +38,10 @@ public void test() throws IOException { null, fileManager, diagnostics, - List.of( - "-Xplugin:WitnessResolutionChecker", - "-classpath", - System.getProperty("java.class.path")), + List.of("-classpath", System.getProperty("java.class.path")), null, compilationUnits); + task.setProcessors(List.of(new WitnessResolutionChecker())); // When boolean success = task.call();