Java 代码变更影响分析 - JCCI 介绍





5.00/5 (1投票)
Java 代码变更影响分析
背景
随着业务的日益复杂,进行全面的回归测试变得越来越困难。为了更准确地定位后端项目变更的影响范围,并更精确地定义回归测试的范围,从而提高测试效率,有必要分析 Java 代码提交的影响范围。
实现
基本原理与 Idea 中的 Find Usage 功能相同,定位代码变更的影响,并持续遍历受影响的类和方法,直到找到顶层控制器层。
代码主要使用 Python 编写,涉及两个库
javalang
:用于解析 Java 文件语法的库
unidiff
:用于解析 git diff 信息的库
使用 javalang
进行语法解析,从每个 Java 文件中提取导入、类、继承、实现、声明器、方法等信息。Java 文件解析的结果使用 sqlite3 存储,分为 project、class、import、field、methods 等多个表,分别存储相应的信息。然后,使用 SQL 查询来调用方法。
使用 unidiff
解析 git diff 信息(diff 文件、added_line_num
、removed_line_num
)。然后,基于文件中添加或删除的代码行,确定受影响的类和方法,持续遍历受影响的类和方法,直到找到顶层控制器层。
SQLite3 表结构
表结构如下
CREATE TABLE project (
project_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
project_name TEXT NOT NULL,
git_url TEXT NOT NULL,
branch TEXT NOT NULL,
commit_or_branch_new TEXT NOT NULL,
commit_or_branch_old TEXT,
create_at TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE TABLE class (
class_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
filepath TEXT,
access_modifier TEXT,
class_type TEXT NOT NULL,
class_name TEXT NOT NULL,
package_name TEXT NOT NULL,
extends_class TEXT,
project_id INTEGER NOT NULL,
implements TEXT,
annotations TEXT,
documentation TEXT,
is_controller REAL,
controller_base_url TEXT,
commit_or_branch TEXT,
create_at TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE TABLE import (
import_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
class_id INTEGER NOT NULL,
project_id INTEGER NOT NULL,
start_line INTEGER,
end_line INTEGER,
import_path TEXT,
is_static REAL,
is_wildcard REAL,
create_at TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE TABLE field (
field_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
class_id INTEGER,
project_id INTEGER NOT NULL,
annotations TEXT,
access_modifier TEXT,
field_type TEXT,
field_name TEXT,
is_static REAL,
start_line INTEGER,
end_line INTEGER,
documentation TEXT,
create_at TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE TABLE methods (
method_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
class_id INTEGER NOT NULL,
project_id INTEGER NOT NULL,
annotations TEXT,
access_modifier TEXT,
return_type TEXT,
method_name TEXT NOT NULL,
parameters TEXT,
body TEXT,
method_invocation_map TEXT,
is_static REAL,
is_abstract REAL,
is_api REAL,
api_path TEXT,
start_line INTEGER NOT NULL,
end_line INTEGER NOT NULL,
documentation TEXT,
create_at TIMESTAMP NOT NULL DEFAULT (datetime('now','localtime'))
);
本节主要介绍 methods 表中的 method_invocation_map
字段,它存储了解析的方法所使用的类和方法,方便后续查询哪些方法使用了某个类或方法。method_invocation_map
字段存储示例
{
"com.XXX.Account": {
"entity": {
"return_type": true
}
},
"com.XXXX.AccountService": {
"methods": {
"functionAA(Map<String#Object>)": [303]
},
"fields": {
"fieldBB": [132]
}
}
}
分析调用
这部分主要逻辑更改为分析哪些方法调用了被修改的类或方法,通过 SQL 查询结果。示例代码
SELECT
*
FROM
methods
WHERE
project_id = 1
AND (json_extract(method_invocation_map,
'$."com.xxxx.ProductUtil".methods."convertProductMap(QueryForCartResponseDTO)"') IS NOT NULL
OR json_extract(method_invocation_map,
'$."com.xxxx.ProductUtil".methods."convertProductMap(null)"') IS NOT NULL)
显示方法
之前,显示方法使用了树形图和关系图。但是树形图没有清晰地显示链接,而关系图中节点的坐标不合理。这部分也进行了优化,节点坐标基于节点关系计算。关系链接越长,水平坐标越大,使显示更清晰。示例代码
def max_relationship_length(relationships):
if not relationships:
return {}
# Build adjacency list
graph = {}
for relationship in relationships:
source = relationship['source']
target = relationship['target']
if source not in graph:
graph[source] = []
if target not in graph:
graph[target] = []
graph[source].append(target)
# BFS Traverse and calculate the longest path length from each node to the starting point
longest_paths = {node: 0 for node in graph.keys()}
graph_keys = [node for node in graph.keys()]
longest_paths[graph_keys[0]] = 0
queue = deque([(graph_keys[0], 0)])
while queue:
node, path_length = queue.popleft()
if not graph.get(node) and not queue and graph_keys.index(node) + 1 < len(graph_keys):
next_node = graph_keys[graph_keys.index(node) + 1]
next_node_path_length = longest_paths[next_node]
queue.append((next_node, next_node_path_length))
continue
for neighbor in graph.get(node, []):
if path_length + 1 > longest_paths[neighbor]:
longest_paths[neighbor] = path_length + 1
queue.append((neighbor, path_length + 1))
return longest_paths
显示效果
三种分析方法
JCCI 可以分析三种不同的场景:比较同一分支上的两个提交、分析指定的类以及分析两个分支之间的特性。示例
from path.to.jcci.src.jcci.analyze import JCCI
# Comparison of different commits on the same branch
commit_analyze = JCCI('git@xxxx.git', 'username1')
commit_analyze.analyze_two_commit('master','commit_id1','commit_id2')
# Analyze the method impact of a class. The last parameter of the analyze_class_method method is the number of lines where the method is located. The number of lines of different methods is separated by commas. If left blank, the impact of the complete class will be analyzed.
class_analyze = JCCI('git@xxxx.git', 'username1')
class_analyze.analyze_class_method('master','commit_id1', 'package\src\main\java\ClassA.java', '20,81')
# Compare different branches
branch_analyze = JCCI('git@xxxx.git', 'username1')
branch_analyze.analyze_two_branch('branch_new','branch_old')
灵活配置
可以在配置文件中配置 sqlite3 数据库存储路径、项目代码存储路径以及解析时要忽略的文件。示例
db_path = os.path.dirname(os.path.abspath(__file__))
project_path = os.path.dirname(os.path.abspath(__file__))
ignore_file = ['*/pom.xml', '*/test/*', '*.sh', '*.md', '*/checkstyle.xml', '*.yml', '.git/*']
结论
项目 URL: JCCI 欢迎大家试用并提供反馈。期待您的 star~ 联系方式和更多信息可以在 GitHub 项目的 readme 中找到。谢谢~~