package com.codechecker.api.analyzer; import com.codechecker.analyzer.FieldDiffEngine; import com.codechecker.api.model.EndpointSnapshot; import com.codechecker.api.model.MethodParameterSnapshot; import com.codechecker.api.model.ParameterChange; import com.codechecker.api.parser.NestedDtoFieldParser; import com.codechecker.api.parser.NestedFieldInfo; import com.codechecker.git.GitChangeScanner; import com.codechecker.model.FieldChange; import com.codechecker.model.FieldInfo; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * 接口入参 diff(普通参数 + RequestBody 嵌套 Dto 字段)。 * * path/query 规则: * - 形参名+类型相同,仅绑定名变 → 重命名 * - 形参名+绑定名相同,仅类型变 → 类型变更 * - 仅形参名变(绑定名不变)→ 不通知 * - 类型与绑定名同时变,或三者都变 → 先删除后新增 */ public class ParameterDiffEngine { private final NestedDtoFieldParser nestedDtoFieldParser; private final FieldDiffEngine fieldDiffEngine = new FieldDiffEngine(); public ParameterDiffEngine(Path repoRoot, List searchDirs, 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 { List changes = new ArrayList<>(); changes.addAll(diffBodyParams(oldSnap, newSnap)); changes.addAll(diffBindingParams(oldSnap, newSnap)); return changes; } private List diffBodyParams(EndpointSnapshot oldSnap, EndpointSnapshot newSnap) throws IOException { Map oldParams = filterBySource(oldSnap, "body"); Map newParams = filterBySource(newSnap, "body"); List changes = new ArrayList<>(); for (Map.Entry entry : newParams.entrySet()) { MethodParameterSnapshot oldParam = oldParams.get(entry.getKey()); MethodParameterSnapshot newParam = entry.getValue(); if (oldParam == null) { changes.addAll(addedBodyChanges(newParam)); } else { changes.addAll(diffBodyDto(oldParam, newParam)); } } for (Map.Entry entry : oldParams.entrySet()) { if (!newParams.containsKey(entry.getKey())) { changes.addAll(removedBodyChanges(entry.getValue())); } } return changes; } private List diffBindingParams(EndpointSnapshot oldSnap, EndpointSnapshot newSnap) { Map oldParams = filterBindingParams(oldSnap); Map newParams = filterBindingParams(newSnap); List changes = new ArrayList<>(); List unmatchedOld = new ArrayList<>(); List unmatchedNew = new ArrayList<>(); for (Map.Entry entry : newParams.entrySet()) { MethodParameterSnapshot oldParam = oldParams.get(entry.getKey()); MethodParameterSnapshot newParam = entry.getValue(); if (oldParam == null) { unmatchedNew.add(newParam); } else if (!oldParam.getType().equals(newParam.getType())) { changes.add(ParameterChange.modified( newParam.displayName(), newParam.getType(), newParam.getDescription(), oldParam.getType() + " → " + newParam.getType(), newParam.getSource(), null, null, null)); } } for (Map.Entry entry : oldParams.entrySet()) { if (!newParams.containsKey(entry.getKey())) { unmatchedOld.add(entry.getValue()); } } pairRenamedBindingParams(unmatchedOld, unmatchedNew, changes); for (MethodParameterSnapshot removed : unmatchedOld) { changes.add(ParameterChange.removed( removed.displayName(), removed.getType(), removed.getDescription(), removed.getSource(), null, null, null)); } for (MethodParameterSnapshot added : unmatchedNew) { changes.add(ParameterChange.added( added.displayName(), added.getType(), added.getDescription(), added.getSource(), null, null, null)); } return changes; } /** 形参名+类型相同,仅绑定名变化 → 重命名 */ private void pairRenamedBindingParams(List unmatchedOld, List unmatchedNew, List changes) { Set pairedOld = new HashSet<>(); Set pairedNew = new HashSet<>(); for (MethodParameterSnapshot oldParam : unmatchedOld) { if (pairedOld.contains(oldParam)) { continue; } for (MethodParameterSnapshot newParam : unmatchedNew) { if (pairedNew.contains(newParam)) { continue; } if (!oldParam.getSource().equals(newParam.getSource())) { continue; } if (!oldParam.getName().equals(newParam.getName())) { continue; } if (!oldParam.getType().equals(newParam.getType())) { continue; } changes.add(ParameterChange.renamed( oldParam.getBindingName(), newParam.getBindingName(), newParam.getType(), newParam.getDescription(), newParam.getSource(), null, null, null)); pairedOld.add(oldParam); pairedNew.add(newParam); break; } } unmatchedOld.removeIf(pairedOld::contains); unmatchedNew.removeIf(pairedNew::contains); } private Map filterBindingParams(EndpointSnapshot snap) { Map map = new LinkedHashMap<>(); for (MethodParameterSnapshot p : snap.getParameters()) { if (isBindingParam(p)) { map.put(p.identityKey(), p); } } return map; } private Map filterBySource(EndpointSnapshot snap, String source) { Map map = new LinkedHashMap<>(); for (MethodParameterSnapshot p : snap.getParameters()) { if (source.equals(p.getSource())) { map.put(p.identityKey(), p); } } return map; } private boolean isBindingParam(MethodParameterSnapshot param) { return "path".equals(param.getSource()) || "query".equals(param.getSource()); } private List diffBodyDto(MethodParameterSnapshot oldParam, MethodParameterSnapshot newParam) throws IOException { List oldFields = nestedDtoFieldParser.parseNestedFieldsAtOldCommit(oldParam.getDtoClassName()); List newFields = nestedDtoFieldParser.parseNestedFieldsAtNewCommit(newParam.getDtoClassName()); List fieldChanges = fieldDiffEngine.diff(toFieldInfo(oldFields), toFieldInfo(newFields)); List result = new ArrayList<>(); for (FieldChange fc : fieldChanges) { result.add(mapFieldChange(fc, newParam.getName(), newParam.getDtoClassName())); } return result; } private ParameterChange mapFieldChange(FieldChange fc, String bodyParamName, String dtoName) { String path = fc.getFieldName(); switch (fc.getKind()) { case ADDED: return ParameterChange.added(path, fc.getNewType(), fc.getDescription(), "body", bodyParamName, dtoName, path); case REMOVED: return ParameterChange.removed(path, fc.getOldType(), fc.getDescription(), "body", bodyParamName, dtoName, path); case RENAMED: return ParameterChange.renamed(fc.getOldFieldName(), fc.getFieldName(), fc.getNewType(), fc.getDescription(), "body", bodyParamName, dtoName, path); case MODIFIED: default: return ParameterChange.modified(path, fc.getNewType(), fc.getDescription(), fc.getDetail(), "body", bodyParamName, dtoName, path); } } private List addedBodyChanges(MethodParameterSnapshot param) throws IOException { List list = new ArrayList<>(); for (NestedFieldInfo field : nestedDtoFieldParser.parseNestedFieldsAtNewCommit(param.getDtoClassName())) { list.add(ParameterChange.added(field.getPath(), field.getType(), field.getDescription(), "body", param.getName(), param.getDtoClassName(), field.getPath())); } return list; } private List removedBodyChanges(MethodParameterSnapshot param) throws IOException { List list = new ArrayList<>(); for (NestedFieldInfo field : nestedDtoFieldParser.parseNestedFieldsAtOldCommit(param.getDtoClassName())) { list.add(ParameterChange.removed(field.getPath(), field.getType(), field.getDescription(), "body", param.getName(), param.getDtoClassName(), field.getPath())); } return list; } private List toFieldInfo(List nested) { List result = new ArrayList<>(); for (NestedFieldInfo info : nested) { result.add(new FieldInfo(info.getPath(), info.getType(), info.getDescription())); } return result; } }