diff --git a/.gitea/checker/src/main/java/com/codechecker/CodeCheckMain.java b/.gitea/checker/src/main/java/com/codechecker/CodeCheckMain.java index 5719409..f26ae68 100644 --- a/.gitea/checker/src/main/java/com/codechecker/CodeCheckMain.java +++ b/.gitea/checker/src/main/java/com/codechecker/CodeCheckMain.java @@ -1,6 +1,7 @@ package com.codechecker; import com.codechecker.analyzer.ClassChangeAnalyzer; +import com.codechecker.analyzer.DtoNestIndex; import com.codechecker.analyzer.EndpointIndexBuilder; import com.codechecker.api.analyzer.ApiChangeAnalyzer; import com.codechecker.api.analyzer.DtoImpactedApiAnalyzer; @@ -67,23 +68,24 @@ public class CodeCheckMain implements Callable { } GitChangeScanner gitScanner = new GitChangeScanner(repoRoot.toAbsolutePath()); + DtoNestIndex nestIndex = DtoNestIndex.build(repoRoot.toAbsolutePath(), appConfig); List classReports = List.of(); List apiReports = List.of(); if (appConfig.isClassCheckEnabled()) { - classReports = analyzeClassChanges(appConfig, gitScanner); + classReports = analyzeClassChanges(appConfig, gitScanner, nestIndex); } else { System.out.println("类变更检测已关闭(class_check.enabled=false)"); } if (appConfig.isApiCheckEnabled()) { - apiReports = analyzeApiChanges(appConfig, gitScanner, classReports); + apiReports = analyzeApiChanges(appConfig, gitScanner, classReports, nestIndex); } else { System.out.println("API 变更检测已关闭(api_check.enabled=false)"); } OverlapNotificationFilter.FilterResult filtered = OverlapNotificationFilter.apply( - classReports, apiReports, appConfig.getDtoOverlapMode()); + classReports, apiReports, appConfig.getDtoOverlapMode(), nestIndex); int totalSent = sendClassNotifications(appConfig, filtered.classReports()) + sendApiNotifications(appConfig, filtered.apiReports()); @@ -93,8 +95,8 @@ public class CodeCheckMain implements Callable { return 0; } - private List analyzeClassChanges(AppConfig appConfig, GitChangeScanner gitScanner) - throws Exception { + private List analyzeClassChanges(AppConfig appConfig, GitChangeScanner gitScanner, + DtoNestIndex nestIndex) throws Exception { System.out.println("=== 类变更检测 ==="); EndpointIndexBuilder indexBuilder = new EndpointIndexBuilder(); Map endpointIndex = indexBuilder.buildIndex(repoRoot.toAbsolutePath(), appConfig); @@ -102,13 +104,14 @@ public class CodeCheckMain implements Callable { ClassChangeAnalyzer analyzer = new ClassChangeAnalyzer(gitScanner); List reports = analyzer.analyze( - repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, endpointIndex); + repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, endpointIndex, nestIndex); System.out.println("检测到需通知的类变更数量: " + reports.size()); return reports; } private List analyzeApiChanges(AppConfig appConfig, GitChangeScanner gitScanner, - List classReports) throws Exception { + List classReports, + DtoNestIndex nestIndex) throws Exception { System.out.println("=== API 变更检测 ==="); ApiFileChangeScanner fileScanner = new ApiFileChangeScanner(gitScanner); Set changedApiFiles = new LinkedHashSet<>(fileScanner.scanChangedFiles( @@ -123,7 +126,7 @@ public class CodeCheckMain implements Callable { if (appConfig.isDtoApiFollowUpEnabled() && !classReports.isEmpty()) { DtoImpactedApiAnalyzer dtoAnalyzer = new DtoImpactedApiAnalyzer(gitScanner); List followUpReports = dtoAnalyzer.analyze( - repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, classReports, changedApiFiles); + repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, classReports, changedApiFiles, nestIndex); if (!followUpReports.isEmpty()) { System.out.println("Dto 跟进检测到 API 参数变更数量: " + followUpReports.size()); reports.addAll(followUpReports); diff --git a/.gitea/checker/src/main/java/com/codechecker/analyzer/ClassChangeAnalyzer.java b/.gitea/checker/src/main/java/com/codechecker/analyzer/ClassChangeAnalyzer.java index 0746457..97cd605 100644 --- a/.gitea/checker/src/main/java/com/codechecker/analyzer/ClassChangeAnalyzer.java +++ b/.gitea/checker/src/main/java/com/codechecker/analyzer/ClassChangeAnalyzer.java @@ -32,16 +32,18 @@ public class ClassChangeAnalyzer { /** 扫描变更文件并逐条分析,无实质变更的 MODIFIED 会被跳过 */ public List analyze(Path repoRoot, AppConfig config, String oldSha, String newSha, - Map endpointIndex) throws IOException { + Map endpointIndex, + DtoNestIndex nestIndex) throws IOException { List changedFiles = gitScanner.scanChangedClasses(oldSha, newSha); List reports = new ArrayList<>(); for (ChangedClassFile changedFile : changedFiles) { if (changedFile.getStatus() == ChangedClassFile.ChangeStatus.DELETED) { - reports.add(analyzeDeleted(changedFile, config, repoRoot, oldSha, endpointIndex)); + reports.add(analyzeDeleted(changedFile, config, repoRoot, oldSha, endpointIndex, nestIndex)); continue; } - ClassChangeReport report = analyzeModifiedOrRenamed(changedFile, config, repoRoot, oldSha, newSha, endpointIndex); + ClassChangeReport report = analyzeModifiedOrRenamed(changedFile, config, repoRoot, oldSha, newSha, + endpointIndex, nestIndex); if (report != null) { reports.add(report); } @@ -51,7 +53,8 @@ public class ClassChangeAnalyzer { /** 处理删除:标记 DELETED 并分析影响(基于旧源码) */ private ClassChangeReport analyzeDeleted(ChangedClassFile changedFile, AppConfig config, Path repoRoot, - String oldSha, Map endpointIndex) + String oldSha, Map endpointIndex, + DtoNestIndex nestIndex) throws IOException { String path = changedFile.getRelativePath(); String oldSource = gitScanner.readFileAtCommit(oldSha, path); @@ -68,14 +71,15 @@ public class ClassChangeAnalyzer { config.isDtoEntityConversionEnabled(), classDescription ); - impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, oldSource, oldSource); + impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, oldSource, oldSource, nestIndex); return report; } /** 处理修改/重命名:字段 diff → 判定 changeKind → 影响分析 */ private ClassChangeReport analyzeModifiedOrRenamed(ChangedClassFile changedFile, AppConfig config, Path repoRoot, String oldSha, String newSha, - Map endpointIndex) + Map endpointIndex, + DtoNestIndex nestIndex) throws IOException { String oldPath = changedFile.pathForOldCommit(); String newPath = changedFile.getRelativePath(); @@ -121,7 +125,7 @@ public class ClassChangeAnalyzer { classDescription ); fieldChanges.forEach(report::addFieldChange); - impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, newSource, oldSource); + impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, newSource, oldSource, nestIndex); return report; } } diff --git a/.gitea/checker/src/main/java/com/codechecker/analyzer/DtoNestIndex.java b/.gitea/checker/src/main/java/com/codechecker/analyzer/DtoNestIndex.java new file mode 100644 index 0000000..2539339 --- /dev/null +++ b/.gitea/checker/src/main/java/com/codechecker/analyzer/DtoNestIndex.java @@ -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 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> ancestorsOf = new LinkedHashMap<>(); + private final Map 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 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 entry : index.sourceByClass.entrySet()) { + String rootClass = entry.getKey(); + List fields = fieldParser.parseFields(entry.getValue(), rootClass); + Set visiting = new LinkedHashSet<>(); + index.walkNested(rootClass, fields, rootClass, 1, visiting, fieldParser); + } + return index; + } + + /** 自身 + 所有祖先 Dto/Vo 类名(用于接口影响匹配) */ + public Set expandImpactNames(String className) { + Set 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 findRequestBodyRoots(String className) { + Set 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 fields, String rootAncestor, + int depth, Set 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 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("[]"); + } +} diff --git a/.gitea/checker/src/main/java/com/codechecker/analyzer/ImpactAnalyzer.java b/.gitea/checker/src/main/java/com/codechecker/analyzer/ImpactAnalyzer.java index fb761bd..44410d7 100644 --- a/.gitea/checker/src/main/java/com/codechecker/analyzer/ImpactAnalyzer.java +++ b/.gitea/checker/src/main/java/com/codechecker/analyzer/ImpactAnalyzer.java @@ -4,6 +4,8 @@ import com.codechecker.config.AppConfig; import com.codechecker.model.ApiEndpoint; import com.codechecker.model.ClassChangeReport; import com.codechecker.model.ClassType; + +import java.util.ArrayList; import com.codechecker.parser.ConversionParser; import java.io.IOException; @@ -25,8 +27,9 @@ public class ImpactAnalyzer { * 填充 report 的影响列表;新旧类名均参与匹配;Entity/Model 不匹配接口。 */ public void analyze(ClassChangeReport report, Map endpointIndex, - AppConfig config, Path repoRoot, String newSource, String oldSource) throws IOException { - Set matchNames = namesForMatching(report); + AppConfig config, Path repoRoot, String newSource, String oldSource, + DtoNestIndex nestIndex) throws IOException { + Set matchNames = namesForMatching(report, nestIndex); if (report.getClassType() != ClassType.ENTITY && report.getClassType() != ClassType.MODEL) { matchEndpoints(report, endpointIndex, matchNames); @@ -39,13 +42,19 @@ public class ImpactAnalyzer { analyzeConversion(report, config, repoRoot, newSource, oldSource, matchNames); } - /** 收集新旧类名用于接口/转换匹配 */ - private Set namesForMatching(ClassChangeReport report) { + /** 收集新旧类名及嵌套祖先 Dto/Vo,用于接口/转换匹配 */ + private Set namesForMatching(ClassChangeReport report, DtoNestIndex nestIndex) { Set names = new LinkedHashSet<>(); names.add(report.getClassName()); if (report.getOldClassName() != null && !report.getOldClassName().isBlank()) { 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; } diff --git a/.gitea/checker/src/main/java/com/codechecker/api/analyzer/ApiChangeAnalyzer.java b/.gitea/checker/src/main/java/com/codechecker/api/analyzer/ApiChangeAnalyzer.java index 376539f..4515d0d 100644 --- a/.gitea/checker/src/main/java/com/codechecker/api/analyzer/ApiChangeAnalyzer.java +++ b/.gitea/checker/src/main/java/com/codechecker/api/analyzer/ApiChangeAnalyzer.java @@ -34,7 +34,7 @@ public class ApiChangeAnalyzer { EndpointSnapshotParser parser = new EndpointSnapshotParser(config.isApiExcludeFrameworkParams()); ParameterDiffEngine parameterDiffEngine = new ParameterDiffEngine( - repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha); + repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha, config.getNestMaxDepth()); EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine); List oldSnapshots = new ArrayList<>(); diff --git a/.gitea/checker/src/main/java/com/codechecker/api/analyzer/DtoImpactedApiAnalyzer.java b/.gitea/checker/src/main/java/com/codechecker/api/analyzer/DtoImpactedApiAnalyzer.java index 68225e5..3b0736d 100644 --- a/.gitea/checker/src/main/java/com/codechecker/api/analyzer/DtoImpactedApiAnalyzer.java +++ b/.gitea/checker/src/main/java/com/codechecker/api/analyzer/DtoImpactedApiAnalyzer.java @@ -1,5 +1,6 @@ package com.codechecker.api.analyzer; +import com.codechecker.analyzer.DtoNestIndex; import com.codechecker.api.model.ApiChangeKind; import com.codechecker.api.model.EndpointChangeReport; import com.codechecker.api.model.EndpointSnapshot; @@ -21,7 +22,7 @@ import java.util.Map; import java.util.Set; /** - * 类变更(Dto 字段)后,对受影响的 Controller 继续 API 参数 diff,产出 PARAM_CHANGED 报告。 + * 类变更(Dto/Vo 嵌套字段)后,对受影响的 Controller 继续 API 参数 diff,产出 PARAM_CHANGED 报告。 */ public class DtoImpactedApiAnalyzer { private final GitChangeScanner gitScanner; @@ -33,15 +34,17 @@ public class DtoImpactedApiAnalyzer { public List analyze(Path repoRoot, AppConfig config, String oldSha, String newSha, List classReports, - Set alreadyScannedFiles) throws IOException { - Map> controllerToDtos = collectImpactedControllers(classReports, alreadyScannedFiles); + Set alreadyScannedFiles, + DtoNestIndex nestIndex) throws IOException { + Map> controllerToDtos = collectImpactedControllers(classReports, alreadyScannedFiles, + nestIndex); if (controllerToDtos.isEmpty()) { return List.of(); } EndpointSnapshotParser parser = new EndpointSnapshotParser(config.isApiExcludeFrameworkParams()); ParameterDiffEngine parameterDiffEngine = new ParameterDiffEngine( - repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha); + repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha, config.getNestMaxDepth()); EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine); List oldSnapshots = new ArrayList<>(); @@ -69,24 +72,47 @@ public class DtoImpactedApiAnalyzer { } private Map> collectImpactedControllers(List classReports, - Set alreadyScannedFiles) { + Set alreadyScannedFiles, + DtoNestIndex nestIndex) { Map> controllerToDtos = new LinkedHashMap<>(); 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 bodyRoots = resolveBodyRoots(report, nestIndex); + if (bodyRoots.isEmpty()) { continue; } - Set dtoNames = dtoNames(report); for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) { String controllerFile = endpoint.getSourceFile(); if (alreadyScannedFiles.contains(controllerFile)) { continue; } - controllerToDtos.computeIfAbsent(controllerFile, k -> new LinkedHashSet<>()).addAll(dtoNames); + controllerToDtos.computeIfAbsent(controllerFile, k -> new LinkedHashSet<>()).addAll(bodyRoots); } } return controllerToDtos; } + private Set resolveBodyRoots(ClassChangeReport report, DtoNestIndex nestIndex) { + if (nestIndex == null) { + Set names = new LinkedHashSet<>(); + if (report.getClassName().endsWith("Dto")) { + names.add(report.getClassName()); + } + return names; + } + Set 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> controllerToDtos) { Set impactedDtos = controllerToDtos.getOrDefault(report.getSourceFile(), Set.of()); for (ParameterChange change : report.getParameterChanges()) { @@ -101,15 +127,6 @@ public class DtoImpactedApiAnalyzer { return null; } - private Set dtoNames(ClassChangeReport report) { - Set names = new LinkedHashSet<>(); - names.add(report.getClassName()); - if (report.getOldClassName() != null && !report.getOldClassName().isBlank()) { - names.add(report.getOldClassName()); - } - return names; - } - private List buildSearchDirs(AppConfig config) { List dirs = new ArrayList<>(); dirs.addAll(config.getModelDirs()); diff --git a/.gitea/checker/src/main/java/com/codechecker/api/analyzer/ParameterDiffEngine.java b/.gitea/checker/src/main/java/com/codechecker/api/analyzer/ParameterDiffEngine.java index 1a789e2..4a241c5 100644 --- a/.gitea/checker/src/main/java/com/codechecker/api/analyzer/ParameterDiffEngine.java +++ b/.gitea/checker/src/main/java/com/codechecker/api/analyzer/ParameterDiffEngine.java @@ -33,8 +33,8 @@ public class ParameterDiffEngine { private final FieldDiffEngine fieldDiffEngine = new FieldDiffEngine(); public ParameterDiffEngine(Path repoRoot, List searchDirs, - GitChangeScanner gitScanner, String oldSha, String newSha) { - this.nestedDtoFieldParser = new NestedDtoFieldParser(repoRoot, searchDirs, gitScanner, oldSha, newSha); + GitChangeScanner gitScanner, String oldSha, String newSha, int maxDepth) { + this.nestedDtoFieldParser = new NestedDtoFieldParser(repoRoot, searchDirs, gitScanner, oldSha, newSha, maxDepth); } public List diff(EndpointSnapshot oldSnap, EndpointSnapshot newSnap) throws IOException { diff --git a/.gitea/checker/src/main/java/com/codechecker/api/parser/NestedDtoFieldParser.java b/.gitea/checker/src/main/java/com/codechecker/api/parser/NestedDtoFieldParser.java index 847a6d2..a751f4f 100644 --- a/.gitea/checker/src/main/java/com/codechecker/api/parser/NestedDtoFieldParser.java +++ b/.gitea/checker/src/main/java/com/codechecker/api/parser/NestedDtoFieldParser.java @@ -29,13 +29,15 @@ public class NestedDtoFieldParser { private final GitChangeScanner gitScanner; private final String oldSha; private final String newSha; + private final int maxDepth; public NestedDtoFieldParser(Path repoRoot, List searchDirs, - GitChangeScanner gitScanner, String oldSha, String newSha) { + GitChangeScanner gitScanner, String oldSha, String newSha, int maxDepth) { this.sourceLocator = new JavaSourceLocator(repoRoot, searchDirs); this.gitScanner = gitScanner; this.oldSha = oldSha; this.newSha = newSha; + this.maxDepth = maxDepth; } public List parseNestedFieldsAtOldCommit(String dtoClassName) throws IOException { @@ -49,13 +51,13 @@ public class NestedDtoFieldParser { private List parseNestedFields(String dtoClassName, String sha) throws IOException { Set visiting = new HashSet<>(); List result = new ArrayList<>(); - collectFields(dtoClassName, "", visiting, result, sha); + collectFields(dtoClassName, "", visiting, result, sha, 1); return result; } private void collectFields(String className, String prefix, Set visiting, - List out, String sha) throws IOException { - if (className == null || className.isBlank() || visiting.contains(className)) { + List out, String sha, int depth) throws IOException { + if (className == null || className.isBlank() || visiting.contains(className) || depth > maxDepth) { return; } visiting.add(className); @@ -67,12 +69,17 @@ public class NestedDtoFieldParser { List fields = classFieldParser.parseFields(source.get(), className); for (FieldInfo field : fields) { String path = prefix.isBlank() ? field.getName() : prefix + "." + field.getName(); - String simpleType = TypeNameUtils.simpleName(field.getType()); - if (isLeafType(simpleType)) { + Set nestedTypes = TypeNameUtils.peelDirectTypeNames(field.getType()); + 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())); - } else { - out.add(new NestedFieldInfo(path, field.getType(), field.getDescription())); - collectFields(simpleType, path, visiting, out, sha); } } visiting.remove(className); diff --git a/.gitea/checker/src/main/java/com/codechecker/config/AppConfig.java b/.gitea/checker/src/main/java/com/codechecker/config/AppConfig.java index caf5936..6b35058 100644 --- a/.gitea/checker/src/main/java/com/codechecker/config/AppConfig.java +++ b/.gitea/checker/src/main/java/com/codechecker/config/AppConfig.java @@ -26,6 +26,7 @@ public class AppConfig { private boolean onlyOnChange = true; private boolean dtoApiFollowUpEnabled = true; + private int nestMaxDepth = 3; private boolean apiCheckEnabled = true; private boolean apiExcludeFrameworkParams = true; private List apiControllerScanDirs = new ArrayList<>(); @@ -54,6 +55,9 @@ public class AppConfig { Map dtoApiFollowUp = mapOrEmpty(classCheck.get("dto_api_follow_up")); config.dtoApiFollowUpEnabled = boolOrDefault(dtoApiFollowUp.get("enabled"), true); + Map nestIndex = mapOrEmpty(classCheck.get("nest_index")); + config.nestMaxDepth = intOrDefault(nestIndex.get("max_depth"), 3); + Map conversion = mapOrEmpty(classCheck.get("dto_entity_conversion")); config.dtoEntityConversionEnabled = boolOrDefault(conversion.get("enabled"), true); @@ -120,6 +124,21 @@ public class AppConfig { 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 则空串 */ private static String stringOrEmpty(Object value) { return value == null ? "" : value.toString(); @@ -180,6 +199,11 @@ public class AppConfig { return dtoApiFollowUpEnabled; } + /** Dto/Vo 嵌套展开最大深度(默认 3,可按需调至 4、5) */ + public int getNestMaxDepth() { + return nestMaxDepth; + } + /** API 变更检测总开关 */ public boolean isApiCheckEnabled() { return apiCheckEnabled; diff --git a/.gitea/checker/src/main/java/com/codechecker/notify/OverlapNotificationFilter.java b/.gitea/checker/src/main/java/com/codechecker/notify/OverlapNotificationFilter.java index 28bd146..948dd7c 100644 --- a/.gitea/checker/src/main/java/com/codechecker/notify/OverlapNotificationFilter.java +++ b/.gitea/checker/src/main/java/com/codechecker/notify/OverlapNotificationFilter.java @@ -1,5 +1,6 @@ package com.codechecker.notify; +import com.codechecker.analyzer.DtoNestIndex; import com.codechecker.api.model.ApiChangeKind; import com.codechecker.api.model.EndpointChangeReport; import com.codechecker.api.model.ParameterChange; @@ -38,11 +39,12 @@ public class OverlapNotificationFilter { public static FilterResult apply(List classReports, List apiReports, - DtoOverlapMode mode) { + DtoOverlapMode mode, + DtoNestIndex nestIndex) { if (mode == DtoOverlapMode.BOTH) { return new FilterResult(classReports, apiReports); } - Set overlapKeys = buildOverlapKeys(classReports); + Set overlapKeys = buildOverlapKeys(classReports, nestIndex); if (overlapKeys.isEmpty()) { return new FilterResult(classReports, apiReports); } @@ -50,10 +52,15 @@ public class OverlapNotificationFilter { return new FilterResult(classReports, filterApiReports(apiReports, overlapKeys)); } Set apiOverlapKeys = buildApiOverlapKeys(apiReports); - return new FilterResult(filterClassReportsForApiOnly(classReports, apiOverlapKeys), apiReports); + return new FilterResult(filterClassReportsForApiOnly(classReports, apiOverlapKeys, nestIndex), apiReports); } - private static Set buildOverlapKeys(List classReports) { + /** + * 重叠键使用 @RequestBody 根 Dto(如 PunishmentsApprovalDto),与 API 参数通知 parentDto 对齐; + * 嵌套子 Dto(如 UserSelfDto)通过 nestIndex 解析到根 Dto。 + */ + private static Set buildOverlapKeys(List classReports, + DtoNestIndex nestIndex) { Set keys = new LinkedHashSet<>(); for (ClassChangeReport report : classReports) { if (report.getClassType() != ClassType.DTO) { @@ -62,16 +69,30 @@ public class OverlapNotificationFilter { if (!hasDtoFieldChanges(report)) { continue; } - Set dtoNames = dtoNames(report); + Set bodyRoots = requestBodyRoots(report, nestIndex); for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) { - for (String dtoName : dtoNames) { - keys.add(new OverlapKey(dtoName, endpoint.endpointKey())); + for (String rootDto : bodyRoots) { + keys.add(new OverlapKey(rootDto, endpoint.endpointKey())); } } } return keys; } + private static Set requestBodyRoots(ClassChangeReport report, DtoNestIndex nestIndex) { + Set 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 filterApiReports(List apiReports, Set overlapKeys) { List kept = new ArrayList<>(); @@ -84,10 +105,11 @@ public class OverlapNotificationFilter { } private static List filterClassReportsForApiOnly(List classReports, - Set apiOverlapKeys) { + Set apiOverlapKeys, + DtoNestIndex nestIndex) { List kept = new ArrayList<>(); for (ClassChangeReport report : classReports) { - if (!shouldSuppressClassForApiOnly(report, apiOverlapKeys)) { + if (!shouldSuppressClassForApiOnly(report, apiOverlapKeys, nestIndex)) { kept.add(report); } } @@ -121,13 +143,15 @@ public class OverlapNotificationFilter { } private static boolean shouldSuppressClassForApiOnly(ClassChangeReport report, - Set apiOverlapKeys) { + Set apiOverlapKeys, + DtoNestIndex nestIndex) { if (report.getClassType() != ClassType.DTO || !hasDtoFieldChanges(report)) { return false; } + Set bodyRoots = requestBodyRoots(report, nestIndex); for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) { - for (String dtoName : dtoNames(report)) { - if (apiOverlapKeys.contains(new OverlapKey(dtoName, endpoint.endpointKey()))) { + for (String rootDto : bodyRoots) { + if (apiOverlapKeys.contains(new OverlapKey(rootDto, endpoint.endpointKey()))) { return true; } } @@ -165,15 +189,6 @@ public class OverlapNotificationFilter { return !report.getFieldChanges().isEmpty(); } - private static Set dtoNames(ClassChangeReport report) { - Set 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 final String dtoClassName; private final String endpointKey; diff --git a/.gitea/workflows/code-check-config.yaml b/.gitea/workflows/code-check-config.yaml index e2bc01c..cb2dd54 100644 --- a/.gitea/workflows/code-check-config.yaml +++ b/.gitea/workflows/code-check-config.yaml @@ -15,6 +15,9 @@ class_check: # Dto 类字段变更后,继续检测受影响 Controller 的 API 参数变更 dto_api_follow_up: enabled: true + # Dto/Vo 嵌套关系索引:影响分析传播 & API 参数字段展开深度 + nest_index: + max_depth: 3 dto_entity_conversion: enabled: false diff --git a/.gitea/workflows/code-checker.jar b/.gitea/workflows/code-checker.jar index ae8f9a1..b327e3a 100644 Binary files a/.gitea/workflows/code-checker.jar and b/.gitea/workflows/code-checker.jar differ