JavaParser入门:以编程方式分析Java代码

我最喜欢的事情之一是解析代码并对其执行自动操作。 因此,我开始为JavaParser做出贡献,并创建了两个相关项目: java-symbol-solver和Effectivejava 。

作为JavaParser的贡献者,我反复阅读了一些非常类似的问题,这些问题涉及从Java源代码提取信息。 因此,我认为我可以帮助提供一些简单的示例,以帮助您开始解析Java代码。

Github上提供了所有源代码: analyzer-java-code-examples

通用代码

使用JavaParser theere时,我们总是希望进行很多操作。 通常,我们希望对整个项目进行操作,因此在给定目录的情况下,我们将探索所有Java文件。 此类应有助于完成此任务:

package me.tomassetti.support;

import java.io.File;

public class DirExplorer {

public interface FileHandler {

void handle(int level, String path, File file);

}

public interface Filter {

boolean interested(int level, String path, File file);

}

private FileHandler fileHandler;

private Filter filter;

public DirExplorer(Filter filter, FileHandler fileHandler) {

this.filter = filter;

this.fileHandler = fileHandler;

}

public void explore(File root) {

explore(0, "", root);

}

private void explore(int level, String path, File file) {

if (file.isDirectory()) {

for (File child : file.listFiles()) {

explore(level + 1, path + "/" + child.getName(), child);

}

} else {

if (filter.interested(level, path, file)) {

fileHandler.handle(level, path, file);

}

}

}

}

对于每个Java文件,我们首先要为每个Java文件构建一个抽象语法树(AST),然后对其进行导航。 这样做有两种主要策略:

使用访客:要在特定类型的AST节点上进行操作时,这是正确的策略 使用递归迭代器:这允许处理所有类型的节点

可以编写访问者扩展JavaParser中包含的类,而这是一个简单的节点迭代器:

package me.tomassetti.support;

import com.github.javaparser.ast.Node;

public class NodeIterator {

public interface NodeHandler {

boolean handle(Node node);

}

private NodeHandler nodeHandler;

public NodeIterator(NodeHandler nodeHandler) {

this.nodeHandler = nodeHandler;

}

public void explore(Node node) {

if (nodeHandler.handle(node)) {

for (Node child : node.getChildrenNodes()) {

explore(child);

}

}

}

}

现在,让我们看看如何使用此代码解决Stack Overflow上的一些问题。

如何从Java类中提取普通字符串中所有类的名称?

在堆栈溢出时询问

寻找ClassOrInterfaceDeclaration节点可以解决此解决方案。 给定我们想要一种特定类型的节点,我们可以使用访客。 请注意,VoidVisitorAdapter允许传递任意参数。 在这种情况下,我们不需要这样做,因此我们指定对象类型,而在访问方法中将其忽略即可。

package me.tomassetti.examples;

import com.github.javaparser.JavaParser;

import com.github.javaparser.ParseException;

import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;

import com.github.javaparser.ast.visitor.VoidVisitorAdapter;

import com.google.common.base.Strings;

import me.tomassetti.support.DirExplorer;

import java.io.File;

import java.io.IOException;

public class ListClassesExample {

public static void listClasses(File projectDir) {

new DirExplorer((level, path, file) -> path.endsWith(".java"), (level, path, file) -> {

System.out.println(path);

System.out.println(Strings.repeat("=", path.length()));

try {

new VoidVisitorAdapter() {

@Override

public void visit(ClassOrInterfaceDeclaration n, Object arg) {

super.visit(n, arg);

System.out.println(" * " + n.getName());

}

}.visit(JavaParser.parse(file), null);

System.out.println(); // empty line

} catch (ParseException | IOException e) {

new RuntimeException(e);

}

}).explore(projectDir);

}

public static void main(String[] args) {

File projectDir = new File("source_to_parse/junit-master");

listClasses(projectDir);

}

}

我们在JUnit的源代码上运行示例,并获得以下输出:

/src/test/java/org/junit/internal/MethodSorterTest.java

=======================================================

* DummySortWithoutAnnotation

* Super

* Sub

* DummySortWithDefault

* DummySortJvm

* DummySortWithNameAsc

* MethodSorterTest

/src/test/java/org/junit/internal/matchers/StacktracePrintingMatcherTest.java

=============================================================================

* StacktracePrintingMatcherTest

/src/test/java/org/junit/internal/matchers/ThrowableCauseMatcherTest.java

=========================================================================

* ThrowableCauseMatcherTest

...

... many other lines follow

是否有Java代码解析器可以返回组成语句的行号?

在堆栈溢出时询问

在这种情况下,我需要查找各种语句。 现在,有几个类扩展了Statement基类,因此我可以使用一个访问者,但是我需要在几种访问方法中编写相同的代码,每个方法用于Statement的每个子类。 另外,我只想获取顶层语句,而不要获取其中的语句。 例如,一个for语句可以包含其他几个语句。 使用我们的自定义NodeIterator,我们可以轻松实现此逻辑。

package me.tomassetti.examples;

import com.github.javaparser.JavaParser;

import com.github.javaparser.ParseException;

import com.github.javaparser.ast.Node;

import com.github.javaparser.ast.stmt.Statement;

import com.google.common.base.Strings;

import me.tomassetti.support.DirExplorer;

import me.tomassetti.support.NodeIterator;

import java.io.File;

import java.io.IOException;

