Анализируем Java-проект при помощи Python

User Rating: 5 / 5

Доброго времени суток, друзья. В сегодняшней статье мы попробуем решить следующую задачу при помощи языка Python: мы напишем с вами небольшой собственный модуль (я назвал его projectinfo.py), который будет решать задачу анализа структуры различных проектов, а в качестве экспериментального проекта мы выберем простой типовой проект на языке Java и посчитаем какие Java-классы объявлены в файлах проекта, имеющих расширение .java. В основу нашего модуля войдут небольшие классы, которые решают общую задачу сбора метаданных о файлах проекта, поэтому при необходимости вы без особого труда сможете адаптировать модуль для решения задач анализа других проектов - например, на языках C#, C++ и любых других (да и в целом, совсем не обязательно, чтобы проект представлял собой обязательно программу на каком-то языке программирования. Возможно, вы сможете найти применение наработкам из текущей статьи для анализа ваших проектов, имеющих иную природу и назначение).

Итак, начнём с полного текста нашего модуля projectinfo.py:

#!/usr/bin/env python
"""
[EN] Module allows you to aggregate information about some abstract project represented
in the form of folders structure containing some project files. Currently module
supports special classes for analyzing Java-based projects that contain files with the
*.java extension

[RU] Модуль позволяет вам собирать информацию о некотором абстрактном проекте, представленном
в форме структуры каталогов, содержащих какие-то проектные файлы. На текущий момент
модуль поддерживает специальные классы для анализа Java-проектов, содержащих файлы с расширением
*.java

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
"""
__author__ = "allineed.ru"
__contact__ = "allineed.ru[at]gmail.com"
__copyright__ = "Copyright 2022, Allineed.Ru"
__date__ = "2022/06/13"
__deprecated__ = False
__email__ = "allineed.ru[at]gmail.com"
__license__ = "GPLv3"
__maintainer__ = "Max Damascus"
__status__ = "Alpha"
__version__ = "0.0.1"

import os
import re
from typing import List


class ResourceFileExtension:
    """
    [EN] Class containing possible known extensions of the analyzed project

    [RU] Класс, содержащий возможные известные расширения анализируемого проекта
    """
    JAVA_FILE = '.java'
    XML_FILE = '.xml'
    GITIGNORE_FILE = '.gitignore'
    IDEA_PROJECT_FILE = '.iml'


class ResourceFile:
    """
    [EN] Class contains meta information about abstract resource file of the project

    [RU] Класс содержит метаинформацию об абстрактном ресурсном файле проекта
    """

    def __init__(self):
        self.__init__('', '', '')

    def __init__(self, name, path, extension):
        self.__name = name
        self.__path = path
        self.__extension = extension

    def get_name(self):
        """
        [EN] Method gets the file name that corresponds to this resource

        [RU] Метод получает имя файла, соответствующее данному ресурсу
        """
        return self.__name

    def get_path(self):
        """
        [EN] Method gets the full path to the resource

        [RU] Метод получает путь, по которому хранится данный ресурс
        """
        return self.__path

    def get_extension(self):
        """
        [EN] Method gets the file extension corresponding to this resource

        [RU] Метод получает расширение файла для данного ресурса
        """
        return self.__extension


class ProjectResourceAnalyzer:
    """
    [EN] Class helps to analyze the project and collect information about resource files included into the project

    [RU] Этот класс является анализатором проекта и собирает информацию о входящих в него файлах ресурсов
    """

    def __init__(self):
        self.__resource_files: List[ResourceFile] = []

    def __add_resource_file(self, source_file: ResourceFile):
        """
        [EN] Private method that adds a resource file into the list of project resources

        [RU] Приватный метод, добавляет файл ресурса в список файлов ресурса проекта
        """
        self.__resource_files.append(source_file)

    def get_resource_files(self, extension='') -> List[ResourceFile]:
        """
        [EN] Method gets the list of ResourceFile elements holding all the resources included in the project

        [RU] Метод получает список элементов ResourceFile, содержащий все файлы ресурсов, входящих в указанный проект
        """
        if extension == '':
            return self.__resource_files

        result: List[ResourceFile] = []
        for rf in self.__resource_files:
            if rf.get_extension() == extension:
                result.append(rf)
        return result

    def analyze(self, path):
        """
        [EN] Method analyzes the project by the given path and aggregates meta information about its resource files

        [RU] Метод анализирует проект по заданному пути и агрегирует метаинформацию о входящих в проект файлах ресуров
        """
        for current_path, sub_folders, files in os.walk(path):
            for file in files:
                if isinstance(file, str):
                    str_file = str(file)
                    file_name, file_ext = os.path.splitext(str_file)
                    resource_file = ResourceFile(str_file, current_path, file_ext)
                    self.__add_resource_file(resource_file)


