Статьи

Начало работы с JavaParser: программный анализ кода Java

Одна из вещей, которые мне нравятся больше всего, — это анализ кода и автоматические операции над ним. По этой причине я начал вносить вклад в JavaParser и создал несколько связанных проектов: java-symbol-solver иffectivejava . java_jp-1024x648

Как участник JavaParser я снова и снова читал некоторые очень похожие вопросы о извлечении информации из исходного кода Java. По этой причине я подумал, что могу помочь привести несколько простых примеров, просто чтобы начать разбирать код Java.

Общий код

При использовании JavaParser существует множество операций, которые мы хотим выполнять каждый раз. Часто мы хотим работать над целым проектом, поэтому, имея каталог, мы изучим все файлы Java. Этот класс должен помочь сделать это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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-файла мы хотим сначала построить Абстрактное синтаксическое дерево (AST) для каждого Java-файла, а затем перемещаться по нему. Для этого есть две основные стратегии:

  1. использовать посетителя: это правильная стратегия, когда вы хотите работать с определенными типами узлов AST
  2. использовать рекурсивный итератор: это позволяет обрабатывать все виды узлов

Посетители могут быть написаны расширяющие классы, включенные в JavaParser, в то время как это простой итератор узла:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
            }
        }
    }
}

Теперь давайте посмотрим, как использовать этот код для решения некоторых вопросов, связанных с переполнением стека.

Как извлечь имя всех классов в обычной строке из класса Java?

Это решение может быть решено путем поиска узлов ClassOrInterfaceDeclaration . Учитывая, что нам нужен определенный тип узла, мы можем использовать Visitor. Обратите внимание, что VoidVisitorAdapter разрешает передавать произвольный аргумент. В этом случае нам это не нужно, поэтому мы указываем тип Object и просто игнорируем его в нашем методе посещения .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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<Object>() {
                    @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 и получили следующий вывод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/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 мы можем легко реализовать эту логику.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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.

1
2
3
4
5
6
7
/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, так что это довольно просто и довольно похоже на первый пример, который мы видели.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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<Object>() {
                    @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);
    }
}

Как вы можете видеть, решение очень похоже на решение для перечисления классов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/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, мы можем найти имя класса, но не список интерфейсов, которые он реализует косвенно
  • глядя на вызов метода, мы не можем легко найти объявление этого метода. В каком классе или интерфейсе это было объявлено? Какой из различных перегруженных вариантов мы вызываем?

Мы рассмотрим это в будущем. Надеемся, что эти примеры помогут вам начать!