项目结构变更
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
package com.codechecker.analyzer;
|
||||
|
||||
import com.codechecker.config.AppConfig;
|
||||
import com.codechecker.git.GitChangeScanner;
|
||||
import com.codechecker.model.ChangedClassFile;
|
||||
import com.codechecker.model.ClassChangeKind;
|
||||
import com.codechecker.model.ClassChangeReport;
|
||||
import com.codechecker.model.FieldChange;
|
||||
import com.codechecker.model.FieldInfo;
|
||||
import com.codechecker.parser.ClassDeclParser;
|
||||
import com.codechecker.parser.ClassFieldParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 编排 git 扫描、字段 diff、影响分析,生成待通知的 ClassChangeReport 列表。
|
||||
*/
|
||||
public class ClassChangeAnalyzer {
|
||||
private final GitChangeScanner gitScanner;
|
||||
private final ClassFieldParser classFieldParser = new ClassFieldParser();
|
||||
private final FieldDiffEngine fieldDiffEngine = new FieldDiffEngine();
|
||||
private final ImpactAnalyzer impactAnalyzer = new ImpactAnalyzer();
|
||||
private final ClassDeclParser classDeclParser = new ClassDeclParser();
|
||||
|
||||
public ClassChangeAnalyzer(GitChangeScanner gitScanner) {
|
||||
this.gitScanner = gitScanner;
|
||||
}
|
||||
|
||||
/** 扫描变更文件并逐条分析,无实质变更的 MODIFIED 会被跳过 */
|
||||
public List<ClassChangeReport> analyze(Path repoRoot, AppConfig config, String oldSha, String newSha,
|
||||
Map<String, com.codechecker.model.ApiEndpoint> endpointIndex) throws IOException {
|
||||
List<ChangedClassFile> changedFiles = gitScanner.scanChangedClasses(oldSha, newSha);
|
||||
List<ClassChangeReport> reports = new ArrayList<>();
|
||||
|
||||
for (ChangedClassFile changedFile : changedFiles) {
|
||||
if (changedFile.getStatus() == ChangedClassFile.ChangeStatus.DELETED) {
|
||||
reports.add(analyzeDeleted(changedFile, config, repoRoot, oldSha, endpointIndex));
|
||||
continue;
|
||||
}
|
||||
ClassChangeReport report = analyzeModifiedOrRenamed(changedFile, config, repoRoot, oldSha, newSha, endpointIndex);
|
||||
if (report != null) {
|
||||
reports.add(report);
|
||||
}
|
||||
}
|
||||
return reports;
|
||||
}
|
||||
|
||||
/** 处理删除:标记 DELETED 并分析影响(基于旧源码) */
|
||||
private ClassChangeReport analyzeDeleted(ChangedClassFile changedFile, AppConfig config, Path repoRoot,
|
||||
String oldSha, Map<String, com.codechecker.model.ApiEndpoint> endpointIndex)
|
||||
throws IOException {
|
||||
String path = changedFile.getRelativePath();
|
||||
String oldSource = gitScanner.readFileAtCommit(oldSha, path);
|
||||
|
||||
String classDescription = classDeclParser.extractClassDescription(
|
||||
oldSource, changedFile.getClassName());
|
||||
|
||||
ClassChangeReport report = new ClassChangeReport(
|
||||
changedFile.getClassName(),
|
||||
null,
|
||||
changedFile.getClassType(),
|
||||
ClassChangeKind.DELETED,
|
||||
path,
|
||||
config.isDtoEntityConversionEnabled(),
|
||||
classDescription
|
||||
);
|
||||
impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, oldSource, oldSource);
|
||||
return report;
|
||||
}
|
||||
|
||||
/** 处理修改/重命名:字段 diff → 判定 changeKind → 影响分析 */
|
||||
private ClassChangeReport analyzeModifiedOrRenamed(ChangedClassFile changedFile, AppConfig config,
|
||||
Path repoRoot, String oldSha, String newSha,
|
||||
Map<String, com.codechecker.model.ApiEndpoint> endpointIndex)
|
||||
throws IOException {
|
||||
String oldPath = changedFile.pathForOldCommit();
|
||||
String newPath = changedFile.getRelativePath();
|
||||
|
||||
String oldSource = gitScanner.readFileAtCommit(oldSha, oldPath);
|
||||
String newSource = gitScanner.readFileAtCommit(newSha, newPath);
|
||||
if (newSource == null || newSource.isBlank()) {
|
||||
newSource = gitScanner.readFileAtHead(newPath);
|
||||
}
|
||||
|
||||
String oldFallback = ClassDeclParser.classNameFromPath(oldPath);
|
||||
String newFallback = ClassDeclParser.classNameFromPath(newPath);
|
||||
String oldClassName = changedFile.getOldClassName() != null
|
||||
? changedFile.getOldClassName()
|
||||
: classDeclParser.resolveClassName(oldSource, oldFallback);
|
||||
String newClassName = classDeclParser.resolveClassName(newSource, newFallback);
|
||||
|
||||
List<FieldInfo> oldFields = classFieldParser.parseFields(oldSource, oldClassName);
|
||||
List<FieldInfo> newFields = classFieldParser.parseFields(newSource, newClassName);
|
||||
List<FieldChange> fieldChanges = fieldDiffEngine.diff(oldFields, newFields);
|
||||
|
||||
boolean renamed = !oldClassName.equals(newClassName);
|
||||
ClassChangeKind changeKind;
|
||||
if (renamed && fieldChanges.isEmpty()) {
|
||||
changeKind = ClassChangeKind.RENAME_ONLY;
|
||||
} else if (renamed) {
|
||||
changeKind = ClassChangeKind.RENAME_AND_FIELDS;
|
||||
} else if (!fieldChanges.isEmpty()) {
|
||||
changeKind = ClassChangeKind.FIELDS_ONLY;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
String classDescription = classDeclParser.extractClassDescription(newSource, newClassName);
|
||||
|
||||
ClassChangeReport report = new ClassChangeReport(
|
||||
newClassName,
|
||||
renamed ? oldClassName : null,
|
||||
changedFile.getClassType(),
|
||||
changeKind,
|
||||
newPath,
|
||||
config.isDtoEntityConversionEnabled(),
|
||||
classDescription
|
||||
);
|
||||
fieldChanges.forEach(report::addFieldChange);
|
||||
impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, newSource, oldSource);
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.codechecker.analyzer;
|
||||
|
||||
import com.codechecker.config.AppConfig;
|
||||
import com.codechecker.model.ApiEndpoint;
|
||||
import com.codechecker.parser.EndpointParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 预扫描 Controller/Feign 目录,构建 endpointKey → ApiEndpoint 索引。
|
||||
*/
|
||||
public class EndpointIndexBuilder {
|
||||
private final EndpointParser endpointParser = new EndpointParser();
|
||||
|
||||
/** 合并 Controller 与 Feign 扫描结果 */
|
||||
public Map<String, ApiEndpoint> buildIndex(Path repoRoot, AppConfig config) throws IOException {
|
||||
Map<String, ApiEndpoint> index = new LinkedHashMap<>();
|
||||
for (String dir : config.getControllerScanDirs()) {
|
||||
addEndpoints(index, endpointParser.scanControllerDirectory(repoRoot.resolve(dir), dir));
|
||||
}
|
||||
for (String dir : config.getFeignScanDirs()) {
|
||||
addEndpoints(index, endpointParser.scanFeignDirectory(repoRoot.resolve(dir), dir));
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/** 按 endpointKey 去重写入索引 */
|
||||
private void addEndpoints(Map<String, ApiEndpoint> index, List<ApiEndpoint> endpoints) {
|
||||
for (ApiEndpoint endpoint : endpoints) {
|
||||
index.putIfAbsent(endpoint.endpointKey(), endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.codechecker.analyzer;
|
||||
|
||||
import com.codechecker.model.FieldChange;
|
||||
import com.codechecker.model.FieldInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 对比新旧字段列表,产出新增/删除/类型修改/重命名(纯注释变更忽略)。
|
||||
*/
|
||||
public class FieldDiffEngine {
|
||||
|
||||
/**
|
||||
* 按字段名对比;删除+新增且说明匹配时合并为重命名。
|
||||
* 输出顺序:按新字段声明顺序,未配对的删除字段置于末尾。
|
||||
*/
|
||||
public List<FieldChange> diff(List<FieldInfo> oldFields, List<FieldInfo> newFields) {
|
||||
Map<String, FieldInfo> oldMap = toMap(oldFields);
|
||||
Map<String, FieldInfo> newMap = toMap(newFields);
|
||||
|
||||
List<FieldChange> modified = new ArrayList<>();
|
||||
List<FieldInfo> added = new ArrayList<>();
|
||||
List<FieldInfo> removed = new ArrayList<>();
|
||||
|
||||
for (FieldInfo newField : newFields) {
|
||||
FieldInfo oldField = oldMap.get(newField.getName());
|
||||
if (oldField == null) {
|
||||
added.add(newField);
|
||||
} else if (!oldField.getType().equals(newField.getType())) {
|
||||
modified.add(FieldChange.modified(oldField, newField, buildTypeDetail(oldField, newField)));
|
||||
}
|
||||
// 仅 @Schema / 注释文案变化:不纳入字段变更
|
||||
}
|
||||
|
||||
for (FieldInfo oldField : oldFields) {
|
||||
if (!newMap.containsKey(oldField.getName())) {
|
||||
removed.add(oldField);
|
||||
}
|
||||
}
|
||||
|
||||
List<FieldChange> renamed = pairRenames(removed, added);
|
||||
return mergeInOrder(newFields, renamed, modified, added, removed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将删除+新增配对为字段重命名。
|
||||
* 优先:说明相同且类型相同;其次:说明相同但类型不同(重命名+改类型)。
|
||||
*/
|
||||
private List<FieldChange> pairRenames(List<FieldInfo> removed, List<FieldInfo> added) {
|
||||
List<FieldChange> renames = new ArrayList<>();
|
||||
Set<FieldInfo> matchedRemoved = new LinkedHashSet<>();
|
||||
Set<FieldInfo> matchedAdded = new LinkedHashSet<>();
|
||||
|
||||
for (FieldInfo oldField : removed) {
|
||||
FieldInfo pair = findRenamePair(oldField, added, matchedAdded, true);
|
||||
if (pair == null) {
|
||||
pair = findRenamePair(oldField, added, matchedAdded, false);
|
||||
}
|
||||
if (pair != null) {
|
||||
renames.add(FieldChange.renamed(oldField, pair));
|
||||
matchedRemoved.add(oldField);
|
||||
matchedAdded.add(pair);
|
||||
}
|
||||
}
|
||||
|
||||
removed.removeIf(matchedRemoved::contains);
|
||||
added.removeIf(matchedAdded::contains);
|
||||
return renames;
|
||||
}
|
||||
|
||||
private FieldInfo findRenamePair(FieldInfo removed, List<FieldInfo> added,
|
||||
Set<FieldInfo> excluded, boolean requireSameType) {
|
||||
for (FieldInfo candidate : added) {
|
||||
if (excluded.contains(candidate)) {
|
||||
continue;
|
||||
}
|
||||
if (!descriptionsMatch(removed, candidate)) {
|
||||
continue;
|
||||
}
|
||||
if (requireSameType && !removed.getType().equals(candidate.getType())) {
|
||||
continue;
|
||||
}
|
||||
if (!requireSameType && removed.getType().equals(candidate.getType())) {
|
||||
continue;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 说明相同(非空)或双方均为空时视为匹配 */
|
||||
private boolean descriptionsMatch(FieldInfo oldField, FieldInfo newField) {
|
||||
String oldDesc = normalizeDescription(oldField.getDescription());
|
||||
String newDesc = normalizeDescription(newField.getDescription());
|
||||
if (oldDesc.isEmpty() && newDesc.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (oldDesc.isEmpty() || newDesc.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return oldDesc.equals(newDesc);
|
||||
}
|
||||
|
||||
private String normalizeDescription(String description) {
|
||||
return description == null ? "" : description.trim();
|
||||
}
|
||||
|
||||
/** 按新字段声明顺序合并各变更类型 */
|
||||
private List<FieldChange> mergeInOrder(List<FieldInfo> newFields, List<FieldChange> renamed,
|
||||
List<FieldChange> modified, List<FieldInfo> added,
|
||||
List<FieldInfo> removed) {
|
||||
Map<String, FieldChange> renamedByNewName = new LinkedHashMap<>();
|
||||
for (FieldChange change : renamed) {
|
||||
renamedByNewName.put(change.getFieldName(), change);
|
||||
}
|
||||
|
||||
Map<String, FieldChange> modifiedByName = new LinkedHashMap<>();
|
||||
for (FieldChange change : modified) {
|
||||
modifiedByName.put(change.getFieldName(), change);
|
||||
}
|
||||
|
||||
Set<String> emitted = new LinkedHashSet<>();
|
||||
List<FieldChange> result = new ArrayList<>();
|
||||
|
||||
for (FieldInfo newField : newFields) {
|
||||
String name = newField.getName();
|
||||
if (renamedByNewName.containsKey(name)) {
|
||||
result.add(renamedByNewName.get(name));
|
||||
emitted.add(name);
|
||||
} else if (modifiedByName.containsKey(name)) {
|
||||
result.add(modifiedByName.get(name));
|
||||
emitted.add(name);
|
||||
} else if (added.stream().anyMatch(f -> f.getName().equals(name))) {
|
||||
result.add(FieldChange.added(newField));
|
||||
emitted.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
for (FieldInfo oldField : removed) {
|
||||
result.add(FieldChange.removed(oldField));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 字段列表转 LinkedHashMap,保持声明顺序 */
|
||||
private Map<String, FieldInfo> toMap(List<FieldInfo> fields) {
|
||||
Map<String, FieldInfo> map = new LinkedHashMap<>();
|
||||
for (FieldInfo field : fields) {
|
||||
map.put(field.getName(), field);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** 构造类型变化描述,如 Integer → String */
|
||||
private String buildTypeDetail(FieldInfo oldField, FieldInfo newField) {
|
||||
if (oldField.getType().equals(newField.getType())) {
|
||||
return "";
|
||||
}
|
||||
return oldField.getType() + " → " + newField.getType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.codechecker.analyzer;
|
||||
|
||||
import com.codechecker.config.AppConfig;
|
||||
import com.codechecker.model.ApiEndpoint;
|
||||
import com.codechecker.model.ClassChangeReport;
|
||||
import com.codechecker.model.ClassType;
|
||||
import com.codechecker.parser.ConversionParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 根据变更报告匹配受影响的 HTTP 接口与 Dto→Entity 转换目标。
|
||||
*/
|
||||
public class ImpactAnalyzer {
|
||||
private final ConversionParser conversionParser = new ConversionParser();
|
||||
|
||||
/**
|
||||
* 填充 report 的影响列表;新旧类名均参与匹配;Entity/Model 不匹配接口。
|
||||
*/
|
||||
public void analyze(ClassChangeReport report, Map<String, ApiEndpoint> endpointIndex,
|
||||
AppConfig config, Path repoRoot, String newSource, String oldSource) throws IOException {
|
||||
Set<String> matchNames = namesForMatching(report);
|
||||
|
||||
if (report.getClassType() != ClassType.ENTITY && report.getClassType() != ClassType.MODEL) {
|
||||
matchEndpoints(report, endpointIndex, matchNames);
|
||||
}
|
||||
|
||||
if (!config.isDtoEntityConversionEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
analyzeConversion(report, config, repoRoot, newSource, oldSource, matchNames);
|
||||
}
|
||||
|
||||
/** 收集新旧类名用于接口/转换匹配 */
|
||||
private Set<String> namesForMatching(ClassChangeReport report) {
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
names.add(report.getClassName());
|
||||
if (report.getOldClassName() != null && !report.getOldClassName().isBlank()) {
|
||||
names.add(report.getOldClassName());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** 在接口索引中匹配入参/返回类型 */
|
||||
private void matchEndpoints(ClassChangeReport report, Map<String, ApiEndpoint> endpointIndex,
|
||||
Set<String> matchNames) {
|
||||
List<ApiEndpoint> inputImpacts = new ArrayList<>();
|
||||
List<ApiEndpoint> frontendImpacts = new ArrayList<>();
|
||||
|
||||
for (ApiEndpoint endpoint : endpointIndex.values()) {
|
||||
if (matchesAnyType(endpoint.getParamTypes(), matchNames)) {
|
||||
inputImpacts.add(endpoint);
|
||||
}
|
||||
if (matchesAnyType(endpoint.getReturnTypes(), matchNames)) {
|
||||
frontendImpacts.add(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
inputImpacts.forEach(report::addInputImpact);
|
||||
frontendImpacts.forEach(report::addFrontendImpact);
|
||||
}
|
||||
|
||||
/** 扫描 convert 方法与 BeanUtils.copyProperties 关联的 Entity */
|
||||
private void analyzeConversion(ClassChangeReport report, AppConfig config, Path repoRoot,
|
||||
String newSource, String oldSource, Set<String> matchNames) throws IOException {
|
||||
for (String name : matchNames) {
|
||||
if (newSource != null && !newSource.isBlank()) {
|
||||
conversionParser.findConvertTargetsInClass(newSource, name)
|
||||
.forEach(report::addConversionEntity);
|
||||
}
|
||||
if (oldSource != null && !oldSource.isBlank() && !oldSource.equals(newSource)) {
|
||||
conversionParser.findConvertTargetsInClass(oldSource, name)
|
||||
.forEach(report::addConversionEntity);
|
||||
}
|
||||
for (String scanDir : config.getConversionScanDirs()) {
|
||||
conversionParser.findBeanUtilsTargets(repoRoot.resolve(scanDir), name)
|
||||
.forEach(report::addConversionEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 类型集合中是否包含任一目标类名 */
|
||||
private boolean matchesAnyType(Collection<String> types, Set<String> classNames) {
|
||||
if (types == null) {
|
||||
return false;
|
||||
}
|
||||
for (String type : types) {
|
||||
if (classNames.contains(type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user