class JavaResourceFileMetaInfo:
    """
    [EN] Class contains meta information about the resource file that has a .java extension.
    This meta information includes:
        * imports - all the imports that are used in this Java file
        * classes - all the classes declared in this Java file
        * package - the package name specified in this Java file (if any)
    [RU] Класс, содержащий метаинформацию о файле ресурса проекта с расширением .java.
    Эта метаинформация включает в себя:
        * imports - все импорты, использованные в данном Java файле
        * classes - все классы, объявленные в этом Java файле
        * package - название пакета, указанное в данном Java файле (если оно присутствует)
    """

    def __init__(self):
        self.__imports: List[str] = []
        self.__classes: List[str] = []
        self.__package: str = ''

    def classes(self):
        return self.__classes

    def imports(self):
        return self.__imports

    def set_package(self, package: str):
        self.__package = package

    def add_class(self, class_name: str):
        self.__classes.append(class_name)

    def add_import(self, imported_pkg_and_class: str):
        self.__imports.append(imported_pkg_and_class)

    def get_package(self):
        return self.__package


class JavaMetaInfoAnalyzer:
    """
    [EN] Class collects meta information about all resource files with the .java extension

    [RU] Класс, собирающий метаинформацию о всех файлах ресурсов проекта с расширением .java
    """

    def __init__(self, project_resource_analyzer: ProjectResourceAnalyzer):
        self.__project_resource_analyzer = project_resource_analyzer
        self.__java_resources: List[ResourceFile] = self.__project_resource_analyzer \
            .get_resource_files(ResourceFileExtension.JAVA_FILE)
        self.__java_classes_dict: dict[ResourceFile, JavaResourceFileMetaInfo] = {}
        self.__is_multiline_comment_opened = False

    def get_java_resources(self):
        """
        [EN] Method gets the list of ResourceFile elements (files with .java extension)

        [RU] Метод получает список элементов ResourceFile (файлов с расширением .java)

        :return: [EN] the list of ResourceFile elements; [RU] список элементов ResourceFile
        """
        return self.__java_resources

    def get_java_classes_count(self):
        """
        [EN] Method gets the number of Java classes found

        [RU] Метод возвращает количество найденных Java классов

        :return: [EN] the number of Java classes found; [RU] количество найденных Java классов
        """
        return len(self.__java_classes_dict)

    def get_java_classes(self):
        """
        [EN] Method gets the dictionary that holds pairs <ResourceFile, JavaResourceFileMetaInfo>

        [RU] Метод возвращает словарь, содержащий пары <ResourceFile, JavaResourceFileMetaInfo>

        :return: [EN] dictionary containing metainfo about Java classes; [RU] словарь, содержащий метаданные о
        Java-классах
        """
        return self.__java_classes_dict

    def __strip_comments(self, file_line: str):
        """
        [EN] Private method that strips the Java-style comments from the current line that has been read from
        the file

        [RU] Приватный метод, который отрезает всё, что относится к комментариям в текущей строке, считанной из
        файла

        :param file_line: [EN] current line from the Java file being read; [RU] текущая строка из Java файл, который
        считывается в настоящий момент
        :return: [EN] part of the line that has no Java-comments parts; [RU] часть строки, не имеющая Java-комментариев
        """
        single_line_comment_start_pos = file_line.find("//")
        multi_line_comment_start_pos = file_line.find("/*")
        multi_line_comment_end_pos = file_line.find("*/")

        if multi_line_comment_start_pos == 0:
            self.__is_multiline_comment_opened = multi_line_comment_end_pos < 0
            return ''
        elif multi_line_comment_start_pos > 0:
            self.__is_multiline_comment_opened = multi_line_comment_end_pos < 0
            return file_line[0:multi_line_comment_start_pos]

        if multi_line_comment_end_pos >= 0:
            self.__is_multiline_comment_opened = False
            return file_line[multi_line_comment_end_pos + 2:]

        if single_line_comment_start_pos == 0:
            return ''
        elif single_line_comment_start_pos > 0:
            return file_line[0:single_line_comment_start_pos]

        return file_line

    def analyze_java_classes(self):
        """
        [EN] Method starts Java classes analysis on the basis of project resources related to .java files

        [RU] Метод начинает анализ Java-классов на основании проектных ресурсов, относящихся к файлам с расширением
        .java

        :return: None
        """
        self.__is_multiline_comment_opened = False
        java_class_pattern = re.compile('class\s+(?P<class_name>\w+)\s+')
        java_import_pattern = re.compile('import\s+(?P<imported_package>[\w.]+);')
        java_package_pattern = re.compile('package\s+(?P<package>[\w.]+);')

        for java_resource in self.__java_resources:
            full_path = java_resource.get_path() + os.sep + java_resource.get_name()
            java_file = open(full_path, 'r')
            java_file_lines = java_file.readlines()

            java_resource_meta_info = JavaResourceFileMetaInfo()

            for java_file_line in java_file_lines:
                java_file_line = self.__strip_comments(java_file_line)
                if len(java_file_line) == 0 or self.__is_multiline_comment_opened:
                    continue

                pkg_results = java_package_pattern.findall(java_file_line)
                if len(pkg_results) > 0:
                    java_resource_meta_info.set_package(pkg_results[0])

                cls_results = java_class_pattern.findall(java_file_line)
                if len(cls_results) > 0:
                    java_resource_meta_info.add_class(cls_results[0])

                imp_results = java_import_pattern.findall(java_file_line)
                if len(imp_results) > 0:
                    java_resource_meta_info.add_import(imp_results[0])

            self.__java_classes_dict[java_resource] = java_resource_meta_info

            java_file.close()

