项目结构变更

This commit is contained in:
2026-06-09 11:20:24 +08:00
parent fb6cd124c8
commit 871823b3da
45 changed files with 222 additions and 223 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}