Collect tags using the Java Compiler Tree API

This commit is contained in:
naofal.helal
2025-03-24 15:54:42 +03:00
parent f7d058d602
commit 9d67b770da
10 changed files with 338 additions and 2 deletions

View File

@@ -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() {

View File

@@ -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<String> 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
*

View File

@@ -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] <sources...>
""");
}
}

View File

@@ -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<Level, Integer> colors =
new HashMap<Level, Integer>() {
{
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));
}
}
}
}

View File

@@ -0,0 +1,10 @@
package xyz.naofal.jtags;
public record Tag(TagKind kind, String name, String location, String line)
implements Comparable<Tag> {
@Override
public int compareTo(Tag o) {
return String.CASE_INSENSITIVE_ORDER.compare(name, o.name());
}
}

View File

@@ -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<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjects(options.sources);
JavacTask task =
(JavacTask) compiler.getTask(null, fileManager, null, null, null, compilationUnits);
Iterable<? extends CompilationUnitTree> 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;
}
}

View File

@@ -0,0 +1,12 @@
package xyz.naofal.jtags;
public enum TagKind {
PACKAGE,
CLASS,
INTERFACE,
ANNOTATION,
ENUM,
FIELD,
ENUM_CONSTANT,
METHOD;
}

View File

@@ -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<Void, TreeVisitorContext> {
public PriorityQueue<Tag> 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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 {}
//