Сохранив представленный листинг в файле с именем projectinfo.py мы получаем отдельный модуль, предоставляющий возможности для анализа структуры проектов (пока что какую-то пользу он принесёт в части анализа Java-проектов). Чтобы воспользоваться функциональностью модуля, просто подключаем его к нашему скрипту (например, у вас это может быть основной скрипт/модуль main.py) посредством from projectinfo import *:

from projectinfo import *

if __name__ == '__main__':
    pra = ProjectResourceAnalyzer()
    pra.analyze('C:\\JavaProjects\\MyAmazingProject')

    # Вывести на экран информацию обо всех файлах, входящих в проект:
    print('ВСЕ ФАЙЛЫ ПРОЕКТА:')
    for rs in pra.get_resource_files():
        print(f'Имя файла: {rs.get_name()}, Путь к файлу: {rs.get_path()}, Расширение файла: {rs.get_extension()}')

    print('ТОЛЬКО JAVA-ФАЙЛЫ ПРОЕКТА:')
    for rs in pra.get_resource_files('.java'):
        print(f'Имя файла: {rs.get_name()}, Путь к файлу: {rs.get_path()}, Расширение файла: {rs.get_extension()}')

    print('ТОЛЬКО XML-ФАЙЛЫ ПРОЕКТА:')
    for rs in pra.get_resource_files('.xml'):
        print(f'Имя файла: {rs.get_name()}, Путь к файлу: {rs.get_path()}, Расширение файла: {rs.get_extension()}')

    jmia = JavaMetaInfoAnalyzer(pra)
    for jr in jmia.get_java_resources():
        print(f'Файл: {jr.get_name()}, Путь к файлу: {jr.get_path()}')

    jmia.analyze_java_classes()
    print(f'Количество найденных Java-классов: {jmia.get_java_classes_count()}')

    print(f'Информация о классах Java:')
    for java_resource, java_resource_file_meta_info in jmia.get_java_classes().items():
        print(f'============================')
        print(f'Имя файла: {java_resource.get_name()}, Путь к файлу: {java_resource.get_path()}')
        print(f'============================')
        print(f'---> Пакет: {java_resource_file_meta_info.get_package()}')
        print(f'---> Классы: {java_resource_file_meta_info.classes()}')
        print(f'---> Импорты: {java_resource_file_meta_info.imports()}')

