This commit is contained in:
@@ -3,20 +3,28 @@ package com.codechecker;
|
||||
import com.codechecker.analyzer.ClassChangeAnalyzer;
|
||||
import com.codechecker.analyzer.EndpointIndexBuilder;
|
||||
import com.codechecker.api.analyzer.ApiChangeAnalyzer;
|
||||
import com.codechecker.api.analyzer.DtoImpactedApiAnalyzer;
|
||||
import com.codechecker.api.model.ApiChangeKind;
|
||||
import com.codechecker.api.model.EndpointChangeReport;
|
||||
import com.codechecker.api.notify.ApiChangeNotifier;
|
||||
import com.codechecker.api.scanner.ApiFileChangeScanner;
|
||||
import com.codechecker.config.AppConfig;
|
||||
import com.codechecker.git.GitChangeScanner;
|
||||
import com.codechecker.model.ApiEndpoint;
|
||||
import com.codechecker.model.ClassChangeReport;
|
||||
import com.codechecker.notify.OverlapNotificationFilter;
|
||||
import com.codechecker.notify.WeComNotifier;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
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;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
@@ -49,7 +57,7 @@ public class CodeCheckMain implements Callable<Integer> {
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
/** 主流程:类变更与 API 变更独立检测、分条通知 */
|
||||
/** 主流程:类变更与 API 变更检测,支持 Dto 跟进与重叠通知策略 */
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
AppConfig appConfig = AppConfig.load(config.toAbsolutePath());
|
||||
@@ -59,27 +67,34 @@ public class CodeCheckMain implements Callable<Integer> {
|
||||
}
|
||||
|
||||
GitChangeScanner gitScanner = new GitChangeScanner(repoRoot.toAbsolutePath());
|
||||
int totalSent = 0;
|
||||
List<ClassChangeReport> classReports = List.of();
|
||||
List<EndpointChangeReport> apiReports = List.of();
|
||||
|
||||
if (appConfig.isClassCheckEnabled()) {
|
||||
totalSent += executeClassCheck(appConfig, gitScanner);
|
||||
classReports = analyzeClassChanges(appConfig, gitScanner);
|
||||
} else {
|
||||
System.out.println("类变更检测已关闭(class_check.enabled=false)");
|
||||
}
|
||||
|
||||
if (appConfig.isApiCheckEnabled()) {
|
||||
totalSent += executeApiCheck(appConfig, gitScanner);
|
||||
apiReports = analyzeApiChanges(appConfig, gitScanner, classReports);
|
||||
} else {
|
||||
System.out.println("API 变更检测已关闭(api_check.enabled=false)");
|
||||
}
|
||||
|
||||
OverlapNotificationFilter.FilterResult filtered = OverlapNotificationFilter.apply(
|
||||
classReports, apiReports, appConfig.getDtoOverlapMode());
|
||||
int totalSent = sendClassNotifications(appConfig, filtered.classReports())
|
||||
+ sendApiNotifications(appConfig, filtered.apiReports());
|
||||
|
||||
if (totalSent == 0 && appConfig.isOnlyOnChange()) {
|
||||
System.out.println("无变更,静默退出");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int executeClassCheck(AppConfig appConfig, GitChangeScanner gitScanner) throws Exception {
|
||||
private List<ClassChangeReport> analyzeClassChanges(AppConfig appConfig, GitChangeScanner gitScanner)
|
||||
throws Exception {
|
||||
System.out.println("=== 类变更检测 ===");
|
||||
EndpointIndexBuilder indexBuilder = new EndpointIndexBuilder();
|
||||
Map<String, ApiEndpoint> endpointIndex = indexBuilder.buildIndex(repoRoot.toAbsolutePath(), appConfig);
|
||||
@@ -89,6 +104,55 @@ public class CodeCheckMain implements Callable<Integer> {
|
||||
List<ClassChangeReport> reports = analyzer.analyze(
|
||||
repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, endpointIndex);
|
||||
System.out.println("检测到需通知的类变更数量: " + reports.size());
|
||||
return reports;
|
||||
}
|
||||
|
||||
private List<EndpointChangeReport> analyzeApiChanges(AppConfig appConfig, GitChangeScanner gitScanner,
|
||||
List<ClassChangeReport> classReports) throws Exception {
|
||||
System.out.println("=== API 变更检测 ===");
|
||||
ApiFileChangeScanner fileScanner = new ApiFileChangeScanner(gitScanner);
|
||||
Set<String> changedApiFiles = new LinkedHashSet<>(fileScanner.scanChangedFiles(
|
||||
repoRoot.toAbsolutePath(), appConfig.getAllApiScanDirs(), oldSha, newSha));
|
||||
|
||||
ApiChangeAnalyzer analyzer = new ApiChangeAnalyzer(gitScanner);
|
||||
List<EndpointChangeReport> reports = new ArrayList<>();
|
||||
if (!changedApiFiles.isEmpty()) {
|
||||
reports.addAll(analyzer.analyze(repoRoot.toAbsolutePath(), appConfig, oldSha, newSha));
|
||||
}
|
||||
|
||||
if (appConfig.isDtoApiFollowUpEnabled() && !classReports.isEmpty()) {
|
||||
DtoImpactedApiAnalyzer dtoAnalyzer = new DtoImpactedApiAnalyzer(gitScanner);
|
||||
List<EndpointChangeReport> followUpReports = dtoAnalyzer.analyze(
|
||||
repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, classReports, changedApiFiles);
|
||||
if (!followUpReports.isEmpty()) {
|
||||
System.out.println("Dto 跟进检测到 API 参数变更数量: " + followUpReports.size());
|
||||
reports.addAll(followUpReports);
|
||||
}
|
||||
}
|
||||
|
||||
reports = dedupeApiReports(reports);
|
||||
System.out.println("检测到需通知的 API 变更数量: " + reports.size());
|
||||
return reports;
|
||||
}
|
||||
|
||||
private List<EndpointChangeReport> dedupeApiReports(List<EndpointChangeReport> reports) {
|
||||
Map<String, EndpointChangeReport> merged = new LinkedHashMap<>();
|
||||
for (EndpointChangeReport report : reports) {
|
||||
String key = report.getChangeKind() + "|" + report.getHttpMethod() + "|" + report.getUri();
|
||||
EndpointChangeReport existing = merged.get(key);
|
||||
if (existing == null) {
|
||||
merged.put(key, report);
|
||||
continue;
|
||||
}
|
||||
if (report.getChangeKind() == ApiChangeKind.PARAM_CHANGED
|
||||
&& existing.getChangeKind() == ApiChangeKind.PARAM_CHANGED) {
|
||||
report.getParameterChanges().forEach(existing::addParameterChange);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(merged.values());
|
||||
}
|
||||
|
||||
private int sendClassNotifications(AppConfig appConfig, List<ClassChangeReport> reports) {
|
||||
if (reports.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -100,12 +164,7 @@ public class CodeCheckMain implements Callable<Integer> {
|
||||
return reports.size();
|
||||
}
|
||||
|
||||
private int executeApiCheck(AppConfig appConfig, GitChangeScanner gitScanner) throws Exception {
|
||||
System.out.println("=== API 变更检测 ===");
|
||||
ApiChangeAnalyzer analyzer = new ApiChangeAnalyzer(gitScanner);
|
||||
List<EndpointChangeReport> reports = analyzer.analyze(
|
||||
repoRoot.toAbsolutePath(), appConfig, oldSha, newSha);
|
||||
System.out.println("检测到需通知的 API 变更数量: " + reports.size());
|
||||
private int sendApiNotifications(AppConfig appConfig, List<EndpointChangeReport> reports) {
|
||||
if (reports.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class ApiChangeAnalyzer {
|
||||
|
||||
EndpointSnapshotParser parser = new EndpointSnapshotParser(config.isApiExcludeFrameworkParams());
|
||||
ParameterDiffEngine parameterDiffEngine = new ParameterDiffEngine(
|
||||
repoRoot, buildSearchDirs(config));
|
||||
repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha);
|
||||
EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine);
|
||||
|
||||
List<EndpointSnapshot> oldSnapshots = new ArrayList<>();
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.codechecker.api.analyzer;
|
||||
|
||||
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 字段)后,对受影响的 Controller 继续 API 参数 diff,产出 PARAM_CHANGED 报告。
|
||||
*/
|
||||
public class DtoImpactedApiAnalyzer {
|
||||
private final GitChangeScanner gitScanner;
|
||||
|
||||
public DtoImpactedApiAnalyzer(GitChangeScanner gitScanner) {
|
||||
this.gitScanner = gitScanner;
|
||||
}
|
||||
|
||||
public List<EndpointChangeReport> analyze(Path repoRoot, AppConfig config,
|
||||
String oldSha, String newSha,
|
||||
List<ClassChangeReport> classReports,
|
||||
Set<String> alreadyScannedFiles) throws IOException {
|
||||
Map<String, Set<String>> controllerToDtos = collectImpactedControllers(classReports, alreadyScannedFiles);
|
||||
if (controllerToDtos.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
EndpointSnapshotParser parser = new EndpointSnapshotParser(config.isApiExcludeFrameworkParams());
|
||||
ParameterDiffEngine parameterDiffEngine = new ParameterDiffEngine(
|
||||
repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha);
|
||||
EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine);
|
||||
|
||||
List<EndpointSnapshot> oldSnapshots = new ArrayList<>();
|
||||
List<EndpointSnapshot> 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<EndpointChangeReport> 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<String, Set<String>> collectImpactedControllers(List<ClassChangeReport> classReports,
|
||||
Set<String> alreadyScannedFiles) {
|
||||
Map<String, Set<String>> controllerToDtos = new LinkedHashMap<>();
|
||||
for (ClassChangeReport report : classReports) {
|
||||
if (report.getClassType() != ClassType.DTO || report.getFieldChanges().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Set<String> 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);
|
||||
}
|
||||
}
|
||||
return controllerToDtos;
|
||||
}
|
||||
|
||||
private String findRelatedDto(EndpointChangeReport report, Map<String, Set<String>> controllerToDtos) {
|
||||
Set<String> 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 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) {
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
|
||||
@@ -31,8 +32,9 @@ public class ParameterDiffEngine {
|
||||
private final NestedDtoFieldParser nestedDtoFieldParser;
|
||||
private final FieldDiffEngine fieldDiffEngine = new FieldDiffEngine();
|
||||
|
||||
public ParameterDiffEngine(Path repoRoot, List<String> searchDirs) {
|
||||
this.nestedDtoFieldParser = new NestedDtoFieldParser(repoRoot, searchDirs);
|
||||
public ParameterDiffEngine(Path repoRoot, List<String> searchDirs,
|
||||
GitChangeScanner gitScanner, String oldSha, String newSha) {
|
||||
this.nestedDtoFieldParser = new NestedDtoFieldParser(repoRoot, searchDirs, gitScanner, oldSha, newSha);
|
||||
}
|
||||
|
||||
public List<ParameterChange> diff(EndpointSnapshot oldSnap, EndpointSnapshot newSnap) throws IOException {
|
||||
@@ -175,8 +177,8 @@ public class ParameterDiffEngine {
|
||||
|
||||
private List<ParameterChange> diffBodyDto(MethodParameterSnapshot oldParam,
|
||||
MethodParameterSnapshot newParam) throws IOException {
|
||||
List<NestedFieldInfo> oldFields = nestedDtoFieldParser.parseNestedFields(oldParam.getDtoClassName());
|
||||
List<NestedFieldInfo> newFields = nestedDtoFieldParser.parseNestedFields(newParam.getDtoClassName());
|
||||
List<NestedFieldInfo> oldFields = nestedDtoFieldParser.parseNestedFieldsAtOldCommit(oldParam.getDtoClassName());
|
||||
List<NestedFieldInfo> newFields = nestedDtoFieldParser.parseNestedFieldsAtNewCommit(newParam.getDtoClassName());
|
||||
List<FieldChange> fieldChanges = fieldDiffEngine.diff(toFieldInfo(oldFields), toFieldInfo(newFields));
|
||||
List<ParameterChange> result = new ArrayList<>();
|
||||
for (FieldChange fc : fieldChanges) {
|
||||
@@ -206,7 +208,7 @@ public class ParameterDiffEngine {
|
||||
|
||||
private List<ParameterChange> addedBodyChanges(MethodParameterSnapshot param) throws IOException {
|
||||
List<ParameterChange> list = new ArrayList<>();
|
||||
for (NestedFieldInfo field : nestedDtoFieldParser.parseNestedFields(param.getDtoClassName())) {
|
||||
for (NestedFieldInfo field : nestedDtoFieldParser.parseNestedFieldsAtNewCommit(param.getDtoClassName())) {
|
||||
list.add(ParameterChange.added(field.getPath(), field.getType(), field.getDescription(),
|
||||
"body", param.getName(), param.getDtoClassName(), field.getPath()));
|
||||
}
|
||||
@@ -215,7 +217,7 @@ public class ParameterDiffEngine {
|
||||
|
||||
private List<ParameterChange> removedBodyChanges(MethodParameterSnapshot param) throws IOException {
|
||||
List<ParameterChange> list = new ArrayList<>();
|
||||
for (NestedFieldInfo field : nestedDtoFieldParser.parseNestedFields(param.getDtoClassName())) {
|
||||
for (NestedFieldInfo field : nestedDtoFieldParser.parseNestedFieldsAtOldCommit(param.getDtoClassName())) {
|
||||
list.add(ParameterChange.removed(field.getPath(), field.getType(), field.getDescription(),
|
||||
"body", param.getName(), param.getDtoClassName(), field.getPath()));
|
||||
}
|
||||
|
||||
@@ -15,11 +15,20 @@ public class EndpointChangeReport {
|
||||
private final String sourceFile;
|
||||
private final String controllerClass;
|
||||
private final String endpointDescription;
|
||||
private final boolean dtoFollowUp;
|
||||
private final String relatedDtoClassName;
|
||||
private final List<ParameterChange> parameterChanges = new ArrayList<>();
|
||||
|
||||
public EndpointChangeReport(ApiChangeKind changeKind, String httpMethod, String oldHttpMethod,
|
||||
String uri, String oldUri, String sourceFile, String controllerClass,
|
||||
String endpointDescription) {
|
||||
this(changeKind, httpMethod, oldHttpMethod, uri, oldUri, sourceFile, controllerClass,
|
||||
endpointDescription, false, null);
|
||||
}
|
||||
|
||||
public EndpointChangeReport(ApiChangeKind changeKind, String httpMethod, String oldHttpMethod,
|
||||
String uri, String oldUri, String sourceFile, String controllerClass,
|
||||
String endpointDescription, boolean dtoFollowUp, String relatedDtoClassName) {
|
||||
this.changeKind = changeKind;
|
||||
this.httpMethod = httpMethod;
|
||||
this.oldHttpMethod = oldHttpMethod;
|
||||
@@ -28,6 +37,25 @@ public class EndpointChangeReport {
|
||||
this.sourceFile = sourceFile;
|
||||
this.controllerClass = controllerClass;
|
||||
this.endpointDescription = endpointDescription == null ? "" : endpointDescription;
|
||||
this.dtoFollowUp = dtoFollowUp;
|
||||
this.relatedDtoClassName = relatedDtoClassName;
|
||||
}
|
||||
|
||||
/** 基于已有报告创建 Dto 跟进产生的副本 */
|
||||
public static EndpointChangeReport dtoFollowUp(EndpointChangeReport source, String relatedDtoClassName) {
|
||||
EndpointChangeReport copy = new EndpointChangeReport(
|
||||
source.getChangeKind(),
|
||||
source.getHttpMethod(),
|
||||
source.getOldHttpMethod(),
|
||||
source.getUri(),
|
||||
source.getOldUri(),
|
||||
source.getSourceFile(),
|
||||
source.getControllerClass(),
|
||||
source.getEndpointDescription(),
|
||||
true,
|
||||
relatedDtoClassName);
|
||||
source.getParameterChanges().forEach(copy::addParameterChange);
|
||||
return copy;
|
||||
}
|
||||
|
||||
public ApiChangeKind getChangeKind() {
|
||||
@@ -73,4 +101,12 @@ public class EndpointChangeReport {
|
||||
public boolean hasParameterChanges() {
|
||||
return !parameterChanges.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isDtoFollowUp() {
|
||||
return dtoFollowUp;
|
||||
}
|
||||
|
||||
public String getRelatedDtoClassName() {
|
||||
return relatedDtoClassName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.codechecker.api.parser;
|
||||
|
||||
import com.codechecker.git.GitChangeScanner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -27,6 +29,24 @@ public class JavaSourceLocator {
|
||||
return Optional.of(Files.readString(path.get()));
|
||||
}
|
||||
|
||||
public Optional<String> readSourceAtCommit(GitChangeScanner gitScanner, String sha,
|
||||
String simpleClassName) throws IOException {
|
||||
Optional<String> relativePath = findRelativePath(simpleClassName);
|
||||
if (relativePath.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String source = gitScanner.readFileAtCommit(sha, relativePath.get());
|
||||
if (source == null || source.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(source);
|
||||
}
|
||||
|
||||
public Optional<String> findRelativePath(String simpleClassName) throws IOException {
|
||||
Optional<Path> path = findFile(simpleClassName);
|
||||
return path.map(p -> repoRoot.relativize(p).toString().replace('\\', '/'));
|
||||
}
|
||||
|
||||
public Optional<Path> findFile(String simpleClassName) throws IOException {
|
||||
String fileName = simpleClassName + ".java";
|
||||
for (String dir : searchDirs) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.codechecker.api.parser;
|
||||
|
||||
import com.codechecker.git.GitChangeScanner;
|
||||
import com.codechecker.model.FieldInfo;
|
||||
import com.codechecker.parser.ClassFieldParser;
|
||||
import com.codechecker.parser.TypeNameUtils;
|
||||
@@ -25,25 +26,40 @@ public class NestedDtoFieldParser {
|
||||
|
||||
private final ClassFieldParser classFieldParser = new ClassFieldParser();
|
||||
private final JavaSourceLocator sourceLocator;
|
||||
private final GitChangeScanner gitScanner;
|
||||
private final String oldSha;
|
||||
private final String newSha;
|
||||
|
||||
public NestedDtoFieldParser(Path repoRoot, List<String> searchDirs) {
|
||||
public NestedDtoFieldParser(Path repoRoot, List<String> searchDirs,
|
||||
GitChangeScanner gitScanner, String oldSha, String newSha) {
|
||||
this.sourceLocator = new JavaSourceLocator(repoRoot, searchDirs);
|
||||
this.gitScanner = gitScanner;
|
||||
this.oldSha = oldSha;
|
||||
this.newSha = newSha;
|
||||
}
|
||||
|
||||
public List<NestedFieldInfo> parseNestedFields(String dtoClassName) throws IOException {
|
||||
public List<NestedFieldInfo> parseNestedFieldsAtOldCommit(String dtoClassName) throws IOException {
|
||||
return parseNestedFields(dtoClassName, oldSha);
|
||||
}
|
||||
|
||||
public List<NestedFieldInfo> parseNestedFieldsAtNewCommit(String dtoClassName) throws IOException {
|
||||
return parseNestedFields(dtoClassName, newSha);
|
||||
}
|
||||
|
||||
private List<NestedFieldInfo> parseNestedFields(String dtoClassName, String sha) throws IOException {
|
||||
Set<String> visiting = new HashSet<>();
|
||||
List<NestedFieldInfo> result = new ArrayList<>();
|
||||
collectFields(dtoClassName, "", visiting, result);
|
||||
collectFields(dtoClassName, "", visiting, result, sha);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void collectFields(String className, String prefix, Set<String> visiting,
|
||||
List<NestedFieldInfo> out) throws IOException {
|
||||
List<NestedFieldInfo> out, String sha) throws IOException {
|
||||
if (className == null || className.isBlank() || visiting.contains(className)) {
|
||||
return;
|
||||
}
|
||||
visiting.add(className);
|
||||
Optional<String> source = sourceLocator.readSourceBySimpleName(className);
|
||||
Optional<String> source = readSource(className, sha);
|
||||
if (source.isEmpty()) {
|
||||
visiting.remove(className);
|
||||
return;
|
||||
@@ -56,12 +72,19 @@ public class NestedDtoFieldParser {
|
||||
out.add(new NestedFieldInfo(path, field.getType(), field.getDescription()));
|
||||
} else {
|
||||
out.add(new NestedFieldInfo(path, field.getType(), field.getDescription()));
|
||||
collectFields(simpleType, path, visiting, out);
|
||||
collectFields(simpleType, path, visiting, out, sha);
|
||||
}
|
||||
}
|
||||
visiting.remove(className);
|
||||
}
|
||||
|
||||
private Optional<String> readSource(String className, String sha) throws IOException {
|
||||
if (sha != null && gitScanner != null) {
|
||||
return sourceLocator.readSourceAtCommit(gitScanner, sha, className);
|
||||
}
|
||||
return sourceLocator.readSourceBySimpleName(className);
|
||||
}
|
||||
|
||||
private boolean isLeafType(String simpleType) {
|
||||
return LEAF_TYPES.contains(simpleType) || simpleType.endsWith("[]");
|
||||
}
|
||||
|
||||
@@ -25,10 +25,12 @@ public class AppConfig {
|
||||
private boolean wecomEnabled = true;
|
||||
private boolean onlyOnChange = true;
|
||||
|
||||
private boolean dtoApiFollowUpEnabled = true;
|
||||
private boolean apiCheckEnabled = true;
|
||||
private boolean apiExcludeFrameworkParams = true;
|
||||
private List<String> apiControllerScanDirs = new ArrayList<>();
|
||||
private List<String> apiFeignScanDirs = new ArrayList<>();
|
||||
private DtoOverlapMode dtoOverlapMode = DtoOverlapMode.BOTH;
|
||||
|
||||
/** 从 YAML 文件加载配置 */
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -49,6 +51,9 @@ public class AppConfig {
|
||||
Map<String, Object> classCheck = mapOrEmpty(root.get("class_check"));
|
||||
config.classCheckEnabled = boolOrDefault(classCheck.get("enabled"), true);
|
||||
|
||||
Map<String, Object> dtoApiFollowUp = mapOrEmpty(classCheck.get("dto_api_follow_up"));
|
||||
config.dtoApiFollowUpEnabled = boolOrDefault(dtoApiFollowUp.get("enabled"), true);
|
||||
|
||||
Map<String, Object> conversion = mapOrEmpty(classCheck.get("dto_entity_conversion"));
|
||||
config.dtoEntityConversionEnabled = boolOrDefault(conversion.get("enabled"), true);
|
||||
|
||||
@@ -64,6 +69,7 @@ public class AppConfig {
|
||||
|
||||
Map<String, Object> notify = mapOrEmpty(root.get("notify"));
|
||||
config.onlyOnChange = boolOrDefault(notify.get("only_on_change"), true);
|
||||
config.dtoOverlapMode = DtoOverlapMode.fromString(stringOrEmpty(notify.get("dto_overlap_mode")));
|
||||
|
||||
Map<String, Object> apiCheck = mapOrEmpty(root.get("api_check"));
|
||||
config.apiCheckEnabled = boolOrDefault(apiCheck.get("enabled"), true);
|
||||
@@ -169,11 +175,21 @@ public class AppConfig {
|
||||
return onlyOnChange;
|
||||
}
|
||||
|
||||
/** Dto 类变更后是否继续检测受影响接口的 API 参数变更 */
|
||||
public boolean isDtoApiFollowUpEnabled() {
|
||||
return dtoApiFollowUpEnabled;
|
||||
}
|
||||
|
||||
/** API 变更检测总开关 */
|
||||
public boolean isApiCheckEnabled() {
|
||||
return apiCheckEnabled;
|
||||
}
|
||||
|
||||
/** Dto 类变更与 API 参数变更重叠时的通知策略 */
|
||||
public DtoOverlapMode getDtoOverlapMode() {
|
||||
return dtoOverlapMode;
|
||||
}
|
||||
|
||||
/** 是否排除 Spring 框架注入参数 */
|
||||
public boolean isApiExcludeFrameworkParams() {
|
||||
return apiExcludeFrameworkParams;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.codechecker.config;
|
||||
|
||||
/**
|
||||
* Dto 类变更与 API 参数变更重叠时的通知策略。
|
||||
*/
|
||||
public enum DtoOverlapMode {
|
||||
/** 仅发类变更通知,抑制重叠的 API 参数通知 */
|
||||
CLASS_ONLY,
|
||||
/** 仅发 API 参数通知,抑制重叠的类变更通知 */
|
||||
API_ONLY,
|
||||
/** 两类通知均发送 */
|
||||
BOTH;
|
||||
|
||||
public static DtoOverlapMode fromString(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return BOTH;
|
||||
}
|
||||
try {
|
||||
return DtoOverlapMode.valueOf(value.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return BOTH;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.codechecker.notify;
|
||||
|
||||
import com.codechecker.api.model.ApiChangeKind;
|
||||
import com.codechecker.api.model.EndpointChangeReport;
|
||||
import com.codechecker.api.model.ParameterChange;
|
||||
import com.codechecker.config.DtoOverlapMode;
|
||||
import com.codechecker.model.ApiEndpoint;
|
||||
import com.codechecker.model.ClassChangeReport;
|
||||
import com.codechecker.model.ClassType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 按配置过滤 Dto 类变更与 API 参数变更的重叠通知。
|
||||
*/
|
||||
public class OverlapNotificationFilter {
|
||||
|
||||
public static final class FilterResult {
|
||||
private final List<ClassChangeReport> classReports;
|
||||
private final List<EndpointChangeReport> apiReports;
|
||||
|
||||
public FilterResult(List<ClassChangeReport> classReports, List<EndpointChangeReport> apiReports) {
|
||||
this.classReports = classReports;
|
||||
this.apiReports = apiReports;
|
||||
}
|
||||
|
||||
public List<ClassChangeReport> classReports() {
|
||||
return classReports;
|
||||
}
|
||||
|
||||
public List<EndpointChangeReport> apiReports() {
|
||||
return apiReports;
|
||||
}
|
||||
}
|
||||
|
||||
public static FilterResult apply(List<ClassChangeReport> classReports,
|
||||
List<EndpointChangeReport> apiReports,
|
||||
DtoOverlapMode mode) {
|
||||
if (mode == DtoOverlapMode.BOTH) {
|
||||
return new FilterResult(classReports, apiReports);
|
||||
}
|
||||
Set<OverlapKey> overlapKeys = buildOverlapKeys(classReports);
|
||||
if (overlapKeys.isEmpty()) {
|
||||
return new FilterResult(classReports, apiReports);
|
||||
}
|
||||
if (mode == DtoOverlapMode.CLASS_ONLY) {
|
||||
return new FilterResult(classReports, filterApiReports(apiReports, overlapKeys));
|
||||
}
|
||||
Set<OverlapKey> apiOverlapKeys = buildApiOverlapKeys(apiReports);
|
||||
return new FilterResult(filterClassReportsForApiOnly(classReports, apiOverlapKeys), apiReports);
|
||||
}
|
||||
|
||||
private static Set<OverlapKey> buildOverlapKeys(List<ClassChangeReport> classReports) {
|
||||
Set<OverlapKey> keys = new LinkedHashSet<>();
|
||||
for (ClassChangeReport report : classReports) {
|
||||
if (report.getClassType() != ClassType.DTO) {
|
||||
continue;
|
||||
}
|
||||
if (!hasDtoFieldChanges(report)) {
|
||||
continue;
|
||||
}
|
||||
Set<String> dtoNames = dtoNames(report);
|
||||
for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
|
||||
for (String dtoName : dtoNames) {
|
||||
keys.add(new OverlapKey(dtoName, endpoint.endpointKey()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static List<EndpointChangeReport> filterApiReports(List<EndpointChangeReport> apiReports,
|
||||
Set<OverlapKey> overlapKeys) {
|
||||
List<EndpointChangeReport> kept = new ArrayList<>();
|
||||
for (EndpointChangeReport report : apiReports) {
|
||||
if (!matchesOverlap(report, overlapKeys)) {
|
||||
kept.add(report);
|
||||
}
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
private static List<ClassChangeReport> filterClassReportsForApiOnly(List<ClassChangeReport> classReports,
|
||||
Set<OverlapKey> apiOverlapKeys) {
|
||||
List<ClassChangeReport> kept = new ArrayList<>();
|
||||
for (ClassChangeReport report : classReports) {
|
||||
if (!shouldSuppressClassForApiOnly(report, apiOverlapKeys)) {
|
||||
kept.add(report);
|
||||
}
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
private static Set<OverlapKey> buildApiOverlapKeys(List<EndpointChangeReport> apiReports) {
|
||||
Set<OverlapKey> keys = new LinkedHashSet<>();
|
||||
for (EndpointChangeReport report : apiReports) {
|
||||
if (report.getChangeKind() != ApiChangeKind.PARAM_CHANGED) {
|
||||
continue;
|
||||
}
|
||||
String endpointKey = report.getHttpMethod() + " " + report.getUri();
|
||||
for (ParameterChange change : report.getParameterChanges()) {
|
||||
if (!"body".equals(change.getSource())) {
|
||||
continue;
|
||||
}
|
||||
String parentDto = change.getParentDto();
|
||||
if (parentDto != null && !parentDto.isBlank()) {
|
||||
keys.add(new OverlapKey(parentDto, endpointKey));
|
||||
}
|
||||
}
|
||||
if (report.isDtoFollowUp()) {
|
||||
String relatedDto = report.getRelatedDtoClassName();
|
||||
if (relatedDto != null && !relatedDto.isBlank()) {
|
||||
keys.add(new OverlapKey(relatedDto, endpointKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static boolean shouldSuppressClassForApiOnly(ClassChangeReport report,
|
||||
Set<OverlapKey> apiOverlapKeys) {
|
||||
if (report.getClassType() != ClassType.DTO || !hasDtoFieldChanges(report)) {
|
||||
return false;
|
||||
}
|
||||
for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
|
||||
for (String dtoName : dtoNames(report)) {
|
||||
if (apiOverlapKeys.contains(new OverlapKey(dtoName, endpoint.endpointKey()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean matchesOverlap(EndpointChangeReport report, Set<OverlapKey> overlapKeys) {
|
||||
if (report.getChangeKind() != ApiChangeKind.PARAM_CHANGED) {
|
||||
return false;
|
||||
}
|
||||
String endpointKey = report.getHttpMethod() + " " + report.getUri();
|
||||
for (ParameterChange change : report.getParameterChanges()) {
|
||||
if (!"body".equals(change.getSource())) {
|
||||
continue;
|
||||
}
|
||||
String parentDto = change.getParentDto();
|
||||
if (parentDto == null || parentDto.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (overlapKeys.contains(new OverlapKey(parentDto, endpointKey))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (report.isDtoFollowUp()) {
|
||||
String relatedDto = report.getRelatedDtoClassName();
|
||||
if (relatedDto != null && overlapKeys.contains(new OverlapKey(relatedDto, endpointKey))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean hasDtoFieldChanges(ClassChangeReport report) {
|
||||
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 final String dtoClassName;
|
||||
private final String endpointKey;
|
||||
|
||||
private OverlapKey(String dtoClassName, String endpointKey) {
|
||||
this.dtoClassName = dtoClassName;
|
||||
this.endpointKey = endpointKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof OverlapKey)) {
|
||||
return false;
|
||||
}
|
||||
OverlapKey other = (OverlapKey) obj;
|
||||
return dtoClassName.equals(other.dtoClassName) && endpointKey.equals(other.endpointKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return dtoClassName.hashCode() * 31 + endpointKey.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user