From 9d67b770da52347be92d44245702dc24e1edbcf8 Mon Sep 17 00:00:00 2001 From: "naofal.helal" Date: Mon, 24 Mar 2025 15:54:42 +0300 Subject: [PATCH] Collect tags using the Java Compiler Tree API --- Build.java | 9 ++- nobuild/NoBuild.java | 30 +++++++ src/main/java/xyz/naofal/jtags/Jtags.java | 30 ++++++- .../java/xyz/naofal/jtags/JtagsLogger.java | 63 +++++++++++++++ src/main/java/xyz/naofal/jtags/Tag.java | 10 +++ .../java/xyz/naofal/jtags/TagCollector.java | 40 ++++++++++ src/main/java/xyz/naofal/jtags/TagKind.java | 12 +++ .../java/xyz/naofal/jtags/TreeVisitor.java | 78 +++++++++++++++++++ .../xyz/naofal/jtags/TreeVisitorContext.java | 57 ++++++++++++++ .../xyz/naofal/jtags/example/Example.java | 11 +++ 10 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 src/main/java/xyz/naofal/jtags/JtagsLogger.java create mode 100644 src/main/java/xyz/naofal/jtags/Tag.java create mode 100644 src/main/java/xyz/naofal/jtags/TagCollector.java create mode 100644 src/main/java/xyz/naofal/jtags/TagKind.java create mode 100644 src/main/java/xyz/naofal/jtags/TreeVisitor.java create mode 100644 src/main/java/xyz/naofal/jtags/TreeVisitorContext.java create mode 100644 src/main/java/xyz/naofal/jtags/example/Example.java diff --git a/Build.java b/Build.java index a319d11..6b0d7be 100644 --- a/Build.java +++ b/Build.java @@ -1,6 +1,8 @@ import static nobuild.NoBuild.*; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.stream.Collectors; public class Build { static final String program = "jtags"; @@ -18,7 +20,12 @@ public class Build { compileJava(classPaths, sourcePaths); } - runJava(classPaths, mainClass, args); + var argumentPartitions = + Arrays.stream(args).collect(Collectors.partitioningBy(it -> it.startsWith("-D"))); + String[] javaArguments = argumentPartitions.get(true).toArray(String[]::new); + String[] programArguments = argumentPartitions.get(false).toArray(String[]::new); + + runJava(classPaths, javaArguments, mainClass, programArguments); } private static void ensureDependencies() { diff --git a/nobuild/NoBuild.java b/nobuild/NoBuild.java index 7e1ed67..2245621 100644 --- a/nobuild/NoBuild.java +++ b/nobuild/NoBuild.java @@ -231,6 +231,36 @@ public class NoBuild { return command(commandLineStream.toArray(String[]::new)); } + /** + * Runs a Java class + * + * @param additionalClassPaths Additional class paths to pass to the compiler + * @param javaArguments Arguments to pass to the java binary + * @param mainClass The fully qualified class name, e.g. {@code com.example.MyClass$MySubClass} + * @param args Arguments to pass to the main method + * @return Exit status code + */ + public static int runJava( + String[] additionalClassPaths, String[] javaArguments, String mainClass, String... args) { + String pathSeparator = System.getProperty("path.separator"); + + String classPaths = + String.join( + pathSeparator, + Stream.concat(Stream.of(javaClassPath), Arrays.stream(additionalClassPaths)) + .toArray(String[]::new)); + + Stream commandLineStream = + Stream.of( + Stream.of(javaBin.toString(), "-cp", classPaths), + Arrays.stream(javaArguments), + Stream.of(mainClass), + Arrays.stream(args)) + .flatMap(it -> it); + + return command(commandLineStream.toArray(String[]::new)); + } + /** * Compiles java sources * diff --git a/src/main/java/xyz/naofal/jtags/Jtags.java b/src/main/java/xyz/naofal/jtags/Jtags.java index 6b7a5d3..ca2eac9 100644 --- a/src/main/java/xyz/naofal/jtags/Jtags.java +++ b/src/main/java/xyz/naofal/jtags/Jtags.java @@ -1,7 +1,35 @@ package xyz.naofal.jtags; +import java.util.ArrayDeque; +import java.util.Arrays; + public class Jtags { + static class Options { + String[] sources; + } + public static void main(String[] args) { - System.out.println("Hello"); + var arguments = new ArrayDeque<>(Arrays.asList(args)); + + if (arguments.size() == 0) { + printUsage(); + System.exit(1); + } + + Options options = new Options(); + options.sources = args; + + System.exit(run(options) ? 0 : 1); + } + + static boolean run(Options options) { + return TagCollector.run(options); + } + + static void printUsage() { + System.err.println( + """ + USAGE: jtags [options] + """); } } diff --git a/src/main/java/xyz/naofal/jtags/JtagsLogger.java b/src/main/java/xyz/naofal/jtags/JtagsLogger.java new file mode 100644 index 0000000..7ab4bd3 --- /dev/null +++ b/src/main/java/xyz/naofal/jtags/JtagsLogger.java @@ -0,0 +1,63 @@ +package xyz.naofal.jtags; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +public class JtagsLogger { + public static Logger logger = Logger.getLogger("logger"); + public static Handler loggingHandler = new LogHandler(); + + static { + logger.setUseParentHandlers(false); + logger.addHandler(loggingHandler); + String level; + if ((level = System.getProperty("logger.level")) != null) { + logger.setLevel(Level.parse(level)); + } + } + + public static class LogHandler extends Handler { + @Override + public void close() { + flush(); + } + + @Override + public void flush() { + System.err.flush(); + } + + public static Map colors = + new HashMap() { + { + put(Level.FINEST, 5); + put(Level.FINE, 4); + put(Level.FINER, 6); + put(Level.INFO, 2); + put(Level.WARNING, 3); + put(Level.SEVERE, 1); + } + }; + + @Override + public void publish(LogRecord record) { + if (record.getMessage() != null && !record.getMessage().isEmpty()) { + // if (System.console().isTerminal()) { + int color = colors.get(record.getLevel()); + System.err.printf( + "\u001b[38;5;%dm[%s]\u001b[0m %s%n", + color, record.getLevel().getName(), record.getMessage()); + // } else { + // System.err.printf("[%s] %s%n", record.getLevel().getName(), record.getMessage()); + // } + } + if (record.getThrown() != null) { + System.err.println(record.getThrown().toString().indent(4)); + } + } + } +} diff --git a/src/main/java/xyz/naofal/jtags/Tag.java b/src/main/java/xyz/naofal/jtags/Tag.java new file mode 100644 index 0000000..9256ee2 --- /dev/null +++ b/src/main/java/xyz/naofal/jtags/Tag.java @@ -0,0 +1,10 @@ +package xyz.naofal.jtags; + +public record Tag(TagKind kind, String name, String location, String line) + implements Comparable { + + @Override + public int compareTo(Tag o) { + return String.CASE_INSENSITIVE_ORDER.compare(name, o.name()); + } +} diff --git a/src/main/java/xyz/naofal/jtags/TagCollector.java b/src/main/java/xyz/naofal/jtags/TagCollector.java new file mode 100644 index 0000000..af342b9 --- /dev/null +++ b/src/main/java/xyz/naofal/jtags/TagCollector.java @@ -0,0 +1,40 @@ +package xyz.naofal.jtags; + +import static xyz.naofal.jtags.JtagsLogger.logger; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.Trees; +import java.io.IOException; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import xyz.naofal.jtags.Jtags.Options; + +public class TagCollector { + + static JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + + public static boolean run(Options options) { + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) { + + Iterable compilationUnits = + fileManager.getJavaFileObjects(options.sources); + + JavacTask task = + (JavacTask) compiler.getTask(null, fileManager, null, null, null, compilationUnits); + Iterable trees = task.parse(); + TreeVisitorContext context = new TreeVisitorContext(Trees.instance(task)); + TreeVisitor treeVisitor = new TreeVisitor(); + for (CompilationUnitTree compilationUnitTree : trees) { + compilationUnitTree.accept(treeVisitor, context); + } + + } catch (IOException ex) { + logger.severe(ex.toString()); + } + + return true; + } +} diff --git a/src/main/java/xyz/naofal/jtags/TagKind.java b/src/main/java/xyz/naofal/jtags/TagKind.java new file mode 100644 index 0000000..b2aa63c --- /dev/null +++ b/src/main/java/xyz/naofal/jtags/TagKind.java @@ -0,0 +1,12 @@ +package xyz.naofal.jtags; + +public enum TagKind { + PACKAGE, + CLASS, + INTERFACE, + ANNOTATION, + ENUM, + FIELD, + ENUM_CONSTANT, + METHOD; +} diff --git a/src/main/java/xyz/naofal/jtags/TreeVisitor.java b/src/main/java/xyz/naofal/jtags/TreeVisitor.java new file mode 100644 index 0000000..9f121b5 --- /dev/null +++ b/src/main/java/xyz/naofal/jtags/TreeVisitor.java @@ -0,0 +1,78 @@ +package xyz.naofal.jtags; + +import static xyz.naofal.jtags.JtagsLogger.logger; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.PackageTree; +import com.sun.source.util.SimpleTreeVisitor; +import java.util.PriorityQueue; + +public class TreeVisitor extends SimpleTreeVisitor { + public PriorityQueue tags = new PriorityQueue<>(); + + @Override + public Void visitCompilationUnit(CompilationUnitTree node, TreeVisitorContext p) { + p.compilationUnitTree = node; + + logger.finer("Collecting tags in file: " + p.getLocation()); + + PackageTree packageTree = node.getPackage(); + if (packageTree != null) { + packageTree.accept(this, p); + } + + node.getTypeDecls().forEach(it -> it.accept(this, p)); + + return null; + } + + @Override + public Void visitPackage(PackageTree node, TreeVisitorContext p) { + Tag tag = + new Tag( + TagKind.PACKAGE, node.getPackageName().toString(), p.getLocation(), p.getLine(node)); + + logger.finer(() -> "Package: " + tag); + + tags.add(tag); + + return null; + } + + @Override + public Void visitClass(ClassTree node, TreeVisitorContext p) { + Tag tag = + new Tag( + switch (node.getKind()) { + case CLASS -> TagKind.CLASS; + case INTERFACE -> TagKind.INTERFACE; + case ENUM -> TagKind.ENUM; + case ANNOTATION_TYPE -> TagKind.ANNOTATION; + default -> { + logger.warning("Unknown class kind " + node.getKind()); + yield TagKind.CLASS; + } + }, + node.getSimpleName().toString(), + p.getLocation(), + p.getLine(node)); + + logger.finer(() -> "Type: " + tag); + + tags.add(tag); + + node.getMembers().forEach(it -> it.accept(this, p)); + + return null; + } + + @Override + public Void visitMethod(MethodTree node, TreeVisitorContext p) { + Tag tag = new Tag(TagKind.METHOD, node.getName().toString(), p.getLocation(), p.getLine(node)); + logger.fine(() -> "Method: " + tag); + tags.add(tag); + return null; + } +} diff --git a/src/main/java/xyz/naofal/jtags/TreeVisitorContext.java b/src/main/java/xyz/naofal/jtags/TreeVisitorContext.java new file mode 100644 index 0000000..82e5473 --- /dev/null +++ b/src/main/java/xyz/naofal/jtags/TreeVisitorContext.java @@ -0,0 +1,57 @@ +package xyz.naofal.jtags; + +import static xyz.naofal.jtags.JtagsLogger.logger; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.Trees; +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Paths; + +public class TreeVisitorContext { + private static final int MAX_LINE_LENGTH = 4096; + + final Trees trees; + final SourcePositions sourcePositions; + public CompilationUnitTree compilationUnitTree; + + public TreeVisitorContext(Trees trees) { + this.trees = trees; + sourcePositions = trees.getSourcePositions(); + } + + public String getLocation() { + assert compilationUnitTree != null; + return Paths.get(".") + .toAbsolutePath() + .relativize(Paths.get(compilationUnitTree.getSourceFile().toUri()).toAbsolutePath()) + .toString(); + } + + public String getLine(Tree node) { + assert compilationUnitTree != null; + long offset = sourcePositions.getStartPosition(compilationUnitTree, node); + try (var reader = new BufferedReader(compilationUnitTree.getSourceFile().openReader(true))) { + long skipped = 0; + while (skipped <= offset) { + reader.mark(MAX_LINE_LENGTH); + String line = reader.readLine(); + if (line == null) break; + int lineLength = line.length(); + if (lineLength >= MAX_LINE_LENGTH) { + logger.severe("Line length exceeded"); + System.exit(1); + } + skipped += lineLength + 1; + } + reader.reset(); + return reader.readLine(); + } catch (IOException e) { + logger.severe(e.toString()); + System.exit(1); + return null; + } + } +} diff --git a/src/main/java/xyz/naofal/jtags/example/Example.java b/src/main/java/xyz/naofal/jtags/example/Example.java new file mode 100644 index 0000000..3d724d1 --- /dev/null +++ b/src/main/java/xyz/naofal/jtags/example/Example.java @@ -0,0 +1,11 @@ +package xyz.naofal.jtags.example; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Example {} +//