public class StatementsLinesExample {

public static void statementsByLine(File projectDir) {

new DirExplorer((level, path, file) -> path.endsWith(".java"), (level, path, file) -> {

System.out.println(path);

System.out.println(Strings.repeat("=", path.length()));

try {

new NodeIterator(new NodeIterator.NodeHandler() {

@Override

public boolean handle(Node node) {

if (node instanceof Statement) {

System.out.println(" [Lines " + node.getBeginLine() + " - " + node.getEndLine() + " ] " + node);

return false;

} else {

return true;

}

}

}).explore(JavaParser.parse(file));

System.out.println(); // empty line

} catch (ParseException | IOException e) {

new RuntimeException(e);

}

}).explore(projectDir);

}

public static void main(String[] args) {

File projectDir = new File("source_to_parse/junit-master");

statementsByLine(projectDir);

}

}

这是在JUnit的源代码上运行程序所获得的输出的一部分。

/src/test/java/org/junit/internal/matchers/ThrowableCauseMatcherTest.java

=========================================================================

[Lines 12 - 17 ] {

NullPointerException expectedCause = new NullPointerException("expected");

Exception actual = new Exception(expectedCause);

assertThat(actual, hasCause(is(expectedCause)));

}

您可能会注意到报告的语句跨5个,而不是报告的6个(12..17是6行)。 这是因为我们正在打印该语句的纯净版本,删除了白线,注释并设置了代码格式。

从Java代码中提取方法调用

在堆栈溢出时询问

对于提取方法调用,我们可以再次使用Visitor,因此这非常简单,并且与我们看到的第一个示例非常相似。

package me.tomassetti.examples;

import com.github.javaparser.JavaParser;

import com.github.javaparser.ParseException;

import com.github.javaparser.ast.expr.MethodCallExpr;

import com.github.javaparser.ast.visitor.VoidVisitorAdapter;

import com.google.common.base.Strings;

import me.tomassetti.support.DirExplorer;

import java.io.File;

import java.io.IOException;

public class MethodCallsExample {

public static void listMethodCalls(File projectDir) {

new DirExplorer((level, path, file) -> path.endsWith(".java"), (level, path, file) -> {

System.out.println(path);

System.out.println(Strings.repeat("=", path.length()));

try {

new VoidVisitorAdapter() {

@Override

public void visit(MethodCallExpr n, Object arg) {

super.visit(n, arg);

System.out.println(" [L " + n.getBeginLine() + "] " + n);

}

}.visit(JavaParser.parse(file), null);

System.out.println(); // empty line

} catch (ParseException | IOException e) {

new RuntimeException(e);

}

}).explore(projectDir);

}

public static void main(String[] args) {

File projectDir = new File("source_to_parse/junit-master");

listMethodCalls(projectDir);

}

}

如您所见,该解决方案与列出类的解决方案非常相似。

/src/test/java/org/junit/internal/MethodSorterTest.java

=======================================================

[L 58] MethodSorter.getDeclaredMethods(clazz)

[L 64] m.isSynthetic()

[L 65] m.toString()

[L 65] clazz.getName()

[L 65] m.toString().replace(clazz.getName() + '.', "")

[L 65] names.add(m.toString().replace(clazz.getName() + '.', ""))

[L 74] Arrays.asList(EPSILON, BETA, ALPHA, DELTA, GAMMA_VOID, GAMMA_BOOLEAN)

[L 75] getDeclaredMethodNames(DummySortWithoutAnnotation.class)

[L 76] assertEquals(expected, actual)

[L 81] Arrays.asList(SUPER_METHOD)

[L 82] getDeclaredMethodNames(Super.class)

[L 83] assertEquals(expected, actual)

[L 88] Arrays.asList(SUB_METHOD)

[L 89] getDeclaredMethodNames(Sub.class)

[L 90] assertEquals(expected, actual)

[L 118] Arrays.asList(EPSILON, BETA, ALPHA, DELTA, GAMMA_VOID, GAMMA_BOOLEAN)

[L 119] getDeclaredMethodNames(DummySortWithDefault.class)

[L 120] assertEquals(expected, actual)

[L 148] DummySortJvm.class.getDeclaredMethods()

[L 149] MethodSorter.getDeclaredMethods(DummySortJvm.class)

[L 150] assertArrayEquals(fromJvmWithSynthetics, sorted)

[L 178] Arrays.asList(ALPHA, BETA, DELTA, EPSILON, GAMMA_VOID, GAMMA_BOOLEAN)

[L 179] getDeclaredMethodNames(DummySortWithNameAsc.class)

[L 180] assertEquals(expected, actual)

下一步

您可以使用此处介绍的方法回答很多问题:浏览AST,找到您感兴趣的节点,并获取所需的任何信息。 但是,我们还需要考虑其他几件事:首先,如何转换代码。 尽管提取信息非常有用,但是重构更加有用。 然后,对于更高级的问题,我们需要使用java-symbol-solver解析符号。 例如:

查看AST,我们可以找到一个类的名称,但不能找到它间接实现的接口列表 在查看方法调用时,我们无法轻易找到该方法的声明。 它在哪个类或接口中声明? 我们要调用哪些不同的重载变体?

我们将在将来对此进行研究。 希望这些例子可以帮助您入门!

翻译自: https://www.javacodegeeks.com/2016/02/getting-started-javaparser-analyzing-java-code-programmatically.html

Copyright © 2022 历届世界杯_世界杯篮球 - cnfznx.com All Rights Reserved.