开源地址
技术实现
适用于Java代码改动影响范围分析
代码主要由 python 编写,主要涉及 2 个库:
- javalang java 文件语法解析库
- unidiff git diff 信息解析库
通过 javalang 语法解析获取每个 Java 文件的 import class extends implements declarators methods 等信息
通过 unidiff 解析 git diff 信息(diff file, added_line_num, removed_lin_num)
然后根据文件增删的代码行去判断影响了哪些类和方法,不断遍历受影响的类和方法直至找到最上层的 controller 层
通过传入项目 git 地址 分支 两次的 commit id,即可分析出两次 commit id 之间代码改动所带来的影响,并生成树图数据方便展示影响链路。
具体过程
clone项目
参数传入项目地址后,通过python os.system执行git clone命令
执行Diff
参数传入项目分支,两次的git commit id
通过python os.system执行git diff命令
git diff commit_id2..commit_id1 > diff_commit_id2..commit_id1.txt
解析项目文件
通过javalang库分别解析项目目录中所有的类文件,获取类的所有信息
类
包含文件路径、包名、imports、类名、继承类、实现类、变量、方法和是否是controller
变量
记录变量类型、变量名称、所在行
方法
获取方法名、参数、起始行、结束行、方法体、包含类、包含变量、是否是接口及接口路径等
解析Diff信息
通过unidiff库解析diff_commit_id2..commit_id1.txt结果,获取改动文件和改动行号
from unidiff import PatchSet
with open(diff_file, encoding='UTF-8') as f:
diff_text = f.read()
patch_set = PatchSet(diff_text)
解析patch
遍历每个patch,获取改动影响范围
大致过程是获取改动类的目录路径和增删行号,然后获取解析结果对应的类,判断增删行号落在哪个变量或方法中间,从而定位受影响的类或方法
def _analyze_diff_patch(patch, head_java_file_analyze_result, base_java_file_analyze_result):
if '.git' in patch.path or 'src' + sep + 'test' + sep in patch.path or (not patch.path.endswith('.java') and not patch.path.endswith('.xml')):
return None
line_num_added, line_content_added, line_num_removed, line_content_removed = _diff_patch_lines(patch)
java_file_path = patch.path
methods_impacted = {}
declarators_impacted = {}
_diff_patch_impact(methods_impacted, declarators_impacted, head_java_file_analyze_result, java_file_path,
line_num_added, 'ADD')
_diff_patch_impact(methods_impacted, declarators_impacted, base_java_file_analyze_result, java_file_path,
line_num_removed, 'DEL')
diff_result = JavaDiffResult(patch.path, line_num_added, line_content_added, line_num_removed, line_content_removed)
diff_result.changed_methods = methods_impacted
diff_result.changed_declarators = declarators_impacted
return diff_result
接着继续解析改动范围,评估影响范围,不断遍历至结束,就获得了最终的影响范围结果
生成影响范围结果
影响范围结果以树形方式展现,生成树形展现的json
def _gen_treemap_data(diff_results, commit_first, commit_second):
flare = {'name': commit_second + '..' + commit_first, 'children': []}
api_list = []
diff_results.reverse()
diff_filepath = []
for diff_result in diff_results:
if diff_result.filepath in diff_filepath:
continue
diff_filepath.append(diff_result.filepath)
class_name = diff_result.filepath.split('/')[-1].split('.')[0]
if diff_result.filepath.endswith('.xml'):
class_name = diff_result.filepath.split('/')[-1]
flare_children = {'name': class_name, 'children': [], 'collapsed': True}
flare_children_list = []
changed_methods = diff_result.changed_methods
for key in changed_methods.keys():
if changed_methods[key].get('is_api'):
flare_children_children = {'name': 'methods.' + key + '(' + str(changed_methods[key]['api_path']) + ')',
'children': [], 'collapsed': True}
api_list_item = {'name': str(changed_methods[key]['api_path'])}
if api_list_item not in api_list:
api_list.append(api_list_item)
else:
flare_children_children = {'name': 'methods.' + key, 'children': [], 'collapsed': True}
if changed_methods[key].get('impact') is not None:
for method_impact in changed_methods[key]['impact']:
flare_children_sub = {'name': 'impacted.' + method_impact, 'children': [],
'collapsed': True}
if flare_children_sub not in flare_children_children['children']:
flare_children_children['children'].append(flare_children_sub)
flare_children_list.append(flare_children_children)
changed_declarators = diff_result.changed_declarators
for key in changed_declarators.keys():
flare_children_children = {'name': 'declarators.' + key, 'children': [], 'collapsed': True}
if changed_declarators[key].get('impact') is not None:
for declarator_impact in changed_declarators[key]['impact']:
flare_children_sub = {'name': 'impacted.' + declarator_impact, 'children': [],
'collapsed': True}
if flare_children_sub not in flare_children_children['children']:
flare_children_children['children'].append(flare_children_sub)
flare_children_list.append(flare_children_children)
flare_children['children'] = flare_children_list
flare['children'].append(flare_children)
flare['children'].reverse()
flare['children'].insert(0, {'name': 'Impact_Apis', 'children': api_list})
return flare
展现结果
最终结果如下