В моём случае по пути C:\JavaProjects\MyAmazingProject я создал простенький Java-проект, добавил в него пару-тройку классов и проверил работу нашего Python-модуля на нём. Ниже предоставляю листинг того, что вывел в моём случае основной скрипт main.py:

ВСЕ ФАЙЛЫ ПРОЕКТА:
Имя файла: MyAmazingProject.iml, Путь к файлу: C:\JavaProjects\MyAmazingProject, Расширение файла: .iml
Имя файла: pom.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject, Расширение файла: .xml
Имя файла: .gitignore, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла:
Имя файла: compiler.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: encodings.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: jarRepositories.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: misc.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: workspace.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: App.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example, Расширение файла: .java
Имя файла: SomeClass.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example, Расширение файла: .java
Имя файла: AppTest.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\test\java\org\example, Расширение файла: .java
ТОЛЬКО JAVA-ФАЙЛЫ ПРОЕКТА:
Имя файла: App.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example, Расширение файла: .java
Имя файла: SomeClass.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example, Расширение файла: .java
Имя файла: AppTest.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\test\java\org\example, Расширение файла: .java
ТОЛЬКО XML-ФАЙЛЫ ПРОЕКТА:
Имя файла: pom.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject, Расширение файла: .xml
Имя файла: compiler.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: encodings.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: jarRepositories.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: misc.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Имя файла: workspace.xml, Путь к файлу: C:\JavaProjects\MyAmazingProject\.idea, Расширение файла: .xml
Файл: App.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example
Файл: SomeClass.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example
Файл: AppTest.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\test\java\org\example
Количество найденных Java-классов: 3
Информация о классах Java:
============================
Имя файла: App.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example
============================
---> Пакет: org.example
---> Классы: ['App']
---> Импорты: ['java.lang.BigDecimal']
============================
Имя файла: SomeClass.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\main\java\org\example
============================
---> Пакет: org.example
---> Классы: ['SomeClass', 'SomeInnerClass', 'SomeOtherClassIncluded2']
---> Импорты: ['java.lang.String', 'java.lang.BigInteger', 'java.lang.BigDecimal']
============================
Имя файла: AppTest.java, Путь к файлу: C:\JavaProjects\MyAmazingProject\src\test\java\org\example
============================
---> Пакет: org.example
---> Классы: ['AppTest']
---> Импорты: ['org.junit.Test']

Process finished with exit code 0

Думаю, что по выводу в консоли, а также приведённым листингам должно быть понятно, как работает наш новый модуль: он обходит рекурсивно заданный ему каталог файловой системы, где располагается наш проект, а также агрегирует информацию о входящих в проект ресурсах (т.е. файлах с различными расширениями). В модуле есть два удобных класса для анализа информации о Java-файлах:

  • JavaResourceFileMetaInfo
  • JavaMetaInfoAnalyzer

Все классы и методы я постарался снабдить документационными комментариями, так что вы без труда разберётесь в среде разработки PyCharm с их назначением при помощи всплывающих подсказок над  классами/методами.

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

Задавайте вопросы в комментариях, если что-то непонятно, а также делитесь мнением о том, насколько может пригодиться подобный модуль в решении ваших задач. Буду благодарен за обратную связь.

Ну а пока на этом всё, пробуйте модуль на своих проектах, делитесь мыслями, чего в нём не хватает и хотелось бы увидеть. Успехов!

Яндекс.Метрика