Java 编译时替换(带注释的方法/满足条件的方法)

问题描述 投票:0回答:1

我可以想象这是不好的做法,但这纯粹是为了一个私人项目以及作为我自己的练习。

我有一个类,它有很多不同的静态方法。它们用于测试一些有关 Java 的东西(我仍在学习 Java,所以我在那里放了不同的东西,我想测试自己,比如继承如何工作等)。

其中一些方法会生成编译时错误,因此我的项目将无法构建。我想创建一个像

@DoNotCompile
这样的注释,它将标记该方法,并且在编译之前该方法将被替换为空主体或类似
throw new RuntimeError("This method should not be called")
之类的东西。

目前我只有

@Test
表示将要执行的方法,
@NotTest
表示不应该执行的方法,然后我将正文放在注释中。我的目标是学习 - 我想看看我的编辑器中会产生哪些错误,但在编译时忽略它。

如果有帮助的话,我正在 IntelliJ IDEA 2023.1 社区版上使用 Java 17 进行编码。 我使用 gradle,但我对此经验不是很丰富,但也许有一些 gradle 脚本可以实现这一点?

感谢任何帮助,并再次:我知道这是不好的做法,我只是很好奇是否可以实现。

编辑:如果我能以某种方式记录此过程中的编译时错误,并且可能用类似

sout("Did not compile: <errors or warnings from compiler>");

之类的内容替换正文,那就太好了

编辑2:最后我只是编写了一个用于预处理的node.js脚本,并将其作为外部工具添加到我在IntelliJ中的运行配置中。此外,我删除了正常的构建命令,并将类路径替换为我处理的文件路径。现在工作起来就像一个魅力,但它有一些缺点:我只在运行代码时测试特定的注释,所以它不灵活。另外,我不使用解析器来获取该方法,因此它只是假设保留代码样式。我将很快上传脚本作为答案。

java reflection compiler-errors annotations compile-time
1个回答
0
投票

作为一种解决方法,我只能创建一个脏预处理器,以下是您需要的设置: (注意:我使用的是 IntelliJ,如果您使用其他 IDE,这可能会有所不同)

  1. 安装NodeJS来运行Javascript(碰巧我已经有了它,理论上你也可以使用任何其他语言,甚至Java本身来重写那个预处理器)
  2. 在可访问的位置创建一个包含以下内容的文件:
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));
    });
})();
  1. 在 IntelliJ 中创建外部工具: 不要忘记添加 classPath 作为参数;如果您向脚本添加一些调试行,您可以选择启用“打开控制台”
  2. 在运行配置中添加此工具,删除“build”并修改您的 classPath,如下所示: 您可以通过单击“修改选项”来访问此选项。如果您无法添加类路径,因为它尚不存在 - 创建一个具有该名称的空文件夹并重复该过程。

注意事项:

  • 脏语法解析。由于Javac无法提供除错误之外的一些有用信息,因此我在行首使用空格来确定方法的开始和结束位置,因此只允许这种格式
  • 现在我使用硬编码注释
    @Test
    来查找此类方法,如果你想使用其他注释,你必须自己修改它
  • 包含错误而没有注释的方法会破坏处理器
  • @Test
    注释不必是最后一个,但建议仍将其设为最后一个

预处理器的流程:

  1. 尝试使用给定的设置进行编译
  2. 记录发生的任何错误
  3. 解析方法并注释掉主体,添加
    throw new RuntimeException("Compile time error was encountered: <error(s)>")
  4. 将所有代码保存到
    processed/src
    文件夹
  5. 将代码编译到
    processed/out
    文件夹
  6. 由于我们修改了 IntelliJ 中的 classPath,它现在使用我们处理过的编译代码来运行 -> 编译时不会显示错误,错误仍保留在我最初想要的代码中。
© www.soinside.com 2019 - 2024. All rights reserved.