I can imagine this is bad practice, but this is purely for a private project and as an exercise for myself.
I have a Class, which has a lot different static methods. They are used to test some stuff about Java (I am still learning Java, so I put different things there which I want to test myself like how Inheritance works etc.).
Some of these methods will generate compile-time errors, so my Project will not build. I want to create an annotation like @DoNotCompile
, which will mark the method, and before compiling the method will be replaced with empty body or something like throw new RuntimeError("This method should not be called")
.
Currently I just have @Test
for methods which will be executed and @NotTest
for methods which should not be executed, then I put the body in comments. My goal with this is learning - I want to see, which error would be produced in my Editor, but ignore it when compiling.
I am coding in Java 17, on IntelliJ IDEA 2023.1 Community Edition if this helps. I use gradle, but I am not very experienced in it, but maybe there is some gradle script which can achieve this?
Any help is appreciated, and once again: I know this is bad practice, I am just very curious if this can be achieved.
Edit: It would also be great, if I can somehow record the compile-time error in this process and maybe replace the body with something like sout("Did not compile: <errors or warnings from compiler>");
Edit 2: In the end I just wrote a node.js script for preprocessing and added it as external tool to my run configuration in IntelliJ. In addition I removed the normal build command and replaced the classpath with my processed file path. Now works like a charm, but it has some disadvantages: I only test for a specific annotation, when running the code, so it is not flexible. Also I do not use a parser to get the method, so it just assumes, that the code style is kept. I will upload the script as an answer shortly after.
As a workaround I was only able to create a dirty pre-processor, here is the setup you will need: (Note: I am using IntelliJ, this might differ in your installation if you use another IDE)
const fs = require("fs");
const path = require("path");
const { exec } = require("child_process");
function *fromDir(startPath, filter) {
if (!fs.existsSync(startPath)) {
console.log("no dir", startPath);
return;
}
const files = fs.readdirSync(startPath);
for (let i = 0; i < files.length; i++) {
const filename = path.join(startPath, files[i]);
const stat = fs.lstatSync(filename);
if (stat.isDirectory()) {
for (const f of fromDir(filename, filter)) {
yield f;
}
} else if (filename.endsWith(filter)) {
yield filename;
};
};
};
(async () => {
const classpath = process.argv[2] || "";
const files = [...fromDir("src", ".java")];
const output = await new Promise(res => {
exec("javac -cp \"" + classpath + "\" -d tmp " + files.join(" "), (err, out, serr) => res(serr));
});
const errors = output.trim().split("\r\n").filter(line => files.some(file => line.includes(file)));
if (fs.existsSync("processed")) {
fs.rmSync("processed", { "recursive": true, "force": true });
}
fs.mkdirSync("processed");
fs.cpSync("src", "processed/src", { "recursive": true });
for (const file of files) {
// contains errors then process
const ferr = errors.filter(error => error.includes(file));
if (ferr.length) {
const content = fs.readFileSync(file, "utf8").split(/\r?\n/);
const methods = new Set();
const lookup = { };
for (const error of ferr) {
const line = parseInt(error.slice(file.length + 1).split(":")[0]);
const issue = error.split("error: ").slice(1).join("error: ");
for (let i = line;; i --) {
if (content[i].trim().endsWith("@Test")) {
methods.add(i + 1);
if (!((i+1) in lookup)) lookup[i + 1] = [];
lookup[i + 1].push(issue);
break;
}
}
}
for (const method of methods) {
let search = true;
let tabs;
for (let i = method;; i ++) {
if (search) {
if (content[i].endsWith("{")) search = false;
tabs = content[i].match(/^[ ]*/)[0].length;
const issues = lookup[method].join("\\n").replaceAll("\"", "\\\"");
content[i] = content[i] + " throw new RuntimeException(\"Compile time error was encountered:\\n" + issues + "\");"
} else {
const start = content[i].match(/^[ ]*/)[0];
if (content[i].endsWith("}") && start.length == tabs) break;
if (!content[i].length) continue;
content[i] = start + "//" + content[i].slice(start.length);
}
}
}
fs.writeFileSync(path.join("processed", file), content.join("\n"));
}
}
if (fs.existsSync("tmp")) {
fs.rmSync("tmp", { "recursive": true, "force": true });
}
await new Promise(res => {
exec("javac -cp \"" + classpath + "\" -d processed/out " + files.map(file => path.join("processed", file)).join(" "), (err, out, serr) => res(serr));
});
})();
Caveats:
@Test
to find such methods, you will have to modify it yourself if you want to use another annotation@Test
annotation does not have to be the last one, but it is advised to still make it the last oneThe procedure of the pre-processor:
throw new RuntimeException("Compile time error was encountered: <error(s)>")
processed/src
folderprocessed/out
folder