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; import com.codechecker.api.model.ParameterChange; import com.codechecker.api.parser.EndpointSnapshotParser; import com.codechecker.config.AppConfig; import com.codechecker.git.GitChangeScanner; import com.codechecker.model.ApiEndpoint; import com.codechecker.model.ClassChangeReport; import com.codechecker.model.ClassType; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * 类变更(Dto/Vo 嵌套字段)后,对受影响的 Controller 继续 API 参数 diff,产出 PARAM_CHANGED 报告。 */ public class DtoImpactedApiAnalyzer { private final GitChangeScanner gitScanner; public DtoImpactedApiAnalyzer(GitChangeScanner gitScanner) { this.gitScanner = gitScanner; } public List analyze(Path repoRoot, AppConfig config, String oldSha, String newSha, List classReports, 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, config.getNestMaxDepth()); EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine); List oldSnapshots = new ArrayList<>(); List newSnapshots = new ArrayList<>(); for (String path : controllerToDtos.keySet()) { boolean feign = isFeignPath(path, config); String oldSource = gitScanner.readFileAtCommit(oldSha, path); String newSource = gitScanner.readFileAtCommit(newSha, path); oldSnapshots.addAll(parser.parseSource(oldSource, path, feign)); newSnapshots.addAll(parser.parseSource(newSource, path, feign)); } List reports = new ArrayList<>(); for (EndpointChangeReport report : endpointDiffEngine.diff(oldSnapshots, newSnapshots)) { if (report.getChangeKind() != ApiChangeKind.PARAM_CHANGED || !report.hasParameterChanges()) { continue; } String relatedDto = findRelatedDto(report, controllerToDtos); if (relatedDto == null) { continue; } reports.add(EndpointChangeReport.dtoFollowUp(report, relatedDto)); } return reports; } private Map> collectImpactedControllers(List classReports, Set alreadyScannedFiles, DtoNestIndex nestIndex) { Map> controllerToDtos = new LinkedHashMap<>(); for (ClassChangeReport report : classReports) { if (report.getFieldChanges().isEmpty()) { continue; } if (report.getClassType() != ClassType.DTO && report.getClassType() != ClassType.VO) { continue; } Set bodyRoots = resolveBodyRoots(report, nestIndex); if (bodyRoots.isEmpty()) { continue; } for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) { String controllerFile = endpoint.getSourceFile(); if (alreadyScannedFiles.contains(controllerFile)) { continue; } 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()) { if (!"body".equals(change.getSource())) { continue; } String parentDto = change.getParentDto(); if (parentDto != null && impactedDtos.contains(parentDto)) { return parentDto; } } return null; } private List buildSearchDirs(AppConfig config) { List dirs = new ArrayList<>(); dirs.addAll(config.getModelDirs()); dirs.addAll(config.getAllApiScanDirs()); return dirs; } private boolean isFeignPath(String path, AppConfig config) { String normalized = path.replace('\\', '/'); for (String dir : config.getApiFeignScanDirs()) { String prefix = dir.replace('\\', '/'); if (!prefix.endsWith("/")) { prefix = prefix + "/"; } if (normalized.startsWith(prefix)) { return true; } } return false; } }