This commit is contained in:
2025-03-26 21:46:29 +03:00
parent 2d3d4b4de4
commit 011e46ec0f
8 changed files with 321 additions and 50 deletions

View File

@@ -10,6 +10,20 @@ import java.util.stream.Collectors;
public class Build {
static final String program = "jtags";
static void printUsage() {
System.err.println(
"""
Usage: java Build <subcommand> [args...]
Subcommands:
build Build %1$s
run [args...] Run %1$s with arguments
package Package %1$s to a JAR file
[file] Output JAR filename [Default: Jtags.jar]
test Run %1$s tests
"""
.formatted(program));
}
public static void main(String[] args) {
rebuildSelf(Build.class, args);
@@ -47,6 +61,11 @@ public class Build {
packageJar(Optional.ofNullable(arguments.poll()));
break;
case "test":
buildJtags(mainClass, sourcePaths, classPaths);
runTests();
break;
default:
logger.severe("Unknown subcommand: " + subcommand);
printUsage();
@@ -77,16 +96,17 @@ public class Build {
".");
}
static void printUsage() {
System.err.println(
"""
Usage: java Build <subcommand> [args...]
Subcommands:
build Build %1$s
run [args...] Run %1$s with arguments
package Package %1$s to a JAR file
[file] Output JAR filename [Default: Jtags.jar]
"""
.formatted(program));
static void runTests() {
String mainClass = "TestJtags";
String[] sourcePaths = glob("src/test/java/**.java");
if (classNeedsRebuild(mainClass, sourcePaths)) {
logger.info("Compiling %s...".formatted(mainClass));
if (!compileJava(sourcePaths)) {
System.exit(1);
}
}
System.exit(runJava(new String[0], new String[] {"-enableassertions"}, mainClass));
}
}

View File

@@ -6,7 +6,7 @@ import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Trees;
import java.io.IOException;
import java.util.PriorityQueue;
import java.util.AbstractQueue;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
@@ -17,7 +17,7 @@ public class TagCollector {
static JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
public static PriorityQueue<Tag> collectTags(Options options) {
public static AbstractQueue<Tag> collectTags(Options options) {
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
Iterable<? extends JavaFileObject> compilationUnits =
@@ -26,11 +26,14 @@ public class TagCollector {
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(options);
TreeVisitorContext context = new TreeVisitorContext(Trees.instance(task));
for (CompilationUnitTree compilationUnitTree : trees) {
treeVisitor.scan(compilationUnitTree, context);
}
return treeVisitor.tags;
} catch (IOException ex) {

View File

@@ -9,20 +9,20 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.PriorityQueue;
import java.util.AbstractQueue;
import xyz.naofal.jtags.Jtags.Options;
public record TagsWriter(Options options) {
private static final int MAX_PATTERN_LENGTH = 96;
public boolean writeTagsFile(PriorityQueue<Tag> tags) {
public boolean writeTagsFile(AbstractQueue<Tag> tags) {
try (var outputStream = new FileOutputStream(options().output.toFile());
var writer = new OutputStreamWriter(outputStream); ) {
writer.write(
"""
!_TAG_FILE_ENCODING\tutf-8\t
!_TAG_FILE_ENCODING\tutf-8
!_TAG_FILE_SORTED\t2\t/0=unsorted, 1=sorted, 2=foldcase/
""");
@@ -48,7 +48,7 @@ public record TagsWriter(Options options) {
? tag.location().toString()
: options.output.getParent().toAbsolutePath().relativize(tag.location()).toString());
writer.write("\t/^");
writer.write(tag.line().substring(0, Math.min(tag.line().length(), MAX_PATTERN_LENGTH)));
writer.write(tag.line());
writer.write("$/;\"\t");
writer.write(
switch (tag.kind()) {

View File

@@ -9,16 +9,17 @@ import com.sun.source.tree.PackageTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePathScanner;
import java.util.AbstractQueue;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.PriorityQueue;
import java.util.concurrent.PriorityBlockingQueue;
import javax.lang.model.element.Modifier;
import xyz.naofal.jtags.Jtags.Options;
public class TreeVisitor extends TreePathScanner<Void, TreeVisitorContext> {
public final Options options;
public final PriorityQueue<Tag> tags = new PriorityQueue<>();
public final AbstractQueue<Tag> tags = new PriorityBlockingQueue<>();
public TreeVisitor(Options options) {
this.options = options;
@@ -104,7 +105,7 @@ public class TreeVisitor extends TreePathScanner<Void, TreeVisitorContext> {
@Override
public Void visitMethod(MethodTree node, TreeVisitorContext p) {
ClassTree enclosingType = (ClassTree)getCurrentPath().getParentPath().getLeaf();
ClassTree enclosingType = (ClassTree) getCurrentPath().getParentPath().getLeaf();
if (options.excludeNonPublic
&& !node.getModifiers().getFlags().contains(Modifier.PUBLIC)

View File

@@ -1,29 +0,0 @@
package xyz.naofal.jtags.example;
@interface T {
static int t = 1;
}
public class Example {
public static void lorem(
String string1,
String string2,
String string3,
String string4,
String string5,
String string6) {
Object o =
new Object() {
void f() {}
;
};
class T {
void f() {}
private void fp() {}
}
}
}

View File

@@ -0,0 +1,34 @@
import static notest.Test.Util;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import notest.Test;
class TestJtags {
public static final String Jtags = "xyz.naofal.jtags.Jtags";
public static void main(String[] args) {
System.exit(Util.runTests(TestJtags.class) ? 0 : 1);
}
@Test
static void basicExample() throws IOException {
Path file = Files.createTempFile("jtags", null);
Util.runJava(
new String[0],
new String[] {"-Dlogger.level=CONFIG"},
Jtags,
"-o",
file.toString(),
"src/test/java/examples/BasicExample.java");
assert Files.readString(file)
.trim()
.equals(
"""
ABC
"""
.trim())
: "Unexpected Result:\n" + Files.readString(file);
}
}

View File

@@ -0,0 +1,28 @@
package examples;
public class BasicExample {
private boolean method1() {
return true;
}
public static void method2(
String string1,
String string2,
String string3,
String string4,
String string5,
String string6) {
Object innerObject =
new Object() {
void method2() {}
};
class InnerClass {
void method3() {}
private void method4() {}
}
}
}

View File

@@ -0,0 +1,214 @@
/*
* NoTest - Simple testing utilities
*
* MIT License
*
* Copyright (c) 2025 Naofal Helal
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package notest;
import java.io.File;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.IntSummaryStatistics;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** Marks a method as a test method */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
/** Utility methods for performing tests */
public static class Util {
public static Path javaHome = Paths.get(System.getProperty("java.home"));
public static String javaClassPath = System.getProperty("java.class.path");
public static String javaBin = javaHome.resolve("bin", "java").toString();
/** Runs all tests in {@code testClass} */
public static boolean runTests(Class<?> testClass) {
IntSummaryStatistics stats =
Arrays.stream(testClass.getDeclaredMethods())
.filter(it -> it.isAnnotationPresent(Test.class))
.map(
it -> {
try {
System.err.println(
"" + center(" Test " + it.getName() + " ", 48, '─') + "");
it.setAccessible(true);
it.invoke(null);
System.err.println(
"" + center(" " + it.getName() + ": SUCCESS ", 48, '─') + "");
System.err.println();
return 1;
} catch (IllegalAccessException ex) {
System.err.println(
""
+ center(" ERROR: could not run " + it.getName() + " ", 48, '─')
+ "");
System.err.println(ex.toString());
System.err.println();
return 0;
} catch (InvocationTargetException ex) {
System.err.println(
""
+ center(
" " + ex.getTargetException().getClass().getSimpleName() + " ",
48,
'─')
+ "");
Optional.ofNullable(ex.getTargetException().getMessage())
.orElse("")
.lines()
.forEach(line -> System.err.println("" + line));
System.err.println(
"" + center(" " + it.getName() + ": FAIL ", 48, '─') + "");
System.err.println();
return 0;
}
})
.collect(Collectors.summarizingInt(it -> it));
System.err.printf(
"Ran %d tests; %d Succeeded, %d Failed%n"
.formatted(stats.getCount(), stats.getSum(), stats.getCount() - stats.getSum()));
return stats.getCount() == stats.getSum();
}
/**
* Runs a Java class
*
* @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 mainClass, String... args) {
return runJava(new String[0], new String[0], mainClass, args);
}
/**
* 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));
}
/**
* Runs a shell command. May throw an {@code IOException}
*
* @return Exit status code
* @throws Exception
*/
public static int command(String... command) {
ProcessBuilder pb =
new ProcessBuilder(command)
.redirectInput(Redirect.INHERIT)
.redirectOutput(Redirect.INHERIT)
.redirectError(Redirect.INHERIT);
Process process;
try {
process = pb.start();
process.waitFor();
return process.exitValue();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* Returns file paths that match a {@code globPattern}
*
* @see java.nio.file.FileSystem#getPathMatcher(String) getPathMatcher
*/
public static String[] glob(String globPattern) {
Path cwd = Paths.get(".");
PathMatcher pathMatcher =
FileSystems.getDefault()
.getPathMatcher(
String.join("", "glob:", cwd.toString(), File.separator, globPattern));
try (@SuppressWarnings("unused")
Stream<Path> paths =
Files.find(
cwd,
Integer.MAX_VALUE,
(path, basicFileAttributes) -> pathMatcher.matches(path))) {
return paths.map(Path::toString).toArray(String[]::new);
} catch (IOException ex) {
throw new RuntimeException("Error during glob pattern matching", ex);
}
}
/**
* Pads string to center.
*
* <p>From <a href="https://stackoverflow.com/a/8155547">stackoverflow.com/a/8155547</a>
*/
private static String center(String s, int size, char pad) {
if (s == null || size <= s.length()) return s;
StringBuilder sb = new StringBuilder(size);
for (int i = 0; i < (size - s.length()) / 2; i++) {
sb.append(pad);
}
sb.append(s);
while (sb.length() < size) {
sb.append(pad);
}
return sb.toString();
}
}
}