javac 树 API
Sundar Athijegannathan 于 2021 年 9 月 20 日最近在 Twitter 上关于特定(Java)编码风格的讨论引发了 Richard Startin 的一个有趣问题。
有人知道如何使用 checkstyle 禁止循环归纳变量的预增量吗?
— Richard Startin (@richardstartin) 2021 年 9 月 16 日
例如,禁止
`for (int I = 0; i < n; ++i)`
但不禁止
`for (int i = 0; i < n; i++)`
问题之一是您最喜欢的代码风格分析工具可能不支持您想要施加的所有编码风格!
javac 作为库
这是 javac 树 API 可以提供帮助的地方,因为javac
不仅仅是一个命令行工具。它也可以用作库。 javac
(以及其他一些 JDK bin 工具)支持 ToolProvider API。使用此 API,您可以获取 JavaCompiler 实例。使用此编译器实例,您可以创建一个 JavaCompiler.CompilationTask。设置编译任务后,您可以在其上使用 call 方法来启动编译。
但在这种情况下,您不想编译 Java 源文件,因为您只需要解析源文件以获取 抽象语法树。CompilationTask 有一个称为 JavacTask 的子类型。此类提供对编译过程的更精细控制。 parse 方法是必需的方法。还有用于后续编译步骤的其他方法,例如 analyze 用于类型检查,generate 用于代码生成,等等。
树 API 的解析方法
parse 方法返回 CompilationUnitTree 对象的列表。CompilationUnitTree 是 Tree 的子类型。为 Java 源文件获取编译单元树后,您可以使用 访问者模式 遍历树以执行必要的编码风格检查。要访问树,Tree 接口支持 accept 方法。您只需要实现 TreeVisitor 传递给 accept 方法即可。
访问者和 ForLoopTree
对于特定的编码风格检查器,您实际上并不关心 TreeVisitor 的所有 visitXYZ 方法!您只关心 visitForLoop 方法。幸运的是,JDK 已经提供了一个 TreeScanner 类。您只需要对 TreeScanner 进行子类化并覆盖 visitForLoop 以进行分析。在您的 visitForLoop 方法中,您将获得一个 ForLoopTree。从 ForLoopTree 对象中,您可以使用 getUpdate 方法获取循环更新表达式。如果有任何更新表达式(请注意,for
循环不一定总是有更新表达式,它可以是空的!),您必须查看它是否是 UnaryTree 类型 PREFIX_INCREMENT 或 PREFIX_DECREMENT。如果您发现它,您必须使用文件名、行号和列号报告它。就是这样!
实现编码风格检查器的完整 Java 代码如下
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of Oracle nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.*;
import javax.tools.*;
import com.sun.source.tree.*;
import com.sun.source.util.*;
public class CheckForPreIncrement {
public static void main(String[] args) throws Exception {
// a single argument that is directory from which .java sources are scanned.
// If no argument supplied, use the current directory
Path path = Paths.get(args.length == 0? "." : args[0]);
// walk the file system from the given path recursively
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
// if exception (say non-readable dir), just print and continue scanning.
if (exc != null) {
System.err.printf("dir visit failed for %s : %s\n", dir, exc);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
// if a file cannot be read, just print error and continue scanning
if (exc != null) {
System.err.printf("file visit failed for %s : %s\n", file, exc);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
// is this a .java file?
if (file.getFileName().toString().endsWith(".java")) {
try {
// check for ++i and --i pattern and report
check(file.toAbsolutePath());
} catch (IOException exc) {
// report parse failures and continue scanning other files
System.err.printf("parsing failed for %s : %s\n", file, exc);
}
}
return FileVisitResult.CONTINUE;
}
});
}
// major version of JDK such as 16, 17, 18 etc.
private static int getJavaMajorVersion() {
return Runtime.version().feature();
}
// javac options we pass to the compiler. We enable preview so that
// all preview features can be parsed.
private static final List<String> OPTIONS =
List.of("--enable-preview", "--release=" + getJavaMajorVersion());
// get the system java compiler instance
private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
private static void check(Path javaSrc) throws IOException {
// create a compilation task (JavacTask) for the given java source file
var compUnits = compiler.
getStandardFileManager(null, null, null).
getJavaFileObjects(javaSrc);
// we need to cast to JavacTask so that we can call parse method
var task = (JavacTask) compiler.getTask(null, null, null,
OPTIONS, null, compUnits);
// we need this to report line and column numbers of coding patterns we find
var sourcePositions = Trees.instance(task).getSourcePositions();
// TreeVisitor implementation using TreeScanner
var scanner = new TreeScanner<Void, Void>() {
private CompilationUnitTree compUnit;
private LineMap lineMap;
private String fileName;
// store details of the current compilation unit in instance vars
@Override
public Void visitCompilationUnit(CompilationUnitTree t, Void v) {
compUnit = t;
lineMap = t.getLineMap();
fileName = t.getSourceFile().getName();
return super.visitCompilationUnit(t, v);
}
// found a for loop to analyze
@Override
public Void visitForLoop(ForLoopTree t, Void v) {
// check each update expression
for (var est : t.getUpdate()) {
// is this a UnaryTree expression statement?
if (est.getExpression() instanceof UnaryTree unary) {
// is this prefix decrement or increment?
var kind = unary.getKind();
if (kind == Tree.Kind.PREFIX_DECREMENT ||
kind == Tree.Kind.PREFIX_INCREMENT) {
// report file name, line number and column number
var pos = sourcePositions.getStartPosition(compUnit, unary);
var line = lineMap.getLineNumber(pos);
var col = lineMap.getColumnNumber(pos);
System.out.printf("Found ++i or --i in %s %d:%d\n",
fileName, line, col);
}
}
}
return super.visitForLoop(t, v);
}
};
// visit each compilation unit tree object with our scanner
for (var compUnitTree : task.parse()) {
compUnitTree.accept(scanner, null);
}
}
}
有趣的趣闻
此特定编码风格检查器在最新的 OpenJDK 源代码中发现了 905 个上述“for 循环更新模式”的实例