This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.codechecker;
|
package com.codechecker;
|
||||||
|
|
||||||
import com.codechecker.analyzer.ClassChangeAnalyzer;
|
import com.codechecker.analyzer.ClassChangeAnalyzer;
|
||||||
|
import com.codechecker.analyzer.DtoNestIndex;
|
||||||
import com.codechecker.analyzer.EndpointIndexBuilder;
|
import com.codechecker.analyzer.EndpointIndexBuilder;
|
||||||
import com.codechecker.api.analyzer.ApiChangeAnalyzer;
|
import com.codechecker.api.analyzer.ApiChangeAnalyzer;
|
||||||
import com.codechecker.api.analyzer.DtoImpactedApiAnalyzer;
|
import com.codechecker.api.analyzer.DtoImpactedApiAnalyzer;
|
||||||
@@ -67,23 +68,24 @@ public class CodeCheckMain implements Callable<Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GitChangeScanner gitScanner = new GitChangeScanner(repoRoot.toAbsolutePath());
|
GitChangeScanner gitScanner = new GitChangeScanner(repoRoot.toAbsolutePath());
|
||||||
|
DtoNestIndex nestIndex = DtoNestIndex.build(repoRoot.toAbsolutePath(), appConfig);
|
||||||
List<ClassChangeReport> classReports = List.of();
|
List<ClassChangeReport> classReports = List.of();
|
||||||
List<EndpointChangeReport> apiReports = List.of();
|
List<EndpointChangeReport> apiReports = List.of();
|
||||||
|
|
||||||
if (appConfig.isClassCheckEnabled()) {
|
if (appConfig.isClassCheckEnabled()) {
|
||||||
classReports = analyzeClassChanges(appConfig, gitScanner);
|
classReports = analyzeClassChanges(appConfig, gitScanner, nestIndex);
|
||||||
} else {
|
} else {
|
||||||
System.out.println("类变更检测已关闭(class_check.enabled=false)");
|
System.out.println("类变更检测已关闭(class_check.enabled=false)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appConfig.isApiCheckEnabled()) {
|
if (appConfig.isApiCheckEnabled()) {
|
||||||
apiReports = analyzeApiChanges(appConfig, gitScanner, classReports);
|
apiReports = analyzeApiChanges(appConfig, gitScanner, classReports, nestIndex);
|
||||||
} else {
|
} else {
|
||||||
System.out.println("API 变更检测已关闭(api_check.enabled=false)");
|
System.out.println("API 变更检测已关闭(api_check.enabled=false)");
|
||||||
}
|
}
|
||||||
|
|
||||||
OverlapNotificationFilter.FilterResult filtered = OverlapNotificationFilter.apply(
|
OverlapNotificationFilter.FilterResult filtered = OverlapNotificationFilter.apply(
|
||||||
classReports, apiReports, appConfig.getDtoOverlapMode());
|
classReports, apiReports, appConfig.getDtoOverlapMode(), nestIndex);
|
||||||
int totalSent = sendClassNotifications(appConfig, filtered.classReports())
|
int totalSent = sendClassNotifications(appConfig, filtered.classReports())
|
||||||
+ sendApiNotifications(appConfig, filtered.apiReports());
|
+ sendApiNotifications(appConfig, filtered.apiReports());
|
||||||
|
|
||||||
@@ -93,8 +95,8 @@ public class CodeCheckMain implements Callable<Integer> {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ClassChangeReport> analyzeClassChanges(AppConfig appConfig, GitChangeScanner gitScanner)
|
private List<ClassChangeReport> analyzeClassChanges(AppConfig appConfig, GitChangeScanner gitScanner,
|
||||||
throws Exception {
|
DtoNestIndex nestIndex) throws Exception {
|
||||||
System.out.println("=== 类变更检测 ===");
|
System.out.println("=== 类变更检测 ===");
|
||||||
EndpointIndexBuilder indexBuilder = new EndpointIndexBuilder();
|
EndpointIndexBuilder indexBuilder = new EndpointIndexBuilder();
|
||||||
Map<String, ApiEndpoint> endpointIndex = indexBuilder.buildIndex(repoRoot.toAbsolutePath(), appConfig);
|
Map<String, ApiEndpoint> endpointIndex = indexBuilder.buildIndex(repoRoot.toAbsolutePath(), appConfig);
|
||||||
@@ -102,13 +104,14 @@ public class CodeCheckMain implements Callable<Integer> {
|
|||||||
|
|
||||||
ClassChangeAnalyzer analyzer = new ClassChangeAnalyzer(gitScanner);
|
ClassChangeAnalyzer analyzer = new ClassChangeAnalyzer(gitScanner);
|
||||||
List<ClassChangeReport> reports = analyzer.analyze(
|
List<ClassChangeReport> reports = analyzer.analyze(
|
||||||
repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, endpointIndex);
|
repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, endpointIndex, nestIndex);
|
||||||
System.out.println("检测到需通知的类变更数量: " + reports.size());
|
System.out.println("检测到需通知的类变更数量: " + reports.size());
|
||||||
return reports;
|
return reports;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<EndpointChangeReport> analyzeApiChanges(AppConfig appConfig, GitChangeScanner gitScanner,
|
private List<EndpointChangeReport> analyzeApiChanges(AppConfig appConfig, GitChangeScanner gitScanner,
|
||||||
List<ClassChangeReport> classReports) throws Exception {
|
List<ClassChangeReport> classReports,
|
||||||
|
DtoNestIndex nestIndex) throws Exception {
|
||||||
System.out.println("=== API 变更检测 ===");
|
System.out.println("=== API 变更检测 ===");
|
||||||
ApiFileChangeScanner fileScanner = new ApiFileChangeScanner(gitScanner);
|
ApiFileChangeScanner fileScanner = new ApiFileChangeScanner(gitScanner);
|
||||||
Set<String> changedApiFiles = new LinkedHashSet<>(fileScanner.scanChangedFiles(
|
Set<String> changedApiFiles = new LinkedHashSet<>(fileScanner.scanChangedFiles(
|
||||||
@@ -123,7 +126,7 @@ public class CodeCheckMain implements Callable<Integer> {
|
|||||||
if (appConfig.isDtoApiFollowUpEnabled() && !classReports.isEmpty()) {
|
if (appConfig.isDtoApiFollowUpEnabled() && !classReports.isEmpty()) {
|
||||||
DtoImpactedApiAnalyzer dtoAnalyzer = new DtoImpactedApiAnalyzer(gitScanner);
|
DtoImpactedApiAnalyzer dtoAnalyzer = new DtoImpactedApiAnalyzer(gitScanner);
|
||||||
List<EndpointChangeReport> followUpReports = dtoAnalyzer.analyze(
|
List<EndpointChangeReport> followUpReports = dtoAnalyzer.analyze(
|
||||||
repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, classReports, changedApiFiles);
|
repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, classReports, changedApiFiles, nestIndex);
|
||||||
if (!followUpReports.isEmpty()) {
|
if (!followUpReports.isEmpty()) {
|
||||||
System.out.println("Dto 跟进检测到 API 参数变更数量: " + followUpReports.size());
|
System.out.println("Dto 跟进检测到 API 参数变更数量: " + followUpReports.size());
|
||||||
reports.addAll(followUpReports);
|
reports.addAll(followUpReports);
|
||||||
|
|||||||
@@ -32,16 +32,18 @@ public class ClassChangeAnalyzer {
|
|||||||
|
|
||||||
/** 扫描变更文件并逐条分析,无实质变更的 MODIFIED 会被跳过 */
|
/** 扫描变更文件并逐条分析,无实质变更的 MODIFIED 会被跳过 */
|
||||||
public List<ClassChangeReport> analyze(Path repoRoot, AppConfig config, String oldSha, String newSha,
|
public List<ClassChangeReport> analyze(Path repoRoot, AppConfig config, String oldSha, String newSha,
|
||||||
Map<String, com.codechecker.model.ApiEndpoint> endpointIndex) throws IOException {
|
Map<String, com.codechecker.model.ApiEndpoint> endpointIndex,
|
||||||
|
DtoNestIndex nestIndex) throws IOException {
|
||||||
List<ChangedClassFile> changedFiles = gitScanner.scanChangedClasses(oldSha, newSha);
|
List<ChangedClassFile> changedFiles = gitScanner.scanChangedClasses(oldSha, newSha);
|
||||||
List<ClassChangeReport> reports = new ArrayList<>();
|
List<ClassChangeReport> reports = new ArrayList<>();
|
||||||
|
|
||||||
for (ChangedClassFile changedFile : changedFiles) {
|
for (ChangedClassFile changedFile : changedFiles) {
|
||||||
if (changedFile.getStatus() == ChangedClassFile.ChangeStatus.DELETED) {
|
if (changedFile.getStatus() == ChangedClassFile.ChangeStatus.DELETED) {
|
||||||
reports.add(analyzeDeleted(changedFile, config, repoRoot, oldSha, endpointIndex));
|
reports.add(analyzeDeleted(changedFile, config, repoRoot, oldSha, endpointIndex, nestIndex));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ClassChangeReport report = analyzeModifiedOrRenamed(changedFile, config, repoRoot, oldSha, newSha, endpointIndex);
|
ClassChangeReport report = analyzeModifiedOrRenamed(changedFile, config, repoRoot, oldSha, newSha,
|
||||||
|
endpointIndex, nestIndex);
|
||||||
if (report != null) {
|
if (report != null) {
|
||||||
reports.add(report);
|
reports.add(report);
|
||||||
}
|
}
|
||||||
@@ -51,7 +53,8 @@ public class ClassChangeAnalyzer {
|
|||||||
|
|
||||||
/** 处理删除:标记 DELETED 并分析影响(基于旧源码) */
|
/** 处理删除:标记 DELETED 并分析影响(基于旧源码) */
|
||||||
private ClassChangeReport analyzeDeleted(ChangedClassFile changedFile, AppConfig config, Path repoRoot,
|
private ClassChangeReport analyzeDeleted(ChangedClassFile changedFile, AppConfig config, Path repoRoot,
|
||||||
String oldSha, Map<String, com.codechecker.model.ApiEndpoint> endpointIndex)
|
String oldSha, Map<String, com.codechecker.model.ApiEndpoint> endpointIndex,
|
||||||
|
DtoNestIndex nestIndex)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
String path = changedFile.getRelativePath();
|
String path = changedFile.getRelativePath();
|
||||||
String oldSource = gitScanner.readFileAtCommit(oldSha, path);
|
String oldSource = gitScanner.readFileAtCommit(oldSha, path);
|
||||||
@@ -68,14 +71,15 @@ public class ClassChangeAnalyzer {
|
|||||||
config.isDtoEntityConversionEnabled(),
|
config.isDtoEntityConversionEnabled(),
|
||||||
classDescription
|
classDescription
|
||||||
);
|
);
|
||||||
impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, oldSource, oldSource);
|
impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, oldSource, oldSource, nestIndex);
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理修改/重命名:字段 diff → 判定 changeKind → 影响分析 */
|
/** 处理修改/重命名:字段 diff → 判定 changeKind → 影响分析 */
|
||||||
private ClassChangeReport analyzeModifiedOrRenamed(ChangedClassFile changedFile, AppConfig config,
|
private ClassChangeReport analyzeModifiedOrRenamed(ChangedClassFile changedFile, AppConfig config,
|
||||||
Path repoRoot, String oldSha, String newSha,
|
Path repoRoot, String oldSha, String newSha,
|
||||||
Map<String, com.codechecker.model.ApiEndpoint> endpointIndex)
|
Map<String, com.codechecker.model.ApiEndpoint> endpointIndex,
|
||||||
|
DtoNestIndex nestIndex)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
String oldPath = changedFile.pathForOldCommit();
|
String oldPath = changedFile.pathForOldCommit();
|
||||||
String newPath = changedFile.getRelativePath();
|
String newPath = changedFile.getRelativePath();
|
||||||
@@ -121,7 +125,7 @@ public class ClassChangeAnalyzer {
|
|||||||
classDescription
|
classDescription
|
||||||
);
|
);
|
||||||
fieldChanges.forEach(report::addFieldChange);
|
fieldChanges.forEach(report::addFieldChange);
|
||||||
impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, newSource, oldSource);
|
impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, newSource, oldSource, nestIndex);
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.codechecker.analyzer;
|
||||||
|
|
||||||
|
import com.codechecker.config.AppConfig;
|
||||||
|
import com.codechecker.model.ClassType;
|
||||||
|
import com.codechecker.model.FieldInfo;
|
||||||
|
import com.codechecker.parser.ClassFieldParser;
|
||||||
|
import com.codechecker.parser.TypeNameUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dto/Vo 嵌套关系索引:反向查找祖先容器(用于影响分析与 API 跟进)。
|
||||||
|
*/
|
||||||
|
public class DtoNestIndex {
|
||||||
|
private static final Set<String> LEAF_TYPES = Set.of(
|
||||||
|
"String", "Integer", "int", "Long", "long", "Boolean", "boolean", "Double", "double",
|
||||||
|
"Float", "float", "Short", "short", "Byte", "byte", "Character", "char",
|
||||||
|
"BigDecimal", "BigInteger", "Date", "LocalDate", "LocalDateTime", "LocalTime",
|
||||||
|
"Instant", "Timestamp", "Object", "Void", "void"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final int maxDepth;
|
||||||
|
private final Map<String, Set<String>> ancestorsOf = new LinkedHashMap<>();
|
||||||
|
private final Map<String, String> sourceByClass = new HashMap<>();
|
||||||
|
|
||||||
|
private DtoNestIndex(int maxDepth) {
|
||||||
|
this.maxDepth = maxDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DtoNestIndex build(Path repoRoot, AppConfig config) throws IOException {
|
||||||
|
DtoNestIndex index = new DtoNestIndex(config.getNestMaxDepth());
|
||||||
|
ClassFieldParser fieldParser = new ClassFieldParser();
|
||||||
|
for (String dir : config.getModelDirs()) {
|
||||||
|
Path root = repoRoot.resolve(dir.replace('\\', '/'));
|
||||||
|
if (!Files.exists(root)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try (Stream<Path> paths = Files.walk(root)) {
|
||||||
|
paths.filter(path -> path.toString().endsWith(".java"))
|
||||||
|
.forEach(path -> {
|
||||||
|
String className = path.getFileName().toString().replace(".java", "");
|
||||||
|
if (ClassType.fromClassName(className) != ClassType.DTO
|
||||||
|
&& ClassType.fromClassName(className) != ClassType.VO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String source = Files.readString(path, StandardCharsets.UTF_8);
|
||||||
|
index.sourceByClass.put(className, source);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// 跳过无法读取的文件
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, String> entry : index.sourceByClass.entrySet()) {
|
||||||
|
String rootClass = entry.getKey();
|
||||||
|
List<FieldInfo> fields = fieldParser.parseFields(entry.getValue(), rootClass);
|
||||||
|
Set<String> visiting = new LinkedHashSet<>();
|
||||||
|
index.walkNested(rootClass, fields, rootClass, 1, visiting, fieldParser);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 自身 + 所有祖先 Dto/Vo 类名(用于接口影响匹配) */
|
||||||
|
public Set<String> expandImpactNames(String className) {
|
||||||
|
Set<String> names = new LinkedHashSet<>();
|
||||||
|
if (className != null && !className.isBlank()) {
|
||||||
|
names.add(className);
|
||||||
|
names.addAll(ancestorsOf.getOrDefault(className, Set.of()));
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 嵌套类型的 @RequestBody 根 Dto 祖先(仅 Dto 后缀) */
|
||||||
|
public Set<String> findRequestBodyRoots(String className) {
|
||||||
|
Set<String> roots = new LinkedHashSet<>();
|
||||||
|
if (className != null && className.endsWith("Dto")) {
|
||||||
|
roots.add(className);
|
||||||
|
}
|
||||||
|
for (String ancestor : ancestorsOf.getOrDefault(className, Set.of())) {
|
||||||
|
if (ancestor.endsWith("Dto")) {
|
||||||
|
roots.add(ancestor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxDepth() {
|
||||||
|
return maxDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void walkNested(String ownerClass, List<FieldInfo> fields, String rootAncestor,
|
||||||
|
int depth, Set<String> visiting, ClassFieldParser fieldParser) {
|
||||||
|
if (depth > maxDepth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (FieldInfo field : fields) {
|
||||||
|
for (String nestedType : TypeNameUtils.peelDirectTypeNames(field.getType())) {
|
||||||
|
if (isLeafType(nestedType) || nestedType.equals(ownerClass)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ancestorsOf.computeIfAbsent(nestedType, k -> new LinkedHashSet<>()).add(rootAncestor);
|
||||||
|
if (!visiting.add(nestedType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String nestedSource = sourceByClass.get(nestedType);
|
||||||
|
if (nestedSource != null) {
|
||||||
|
List<FieldInfo> nestedFields = fieldParser.parseFields(nestedSource, nestedType);
|
||||||
|
walkNested(nestedType, nestedFields, rootAncestor, depth + 1, visiting, fieldParser);
|
||||||
|
}
|
||||||
|
visiting.remove(nestedType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isLeafType(String simpleType) {
|
||||||
|
return LEAF_TYPES.contains(simpleType) || simpleType.endsWith("[]");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import com.codechecker.config.AppConfig;
|
|||||||
import com.codechecker.model.ApiEndpoint;
|
import com.codechecker.model.ApiEndpoint;
|
||||||
import com.codechecker.model.ClassChangeReport;
|
import com.codechecker.model.ClassChangeReport;
|
||||||
import com.codechecker.model.ClassType;
|
import com.codechecker.model.ClassType;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import com.codechecker.parser.ConversionParser;
|
import com.codechecker.parser.ConversionParser;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -25,8 +27,9 @@ public class ImpactAnalyzer {
|
|||||||
* 填充 report 的影响列表;新旧类名均参与匹配;Entity/Model 不匹配接口。
|
* 填充 report 的影响列表;新旧类名均参与匹配;Entity/Model 不匹配接口。
|
||||||
*/
|
*/
|
||||||
public void analyze(ClassChangeReport report, Map<String, ApiEndpoint> endpointIndex,
|
public void analyze(ClassChangeReport report, Map<String, ApiEndpoint> endpointIndex,
|
||||||
AppConfig config, Path repoRoot, String newSource, String oldSource) throws IOException {
|
AppConfig config, Path repoRoot, String newSource, String oldSource,
|
||||||
Set<String> matchNames = namesForMatching(report);
|
DtoNestIndex nestIndex) throws IOException {
|
||||||
|
Set<String> matchNames = namesForMatching(report, nestIndex);
|
||||||
|
|
||||||
if (report.getClassType() != ClassType.ENTITY && report.getClassType() != ClassType.MODEL) {
|
if (report.getClassType() != ClassType.ENTITY && report.getClassType() != ClassType.MODEL) {
|
||||||
matchEndpoints(report, endpointIndex, matchNames);
|
matchEndpoints(report, endpointIndex, matchNames);
|
||||||
@@ -39,13 +42,19 @@ public class ImpactAnalyzer {
|
|||||||
analyzeConversion(report, config, repoRoot, newSource, oldSource, matchNames);
|
analyzeConversion(report, config, repoRoot, newSource, oldSource, matchNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 收集新旧类名用于接口/转换匹配 */
|
/** 收集新旧类名及嵌套祖先 Dto/Vo,用于接口/转换匹配 */
|
||||||
private Set<String> namesForMatching(ClassChangeReport report) {
|
private Set<String> namesForMatching(ClassChangeReport report, DtoNestIndex nestIndex) {
|
||||||
Set<String> names = new LinkedHashSet<>();
|
Set<String> names = new LinkedHashSet<>();
|
||||||
names.add(report.getClassName());
|
names.add(report.getClassName());
|
||||||
if (report.getOldClassName() != null && !report.getOldClassName().isBlank()) {
|
if (report.getOldClassName() != null && !report.getOldClassName().isBlank()) {
|
||||||
names.add(report.getOldClassName());
|
names.add(report.getOldClassName());
|
||||||
}
|
}
|
||||||
|
if (nestIndex != null
|
||||||
|
&& (report.getClassType() == ClassType.DTO || report.getClassType() == ClassType.VO)) {
|
||||||
|
for (String name : new ArrayList<>(names)) {
|
||||||
|
names.addAll(nestIndex.expandImpactNames(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class ApiChangeAnalyzer {
|
|||||||
|
|
||||||
EndpointSnapshotParser parser = new EndpointSnapshotParser(config.isApiExcludeFrameworkParams());
|
EndpointSnapshotParser parser = new EndpointSnapshotParser(config.isApiExcludeFrameworkParams());
|
||||||
ParameterDiffEngine parameterDiffEngine = new ParameterDiffEngine(
|
ParameterDiffEngine parameterDiffEngine = new ParameterDiffEngine(
|
||||||
repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha);
|
repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha, config.getNestMaxDepth());
|
||||||
EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine);
|
EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine);
|
||||||
|
|
||||||
List<EndpointSnapshot> oldSnapshots = new ArrayList<>();
|
List<EndpointSnapshot> oldSnapshots = new ArrayList<>();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.codechecker.api.analyzer;
|
package com.codechecker.api.analyzer;
|
||||||
|
|
||||||
|
import com.codechecker.analyzer.DtoNestIndex;
|
||||||
import com.codechecker.api.model.ApiChangeKind;
|
import com.codechecker.api.model.ApiChangeKind;
|
||||||
import com.codechecker.api.model.EndpointChangeReport;
|
import com.codechecker.api.model.EndpointChangeReport;
|
||||||
import com.codechecker.api.model.EndpointSnapshot;
|
import com.codechecker.api.model.EndpointSnapshot;
|
||||||
@@ -21,7 +22,7 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 类变更(Dto 字段)后,对受影响的 Controller 继续 API 参数 diff,产出 PARAM_CHANGED 报告。
|
* 类变更(Dto/Vo 嵌套字段)后,对受影响的 Controller 继续 API 参数 diff,产出 PARAM_CHANGED 报告。
|
||||||
*/
|
*/
|
||||||
public class DtoImpactedApiAnalyzer {
|
public class DtoImpactedApiAnalyzer {
|
||||||
private final GitChangeScanner gitScanner;
|
private final GitChangeScanner gitScanner;
|
||||||
@@ -33,15 +34,17 @@ public class DtoImpactedApiAnalyzer {
|
|||||||
public List<EndpointChangeReport> analyze(Path repoRoot, AppConfig config,
|
public List<EndpointChangeReport> analyze(Path repoRoot, AppConfig config,
|
||||||
String oldSha, String newSha,
|
String oldSha, String newSha,
|
||||||
List<ClassChangeReport> classReports,
|
List<ClassChangeReport> classReports,
|
||||||
Set<String> alreadyScannedFiles) throws IOException {
|
Set<String> alreadyScannedFiles,
|
||||||
Map<String, Set<String>> controllerToDtos = collectImpactedControllers(classReports, alreadyScannedFiles);
|
DtoNestIndex nestIndex) throws IOException {
|
||||||
|
Map<String, Set<String>> controllerToDtos = collectImpactedControllers(classReports, alreadyScannedFiles,
|
||||||
|
nestIndex);
|
||||||
if (controllerToDtos.isEmpty()) {
|
if (controllerToDtos.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
EndpointSnapshotParser parser = new EndpointSnapshotParser(config.isApiExcludeFrameworkParams());
|
EndpointSnapshotParser parser = new EndpointSnapshotParser(config.isApiExcludeFrameworkParams());
|
||||||
ParameterDiffEngine parameterDiffEngine = new ParameterDiffEngine(
|
ParameterDiffEngine parameterDiffEngine = new ParameterDiffEngine(
|
||||||
repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha);
|
repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha, config.getNestMaxDepth());
|
||||||
EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine);
|
EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine);
|
||||||
|
|
||||||
List<EndpointSnapshot> oldSnapshots = new ArrayList<>();
|
List<EndpointSnapshot> oldSnapshots = new ArrayList<>();
|
||||||
@@ -69,24 +72,47 @@ public class DtoImpactedApiAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Set<String>> collectImpactedControllers(List<ClassChangeReport> classReports,
|
private Map<String, Set<String>> collectImpactedControllers(List<ClassChangeReport> classReports,
|
||||||
Set<String> alreadyScannedFiles) {
|
Set<String> alreadyScannedFiles,
|
||||||
|
DtoNestIndex nestIndex) {
|
||||||
Map<String, Set<String>> controllerToDtos = new LinkedHashMap<>();
|
Map<String, Set<String>> controllerToDtos = new LinkedHashMap<>();
|
||||||
for (ClassChangeReport report : classReports) {
|
for (ClassChangeReport report : classReports) {
|
||||||
if (report.getClassType() != ClassType.DTO || report.getFieldChanges().isEmpty()) {
|
if (report.getFieldChanges().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (report.getClassType() != ClassType.DTO && report.getClassType() != ClassType.VO) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Set<String> bodyRoots = resolveBodyRoots(report, nestIndex);
|
||||||
|
if (bodyRoots.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Set<String> dtoNames = dtoNames(report);
|
|
||||||
for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
|
for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
|
||||||
String controllerFile = endpoint.getSourceFile();
|
String controllerFile = endpoint.getSourceFile();
|
||||||
if (alreadyScannedFiles.contains(controllerFile)) {
|
if (alreadyScannedFiles.contains(controllerFile)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
controllerToDtos.computeIfAbsent(controllerFile, k -> new LinkedHashSet<>()).addAll(dtoNames);
|
controllerToDtos.computeIfAbsent(controllerFile, k -> new LinkedHashSet<>()).addAll(bodyRoots);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return controllerToDtos;
|
return controllerToDtos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<String> resolveBodyRoots(ClassChangeReport report, DtoNestIndex nestIndex) {
|
||||||
|
if (nestIndex == null) {
|
||||||
|
Set<String> names = new LinkedHashSet<>();
|
||||||
|
if (report.getClassName().endsWith("Dto")) {
|
||||||
|
names.add(report.getClassName());
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
Set<String> roots = new LinkedHashSet<>();
|
||||||
|
roots.addAll(nestIndex.findRequestBodyRoots(report.getClassName()));
|
||||||
|
if (report.getOldClassName() != null && !report.getOldClassName().isBlank()) {
|
||||||
|
roots.addAll(nestIndex.findRequestBodyRoots(report.getOldClassName()));
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
private String findRelatedDto(EndpointChangeReport report, Map<String, Set<String>> controllerToDtos) {
|
private String findRelatedDto(EndpointChangeReport report, Map<String, Set<String>> controllerToDtos) {
|
||||||
Set<String> impactedDtos = controllerToDtos.getOrDefault(report.getSourceFile(), Set.of());
|
Set<String> impactedDtos = controllerToDtos.getOrDefault(report.getSourceFile(), Set.of());
|
||||||
for (ParameterChange change : report.getParameterChanges()) {
|
for (ParameterChange change : report.getParameterChanges()) {
|
||||||
@@ -101,15 +127,6 @@ public class DtoImpactedApiAnalyzer {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> dtoNames(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 List<String> buildSearchDirs(AppConfig config) {
|
private List<String> buildSearchDirs(AppConfig config) {
|
||||||
List<String> dirs = new ArrayList<>();
|
List<String> dirs = new ArrayList<>();
|
||||||
dirs.addAll(config.getModelDirs());
|
dirs.addAll(config.getModelDirs());
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ public class ParameterDiffEngine {
|
|||||||
private final FieldDiffEngine fieldDiffEngine = new FieldDiffEngine();
|
private final FieldDiffEngine fieldDiffEngine = new FieldDiffEngine();
|
||||||
|
|
||||||
public ParameterDiffEngine(Path repoRoot, List<String> searchDirs,
|
public ParameterDiffEngine(Path repoRoot, List<String> searchDirs,
|
||||||
GitChangeScanner gitScanner, String oldSha, String newSha) {
|
GitChangeScanner gitScanner, String oldSha, String newSha, int maxDepth) {
|
||||||
this.nestedDtoFieldParser = new NestedDtoFieldParser(repoRoot, searchDirs, gitScanner, oldSha, newSha);
|
this.nestedDtoFieldParser = new NestedDtoFieldParser(repoRoot, searchDirs, gitScanner, oldSha, newSha, maxDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ParameterChange> diff(EndpointSnapshot oldSnap, EndpointSnapshot newSnap) throws IOException {
|
public List<ParameterChange> diff(EndpointSnapshot oldSnap, EndpointSnapshot newSnap) throws IOException {
|
||||||
|
|||||||
@@ -29,13 +29,15 @@ public class NestedDtoFieldParser {
|
|||||||
private final GitChangeScanner gitScanner;
|
private final GitChangeScanner gitScanner;
|
||||||
private final String oldSha;
|
private final String oldSha;
|
||||||
private final String newSha;
|
private final String newSha;
|
||||||
|
private final int maxDepth;
|
||||||
|
|
||||||
public NestedDtoFieldParser(Path repoRoot, List<String> searchDirs,
|
public NestedDtoFieldParser(Path repoRoot, List<String> searchDirs,
|
||||||
GitChangeScanner gitScanner, String oldSha, String newSha) {
|
GitChangeScanner gitScanner, String oldSha, String newSha, int maxDepth) {
|
||||||
this.sourceLocator = new JavaSourceLocator(repoRoot, searchDirs);
|
this.sourceLocator = new JavaSourceLocator(repoRoot, searchDirs);
|
||||||
this.gitScanner = gitScanner;
|
this.gitScanner = gitScanner;
|
||||||
this.oldSha = oldSha;
|
this.oldSha = oldSha;
|
||||||
this.newSha = newSha;
|
this.newSha = newSha;
|
||||||
|
this.maxDepth = maxDepth;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NestedFieldInfo> parseNestedFieldsAtOldCommit(String dtoClassName) throws IOException {
|
public List<NestedFieldInfo> parseNestedFieldsAtOldCommit(String dtoClassName) throws IOException {
|
||||||
@@ -49,13 +51,13 @@ public class NestedDtoFieldParser {
|
|||||||
private List<NestedFieldInfo> parseNestedFields(String dtoClassName, String sha) throws IOException {
|
private List<NestedFieldInfo> parseNestedFields(String dtoClassName, String sha) throws IOException {
|
||||||
Set<String> visiting = new HashSet<>();
|
Set<String> visiting = new HashSet<>();
|
||||||
List<NestedFieldInfo> result = new ArrayList<>();
|
List<NestedFieldInfo> result = new ArrayList<>();
|
||||||
collectFields(dtoClassName, "", visiting, result, sha);
|
collectFields(dtoClassName, "", visiting, result, sha, 1);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectFields(String className, String prefix, Set<String> visiting,
|
private void collectFields(String className, String prefix, Set<String> visiting,
|
||||||
List<NestedFieldInfo> out, String sha) throws IOException {
|
List<NestedFieldInfo> out, String sha, int depth) throws IOException {
|
||||||
if (className == null || className.isBlank() || visiting.contains(className)) {
|
if (className == null || className.isBlank() || visiting.contains(className) || depth > maxDepth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visiting.add(className);
|
visiting.add(className);
|
||||||
@@ -67,12 +69,17 @@ public class NestedDtoFieldParser {
|
|||||||
List<FieldInfo> fields = classFieldParser.parseFields(source.get(), className);
|
List<FieldInfo> fields = classFieldParser.parseFields(source.get(), className);
|
||||||
for (FieldInfo field : fields) {
|
for (FieldInfo field : fields) {
|
||||||
String path = prefix.isBlank() ? field.getName() : prefix + "." + field.getName();
|
String path = prefix.isBlank() ? field.getName() : prefix + "." + field.getName();
|
||||||
String simpleType = TypeNameUtils.simpleName(field.getType());
|
Set<String> nestedTypes = TypeNameUtils.peelDirectTypeNames(field.getType());
|
||||||
if (isLeafType(simpleType)) {
|
boolean expanded = false;
|
||||||
|
for (String nestedType : nestedTypes) {
|
||||||
|
if (isLeafType(nestedType) || nestedType.equals(className)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expanded = true;
|
||||||
|
collectFields(nestedType, path, visiting, out, sha, depth + 1);
|
||||||
|
}
|
||||||
|
if (!expanded) {
|
||||||
out.add(new NestedFieldInfo(path, field.getType(), field.getDescription()));
|
out.add(new NestedFieldInfo(path, field.getType(), field.getDescription()));
|
||||||
} else {
|
|
||||||
out.add(new NestedFieldInfo(path, field.getType(), field.getDescription()));
|
|
||||||
collectFields(simpleType, path, visiting, out, sha);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visiting.remove(className);
|
visiting.remove(className);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class AppConfig {
|
|||||||
private boolean onlyOnChange = true;
|
private boolean onlyOnChange = true;
|
||||||
|
|
||||||
private boolean dtoApiFollowUpEnabled = true;
|
private boolean dtoApiFollowUpEnabled = true;
|
||||||
|
private int nestMaxDepth = 3;
|
||||||
private boolean apiCheckEnabled = true;
|
private boolean apiCheckEnabled = true;
|
||||||
private boolean apiExcludeFrameworkParams = true;
|
private boolean apiExcludeFrameworkParams = true;
|
||||||
private List<String> apiControllerScanDirs = new ArrayList<>();
|
private List<String> apiControllerScanDirs = new ArrayList<>();
|
||||||
@@ -54,6 +55,9 @@ public class AppConfig {
|
|||||||
Map<String, Object> dtoApiFollowUp = mapOrEmpty(classCheck.get("dto_api_follow_up"));
|
Map<String, Object> dtoApiFollowUp = mapOrEmpty(classCheck.get("dto_api_follow_up"));
|
||||||
config.dtoApiFollowUpEnabled = boolOrDefault(dtoApiFollowUp.get("enabled"), true);
|
config.dtoApiFollowUpEnabled = boolOrDefault(dtoApiFollowUp.get("enabled"), true);
|
||||||
|
|
||||||
|
Map<String, Object> nestIndex = mapOrEmpty(classCheck.get("nest_index"));
|
||||||
|
config.nestMaxDepth = intOrDefault(nestIndex.get("max_depth"), 3);
|
||||||
|
|
||||||
Map<String, Object> conversion = mapOrEmpty(classCheck.get("dto_entity_conversion"));
|
Map<String, Object> conversion = mapOrEmpty(classCheck.get("dto_entity_conversion"));
|
||||||
config.dtoEntityConversionEnabled = boolOrDefault(conversion.get("enabled"), true);
|
config.dtoEntityConversionEnabled = boolOrDefault(conversion.get("enabled"), true);
|
||||||
|
|
||||||
@@ -120,6 +124,21 @@ public class AppConfig {
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 安全转为 int,缺省用 defaultValue */
|
||||||
|
private static int intOrDefault(Object value, int defaultValue) {
|
||||||
|
if (value instanceof Number) {
|
||||||
|
return ((Number) value).intValue();
|
||||||
|
}
|
||||||
|
if (value != null) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(value.toString().trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// 使用默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
/** 安全转为字符串,null 则空串 */
|
/** 安全转为字符串,null 则空串 */
|
||||||
private static String stringOrEmpty(Object value) {
|
private static String stringOrEmpty(Object value) {
|
||||||
return value == null ? "" : value.toString();
|
return value == null ? "" : value.toString();
|
||||||
@@ -180,6 +199,11 @@ public class AppConfig {
|
|||||||
return dtoApiFollowUpEnabled;
|
return dtoApiFollowUpEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dto/Vo 嵌套展开最大深度(默认 3,可按需调至 4、5) */
|
||||||
|
public int getNestMaxDepth() {
|
||||||
|
return nestMaxDepth;
|
||||||
|
}
|
||||||
|
|
||||||
/** API 变更检测总开关 */
|
/** API 变更检测总开关 */
|
||||||
public boolean isApiCheckEnabled() {
|
public boolean isApiCheckEnabled() {
|
||||||
return apiCheckEnabled;
|
return apiCheckEnabled;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.codechecker.notify;
|
package com.codechecker.notify;
|
||||||
|
|
||||||
|
import com.codechecker.analyzer.DtoNestIndex;
|
||||||
import com.codechecker.api.model.ApiChangeKind;
|
import com.codechecker.api.model.ApiChangeKind;
|
||||||
import com.codechecker.api.model.EndpointChangeReport;
|
import com.codechecker.api.model.EndpointChangeReport;
|
||||||
import com.codechecker.api.model.ParameterChange;
|
import com.codechecker.api.model.ParameterChange;
|
||||||
@@ -38,11 +39,12 @@ public class OverlapNotificationFilter {
|
|||||||
|
|
||||||
public static FilterResult apply(List<ClassChangeReport> classReports,
|
public static FilterResult apply(List<ClassChangeReport> classReports,
|
||||||
List<EndpointChangeReport> apiReports,
|
List<EndpointChangeReport> apiReports,
|
||||||
DtoOverlapMode mode) {
|
DtoOverlapMode mode,
|
||||||
|
DtoNestIndex nestIndex) {
|
||||||
if (mode == DtoOverlapMode.BOTH) {
|
if (mode == DtoOverlapMode.BOTH) {
|
||||||
return new FilterResult(classReports, apiReports);
|
return new FilterResult(classReports, apiReports);
|
||||||
}
|
}
|
||||||
Set<OverlapKey> overlapKeys = buildOverlapKeys(classReports);
|
Set<OverlapKey> overlapKeys = buildOverlapKeys(classReports, nestIndex);
|
||||||
if (overlapKeys.isEmpty()) {
|
if (overlapKeys.isEmpty()) {
|
||||||
return new FilterResult(classReports, apiReports);
|
return new FilterResult(classReports, apiReports);
|
||||||
}
|
}
|
||||||
@@ -50,10 +52,15 @@ public class OverlapNotificationFilter {
|
|||||||
return new FilterResult(classReports, filterApiReports(apiReports, overlapKeys));
|
return new FilterResult(classReports, filterApiReports(apiReports, overlapKeys));
|
||||||
}
|
}
|
||||||
Set<OverlapKey> apiOverlapKeys = buildApiOverlapKeys(apiReports);
|
Set<OverlapKey> apiOverlapKeys = buildApiOverlapKeys(apiReports);
|
||||||
return new FilterResult(filterClassReportsForApiOnly(classReports, apiOverlapKeys), apiReports);
|
return new FilterResult(filterClassReportsForApiOnly(classReports, apiOverlapKeys, nestIndex), apiReports);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Set<OverlapKey> buildOverlapKeys(List<ClassChangeReport> classReports) {
|
/**
|
||||||
|
* 重叠键使用 @RequestBody 根 Dto(如 PunishmentsApprovalDto),与 API 参数通知 parentDto 对齐;
|
||||||
|
* 嵌套子 Dto(如 UserSelfDto)通过 nestIndex 解析到根 Dto。
|
||||||
|
*/
|
||||||
|
private static Set<OverlapKey> buildOverlapKeys(List<ClassChangeReport> classReports,
|
||||||
|
DtoNestIndex nestIndex) {
|
||||||
Set<OverlapKey> keys = new LinkedHashSet<>();
|
Set<OverlapKey> keys = new LinkedHashSet<>();
|
||||||
for (ClassChangeReport report : classReports) {
|
for (ClassChangeReport report : classReports) {
|
||||||
if (report.getClassType() != ClassType.DTO) {
|
if (report.getClassType() != ClassType.DTO) {
|
||||||
@@ -62,16 +69,30 @@ public class OverlapNotificationFilter {
|
|||||||
if (!hasDtoFieldChanges(report)) {
|
if (!hasDtoFieldChanges(report)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Set<String> dtoNames = dtoNames(report);
|
Set<String> bodyRoots = requestBodyRoots(report, nestIndex);
|
||||||
for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
|
for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
|
||||||
for (String dtoName : dtoNames) {
|
for (String rootDto : bodyRoots) {
|
||||||
keys.add(new OverlapKey(dtoName, endpoint.endpointKey()));
|
keys.add(new OverlapKey(rootDto, endpoint.endpointKey()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Set<String> requestBodyRoots(ClassChangeReport report, DtoNestIndex nestIndex) {
|
||||||
|
Set<String> roots = new LinkedHashSet<>();
|
||||||
|
if (nestIndex != null) {
|
||||||
|
roots.addAll(nestIndex.findRequestBodyRoots(report.getClassName()));
|
||||||
|
if (report.getOldClassName() != null && !report.getOldClassName().isBlank()) {
|
||||||
|
roots.addAll(nestIndex.findRequestBodyRoots(report.getOldClassName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (roots.isEmpty() && report.getClassName().endsWith("Dto")) {
|
||||||
|
roots.add(report.getClassName());
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<EndpointChangeReport> filterApiReports(List<EndpointChangeReport> apiReports,
|
private static List<EndpointChangeReport> filterApiReports(List<EndpointChangeReport> apiReports,
|
||||||
Set<OverlapKey> overlapKeys) {
|
Set<OverlapKey> overlapKeys) {
|
||||||
List<EndpointChangeReport> kept = new ArrayList<>();
|
List<EndpointChangeReport> kept = new ArrayList<>();
|
||||||
@@ -84,10 +105,11 @@ public class OverlapNotificationFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static List<ClassChangeReport> filterClassReportsForApiOnly(List<ClassChangeReport> classReports,
|
private static List<ClassChangeReport> filterClassReportsForApiOnly(List<ClassChangeReport> classReports,
|
||||||
Set<OverlapKey> apiOverlapKeys) {
|
Set<OverlapKey> apiOverlapKeys,
|
||||||
|
DtoNestIndex nestIndex) {
|
||||||
List<ClassChangeReport> kept = new ArrayList<>();
|
List<ClassChangeReport> kept = new ArrayList<>();
|
||||||
for (ClassChangeReport report : classReports) {
|
for (ClassChangeReport report : classReports) {
|
||||||
if (!shouldSuppressClassForApiOnly(report, apiOverlapKeys)) {
|
if (!shouldSuppressClassForApiOnly(report, apiOverlapKeys, nestIndex)) {
|
||||||
kept.add(report);
|
kept.add(report);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,13 +143,15 @@ public class OverlapNotificationFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean shouldSuppressClassForApiOnly(ClassChangeReport report,
|
private static boolean shouldSuppressClassForApiOnly(ClassChangeReport report,
|
||||||
Set<OverlapKey> apiOverlapKeys) {
|
Set<OverlapKey> apiOverlapKeys,
|
||||||
|
DtoNestIndex nestIndex) {
|
||||||
if (report.getClassType() != ClassType.DTO || !hasDtoFieldChanges(report)) {
|
if (report.getClassType() != ClassType.DTO || !hasDtoFieldChanges(report)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
Set<String> bodyRoots = requestBodyRoots(report, nestIndex);
|
||||||
for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
|
for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
|
||||||
for (String dtoName : dtoNames(report)) {
|
for (String rootDto : bodyRoots) {
|
||||||
if (apiOverlapKeys.contains(new OverlapKey(dtoName, endpoint.endpointKey()))) {
|
if (apiOverlapKeys.contains(new OverlapKey(rootDto, endpoint.endpointKey()))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,15 +189,6 @@ public class OverlapNotificationFilter {
|
|||||||
return !report.getFieldChanges().isEmpty();
|
return !report.getFieldChanges().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Set<String> dtoNames(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 static final class OverlapKey {
|
private static final class OverlapKey {
|
||||||
private final String dtoClassName;
|
private final String dtoClassName;
|
||||||
private final String endpointKey;
|
private final String endpointKey;
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ class_check:
|
|||||||
# Dto 类字段变更后,继续检测受影响 Controller 的 API 参数变更
|
# Dto 类字段变更后,继续检测受影响 Controller 的 API 参数变更
|
||||||
dto_api_follow_up:
|
dto_api_follow_up:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
# Dto/Vo 嵌套关系索引:影响分析传播 & API 参数字段展开深度
|
||||||
|
nest_index:
|
||||||
|
max_depth: 3
|
||||||
dto_entity_conversion:
|
dto_entity_conversion:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user