From 518484015e46ebda9443a66b5bb6855680d11340 Mon Sep 17 00:00:00 2001 From: dongzi Date: Tue, 9 Jun 2026 17:51:58 +0800 Subject: [PATCH] Revert "first commit" This reverts commit ebf30399fb785bed87be5fb73574c8950a847a46. --- .gitignore | 23 - .idea/.gitignore | 10 - dependency-reduced-pom.xml | 51 --- pom.xml | 82 ---- .../java/com/codechecker/CodeCheckMain.java | 178 -------- .../analyzer/ClassChangeAnalyzer.java | 131 ------ .../codechecker/analyzer/DtoNestIndex.java | 135 ------ .../analyzer/EndpointIndexBuilder.java | 37 -- .../codechecker/analyzer/FieldDiffEngine.java | 166 -------- .../codechecker/analyzer/ImpactAnalyzer.java | 113 ----- .../analyzer/NestedObjectRoleResolver.java | 54 --- .../api/analyzer/ApiChangeAnalyzer.java | 74 ---- .../api/analyzer/DtoImpactedApiAnalyzer.java | 150 ------- .../api/analyzer/EndpointDiffEngine.java | 126 ------ .../api/analyzer/ParameterDiffEngine.java | 234 ----------- .../codechecker/api/model/ApiChangeKind.java | 12 - .../api/model/EndpointChangeReport.java | 112 ----- .../api/model/EndpointSnapshot.java | 72 ---- .../api/model/MethodParameterSnapshot.java | 71 ---- .../api/model/ParameterChange.java | 112 ----- .../api/notify/ApiChangeNotifier.java | 272 ------------ .../api/parser/EndpointSnapshotParser.java | 313 -------------- .../api/parser/JavaSourceLocator.java | 68 --- .../parser/MethodDescriptionExtractor.java | 74 ---- .../parser/MethodParamJavadocExtractor.java | 67 --- .../api/parser/NestedDtoFieldParser.java | 99 ----- .../api/parser/NestedFieldInfo.java | 28 -- .../api/scanner/ApiFileChangeScanner.java | 56 --- .../codechecker/common/MarkdownStyles.java | 62 --- .../common/WeComMarkdownSender.java | 75 ---- .../com/codechecker/config/AppConfig.java | 239 ----------- .../codechecker/config/DtoOverlapMode.java | 24 -- .../com/codechecker/git/GitChangeScanner.java | 295 ------------- .../com/codechecker/model/ApiEndpoint.java | 57 --- .../codechecker/model/ChangedClassFile.java | 67 --- .../codechecker/model/ClassChangeKind.java | 15 - .../codechecker/model/ClassChangeReport.java | 136 ------ .../java/com/codechecker/model/ClassType.java | 47 --- .../com/codechecker/model/FieldChange.java | 95 ----- .../java/com/codechecker/model/FieldInfo.java | 52 --- .../notify/OverlapNotificationFilter.java | 218 ---------- .../com/codechecker/notify/WeComNotifier.java | 394 ------------------ .../codechecker/parser/ClassDeclParser.java | 183 -------- .../codechecker/parser/ClassFieldParser.java | 135 ------ .../codechecker/parser/ConversionParser.java | 100 ----- .../codechecker/parser/EndpointParser.java | 301 ------------- .../com/codechecker/parser/TypeNameUtils.java | 117 ------ 47 files changed, 5532 deletions(-) delete mode 100644 .gitignore delete mode 100644 .idea/.gitignore delete mode 100644 dependency-reduced-pom.xml delete mode 100644 pom.xml delete mode 100644 src/main/java/com/codechecker/CodeCheckMain.java delete mode 100644 src/main/java/com/codechecker/analyzer/ClassChangeAnalyzer.java delete mode 100644 src/main/java/com/codechecker/analyzer/DtoNestIndex.java delete mode 100644 src/main/java/com/codechecker/analyzer/EndpointIndexBuilder.java delete mode 100644 src/main/java/com/codechecker/analyzer/FieldDiffEngine.java delete mode 100644 src/main/java/com/codechecker/analyzer/ImpactAnalyzer.java delete mode 100644 src/main/java/com/codechecker/analyzer/NestedObjectRoleResolver.java delete mode 100644 src/main/java/com/codechecker/api/analyzer/ApiChangeAnalyzer.java delete mode 100644 src/main/java/com/codechecker/api/analyzer/DtoImpactedApiAnalyzer.java delete mode 100644 src/main/java/com/codechecker/api/analyzer/EndpointDiffEngine.java delete mode 100644 src/main/java/com/codechecker/api/analyzer/ParameterDiffEngine.java delete mode 100644 src/main/java/com/codechecker/api/model/ApiChangeKind.java delete mode 100644 src/main/java/com/codechecker/api/model/EndpointChangeReport.java delete mode 100644 src/main/java/com/codechecker/api/model/EndpointSnapshot.java delete mode 100644 src/main/java/com/codechecker/api/model/MethodParameterSnapshot.java delete mode 100644 src/main/java/com/codechecker/api/model/ParameterChange.java delete mode 100644 src/main/java/com/codechecker/api/notify/ApiChangeNotifier.java delete mode 100644 src/main/java/com/codechecker/api/parser/EndpointSnapshotParser.java delete mode 100644 src/main/java/com/codechecker/api/parser/JavaSourceLocator.java delete mode 100644 src/main/java/com/codechecker/api/parser/MethodDescriptionExtractor.java delete mode 100644 src/main/java/com/codechecker/api/parser/MethodParamJavadocExtractor.java delete mode 100644 src/main/java/com/codechecker/api/parser/NestedDtoFieldParser.java delete mode 100644 src/main/java/com/codechecker/api/parser/NestedFieldInfo.java delete mode 100644 src/main/java/com/codechecker/api/scanner/ApiFileChangeScanner.java delete mode 100644 src/main/java/com/codechecker/common/MarkdownStyles.java delete mode 100644 src/main/java/com/codechecker/common/WeComMarkdownSender.java delete mode 100644 src/main/java/com/codechecker/config/AppConfig.java delete mode 100644 src/main/java/com/codechecker/config/DtoOverlapMode.java delete mode 100644 src/main/java/com/codechecker/git/GitChangeScanner.java delete mode 100644 src/main/java/com/codechecker/model/ApiEndpoint.java delete mode 100644 src/main/java/com/codechecker/model/ChangedClassFile.java delete mode 100644 src/main/java/com/codechecker/model/ClassChangeKind.java delete mode 100644 src/main/java/com/codechecker/model/ClassChangeReport.java delete mode 100644 src/main/java/com/codechecker/model/ClassType.java delete mode 100644 src/main/java/com/codechecker/model/FieldChange.java delete mode 100644 src/main/java/com/codechecker/model/FieldInfo.java delete mode 100644 src/main/java/com/codechecker/notify/OverlapNotificationFilter.java delete mode 100644 src/main/java/com/codechecker/notify/WeComNotifier.java delete mode 100644 src/main/java/com/codechecker/parser/ClassDeclParser.java delete mode 100644 src/main/java/com/codechecker/parser/ClassFieldParser.java delete mode 100644 src/main/java/com/codechecker/parser/ConversionParser.java delete mode 100644 src/main/java/com/codechecker/parser/EndpointParser.java delete mode 100644 src/main/java/com/codechecker/parser/TypeNameUtils.java diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c299960..0000000 --- a/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# ---> Java -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -!.gitea/workflows/code-checker.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 30cf57e..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml deleted file mode 100644 index bfdcb07..0000000 --- a/dependency-reduced-pom.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - 4.0.0 - com.codechecker - code-checker - 1.0.0 - - code-checker - - - maven-compiler-plugin - 3.13.0 - - - maven-shade-plugin - 3.6.0 - - - package - - shade - - - - - com.codechecker.CodeCheckMain - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - - - - - 11 - 3.25.10 - 11 - UTF-8 - - diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 6912686..0000000 --- a/pom.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - 4.0.0 - - com.codechecker - code-checker - 1.0.0 - jar - - - 11 - 11 - UTF-8 - 3.25.10 - - - - - com.github.javaparser - javaparser-symbol-solver-core - ${javaparser.version} - - - org.yaml - snakeyaml - 2.2 - - - com.squareup.okhttp3 - okhttp - 4.12.0 - - - info.picocli - picocli - 4.7.6 - - - - - code-checker - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - package - - shade - - - - - com.codechecker.CodeCheckMain - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - - - - diff --git a/src/main/java/com/codechecker/CodeCheckMain.java b/src/main/java/com/codechecker/CodeCheckMain.java deleted file mode 100644 index f26ae68..0000000 --- a/src/main/java/com/codechecker/CodeCheckMain.java +++ /dev/null @@ -1,178 +0,0 @@ -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; -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; - -/** - * CLI 入口:加载配置 → 扫描 git 变更 → 分析影响 → 输出/发送企微通知。 - */ -@Command(name = "code-checker", mixinStandardHelpOptions = true, - description = "检测类变更与 API 变更并发送企业微信通知") -public class CodeCheckMain implements Callable { - @Option(names = "--config", required = true, description = "配置文件路径") - private Path config; - - @Option(names = "--repo-root", required = true, description = "仓库根目录") - private Path repoRoot; - - @Option(names = "--old-sha", required = true, description = "旧提交 SHA") - private String oldSha; - - @Option(names = "--new-sha", required = true, description = "新提交 SHA") - private String newSha; - - @Option(names = "--modifier", required = true, description = "修改人") - private String modifier; - - @Option(names = "--modify-time", required = true, description = "修改时间") - private String modifyTime; - - /** 程序入口 */ - public static void main(String[] args) { - int exitCode = new CommandLine(new CodeCheckMain()).execute(args); - System.exit(exitCode); - } - - /** 主流程:类变更与 API 变更检测,支持 Dto 跟进与重叠通知策略 */ - @Override - public Integer call() throws Exception { - AppConfig appConfig = AppConfig.load(config.toAbsolutePath()); - if (!appConfig.isMasterEnabled()) { - System.out.println("变更检测已全部关闭(checker.enabled=false)"); - return 0; - } - - 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, nestIndex); - } else { - System.out.println("类变更检测已关闭(class_check.enabled=false)"); - } - - if (appConfig.isApiCheckEnabled()) { - apiReports = analyzeApiChanges(appConfig, gitScanner, classReports, nestIndex); - } else { - System.out.println("API 变更检测已关闭(api_check.enabled=false)"); - } - - OverlapNotificationFilter.FilterResult filtered = OverlapNotificationFilter.apply( - classReports, apiReports, appConfig.getDtoOverlapMode(), nestIndex); - int totalSent = sendClassNotifications(appConfig, filtered.classReports()) - + sendApiNotifications(appConfig, filtered.apiReports()); - - if (totalSent == 0 && appConfig.isOnlyOnChange()) { - System.out.println("无变更,静默退出"); - } - return 0; - } - - 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); - System.out.println("已索引接口数量: " + endpointIndex.size()); - - ClassChangeAnalyzer analyzer = new ClassChangeAnalyzer(gitScanner); - List reports = analyzer.analyze( - repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, endpointIndex, nestIndex); - System.out.println("检测到需通知的类变更数量: " + reports.size()); - return reports; - } - - private List analyzeApiChanges(AppConfig appConfig, GitChangeScanner gitScanner, - List classReports, - DtoNestIndex nestIndex) throws Exception { - System.out.println("=== API 变更检测 ==="); - ApiFileChangeScanner fileScanner = new ApiFileChangeScanner(gitScanner); - Set changedApiFiles = new LinkedHashSet<>(fileScanner.scanChangedFiles( - repoRoot.toAbsolutePath(), appConfig.getAllApiScanDirs(), oldSha, newSha)); - - ApiChangeAnalyzer analyzer = new ApiChangeAnalyzer(gitScanner); - List 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 followUpReports = dtoAnalyzer.analyze( - repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, classReports, changedApiFiles, nestIndex); - 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 dedupeApiReports(List reports) { - Map 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 reports) { - if (reports.isEmpty()) { - return 0; - } - WeComNotifier notifier = new WeComNotifier(); - if (appConfig.isWecomEnabled()) { - return notifier.sendAll(appConfig.getWecomWebhookUrl(), reports, modifier, modifyTime); - } - notifier.logAll(reports, modifier, modifyTime); - return reports.size(); - } - - private int sendApiNotifications(AppConfig appConfig, List reports) { - if (reports.isEmpty()) { - return 0; - } - ApiChangeNotifier notifier = new ApiChangeNotifier(); - return notifier.sendAll(appConfig.getWecomWebhookUrl(), reports, modifier, modifyTime, - appConfig.isWecomEnabled()); - } -} diff --git a/src/main/java/com/codechecker/analyzer/ClassChangeAnalyzer.java b/src/main/java/com/codechecker/analyzer/ClassChangeAnalyzer.java deleted file mode 100644 index 97cd605..0000000 --- a/src/main/java/com/codechecker/analyzer/ClassChangeAnalyzer.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.codechecker.analyzer; - -import com.codechecker.config.AppConfig; -import com.codechecker.git.GitChangeScanner; -import com.codechecker.model.ChangedClassFile; -import com.codechecker.model.ClassChangeKind; -import com.codechecker.model.ClassChangeReport; -import com.codechecker.model.FieldChange; -import com.codechecker.model.FieldInfo; -import com.codechecker.parser.ClassDeclParser; -import com.codechecker.parser.ClassFieldParser; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * 编排 git 扫描、字段 diff、影响分析,生成待通知的 ClassChangeReport 列表。 - */ -public class ClassChangeAnalyzer { - private final GitChangeScanner gitScanner; - private final ClassFieldParser classFieldParser = new ClassFieldParser(); - private final FieldDiffEngine fieldDiffEngine = new FieldDiffEngine(); - private final ImpactAnalyzer impactAnalyzer = new ImpactAnalyzer(); - private final ClassDeclParser classDeclParser = new ClassDeclParser(); - - public ClassChangeAnalyzer(GitChangeScanner gitScanner) { - this.gitScanner = gitScanner; - } - - /** 扫描变更文件并逐条分析,无实质变更的 MODIFIED 会被跳过 */ - public List analyze(Path repoRoot, AppConfig config, String oldSha, String newSha, - 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, nestIndex)); - continue; - } - ClassChangeReport report = analyzeModifiedOrRenamed(changedFile, config, repoRoot, oldSha, newSha, - endpointIndex, nestIndex); - if (report != null) { - reports.add(report); - } - } - return reports; - } - - /** 处理删除:标记 DELETED 并分析影响(基于旧源码) */ - private ClassChangeReport analyzeDeleted(ChangedClassFile changedFile, AppConfig config, Path repoRoot, - String oldSha, Map endpointIndex, - DtoNestIndex nestIndex) - throws IOException { - String path = changedFile.getRelativePath(); - String oldSource = gitScanner.readFileAtCommit(oldSha, path); - - String classDescription = classDeclParser.extractClassDescription( - oldSource, changedFile.getClassName()); - - ClassChangeReport report = new ClassChangeReport( - changedFile.getClassName(), - null, - changedFile.getClassType(), - ClassChangeKind.DELETED, - path, - config.isDtoEntityConversionEnabled(), - classDescription - ); - 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, - DtoNestIndex nestIndex) - throws IOException { - String oldPath = changedFile.pathForOldCommit(); - String newPath = changedFile.getRelativePath(); - - String oldSource = gitScanner.readFileAtCommit(oldSha, oldPath); - String newSource = gitScanner.readFileAtCommit(newSha, newPath); - if (newSource == null || newSource.isBlank()) { - newSource = gitScanner.readFileAtHead(newPath); - } - - String oldFallback = ClassDeclParser.classNameFromPath(oldPath); - String newFallback = ClassDeclParser.classNameFromPath(newPath); - String oldClassName = changedFile.getOldClassName() != null - ? changedFile.getOldClassName() - : classDeclParser.resolveClassName(oldSource, oldFallback); - String newClassName = classDeclParser.resolveClassName(newSource, newFallback); - - List oldFields = classFieldParser.parseFields(oldSource, oldClassName); - List newFields = classFieldParser.parseFields(newSource, newClassName); - List fieldChanges = fieldDiffEngine.diff(oldFields, newFields); - - boolean renamed = !oldClassName.equals(newClassName); - ClassChangeKind changeKind; - if (renamed && fieldChanges.isEmpty()) { - changeKind = ClassChangeKind.RENAME_ONLY; - } else if (renamed) { - changeKind = ClassChangeKind.RENAME_AND_FIELDS; - } else if (!fieldChanges.isEmpty()) { - changeKind = ClassChangeKind.FIELDS_ONLY; - } else { - return null; - } - - String classDescription = classDeclParser.extractClassDescription(newSource, newClassName); - - ClassChangeReport report = new ClassChangeReport( - newClassName, - renamed ? oldClassName : null, - changedFile.getClassType(), - changeKind, - newPath, - config.isDtoEntityConversionEnabled(), - classDescription - ); - fieldChanges.forEach(report::addFieldChange); - impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, newSource, oldSource, nestIndex); - return report; - } -} diff --git a/src/main/java/com/codechecker/analyzer/DtoNestIndex.java b/src/main/java/com/codechecker/analyzer/DtoNestIndex.java deleted file mode 100644 index 65e027a..0000000 --- a/src/main/java/com/codechecker/analyzer/DtoNestIndex.java +++ /dev/null @@ -1,135 +0,0 @@ -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; - } - - /** 是否被其他 Dto/Vo 嵌套引用(存在至少一个祖先容器) */ - public boolean hasAncestors(String className) { - Set ancestors = ancestorsOf.get(className); - return ancestors != null && !ancestors.isEmpty(); - } - - /** 嵌套类型的 @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/src/main/java/com/codechecker/analyzer/EndpointIndexBuilder.java b/src/main/java/com/codechecker/analyzer/EndpointIndexBuilder.java deleted file mode 100644 index 5dadfb4..0000000 --- a/src/main/java/com/codechecker/analyzer/EndpointIndexBuilder.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.codechecker.analyzer; - -import com.codechecker.config.AppConfig; -import com.codechecker.model.ApiEndpoint; -import com.codechecker.parser.EndpointParser; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * 预扫描 Controller/Feign 目录,构建 endpointKey → ApiEndpoint 索引。 - */ -public class EndpointIndexBuilder { - private final EndpointParser endpointParser = new EndpointParser(); - - /** 合并 Controller 与 Feign 扫描结果 */ - public Map buildIndex(Path repoRoot, AppConfig config) throws IOException { - Map index = new LinkedHashMap<>(); - for (String dir : config.getControllerScanDirs()) { - addEndpoints(index, endpointParser.scanControllerDirectory(repoRoot.resolve(dir), dir)); - } - for (String dir : config.getFeignScanDirs()) { - addEndpoints(index, endpointParser.scanFeignDirectory(repoRoot.resolve(dir), dir)); - } - return index; - } - - /** 按 endpointKey 去重写入索引 */ - private void addEndpoints(Map index, List endpoints) { - for (ApiEndpoint endpoint : endpoints) { - index.putIfAbsent(endpoint.endpointKey(), endpoint); - } - } -} diff --git a/src/main/java/com/codechecker/analyzer/FieldDiffEngine.java b/src/main/java/com/codechecker/analyzer/FieldDiffEngine.java deleted file mode 100644 index 2549c1b..0000000 --- a/src/main/java/com/codechecker/analyzer/FieldDiffEngine.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.codechecker.analyzer; - -import com.codechecker.model.FieldChange; -import com.codechecker.model.FieldInfo; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * 对比新旧字段列表,产出新增/删除/类型修改/重命名(纯注释变更忽略)。 - */ -public class FieldDiffEngine { - - /** - * 按字段名对比;删除+新增且说明匹配时合并为重命名。 - * 输出顺序:按新字段声明顺序,未配对的删除字段置于末尾。 - */ - public List diff(List oldFields, List newFields) { - Map oldMap = toMap(oldFields); - Map newMap = toMap(newFields); - - List modified = new ArrayList<>(); - List added = new ArrayList<>(); - List removed = new ArrayList<>(); - - for (FieldInfo newField : newFields) { - FieldInfo oldField = oldMap.get(newField.getName()); - if (oldField == null) { - added.add(newField); - } else if (!oldField.getType().equals(newField.getType())) { - modified.add(FieldChange.modified(oldField, newField, buildTypeDetail(oldField, newField))); - } - // 仅 @Schema / 注释文案变化:不纳入字段变更 - } - - for (FieldInfo oldField : oldFields) { - if (!newMap.containsKey(oldField.getName())) { - removed.add(oldField); - } - } - - List renamed = pairRenames(removed, added); - return mergeInOrder(newFields, renamed, modified, added, removed); - } - - /** - * 将删除+新增配对为字段重命名。 - * 优先:说明相同且类型相同;其次:说明相同但类型不同(重命名+改类型)。 - */ - private List pairRenames(List removed, List added) { - List renames = new ArrayList<>(); - Set matchedRemoved = new LinkedHashSet<>(); - Set matchedAdded = new LinkedHashSet<>(); - - for (FieldInfo oldField : removed) { - FieldInfo pair = findRenamePair(oldField, added, matchedAdded, true); - if (pair == null) { - pair = findRenamePair(oldField, added, matchedAdded, false); - } - if (pair != null) { - renames.add(FieldChange.renamed(oldField, pair)); - matchedRemoved.add(oldField); - matchedAdded.add(pair); - } - } - - removed.removeIf(matchedRemoved::contains); - added.removeIf(matchedAdded::contains); - return renames; - } - - private FieldInfo findRenamePair(FieldInfo removed, List added, - Set excluded, boolean requireSameType) { - for (FieldInfo candidate : added) { - if (excluded.contains(candidate)) { - continue; - } - if (!descriptionsMatch(removed, candidate)) { - continue; - } - if (requireSameType && !removed.getType().equals(candidate.getType())) { - continue; - } - if (!requireSameType && removed.getType().equals(candidate.getType())) { - continue; - } - return candidate; - } - return null; - } - - /** 说明相同(非空)或双方均为空时视为匹配 */ - private boolean descriptionsMatch(FieldInfo oldField, FieldInfo newField) { - String oldDesc = normalizeDescription(oldField.getDescription()); - String newDesc = normalizeDescription(newField.getDescription()); - if (oldDesc.isEmpty() && newDesc.isEmpty()) { - return true; - } - if (oldDesc.isEmpty() || newDesc.isEmpty()) { - return false; - } - return oldDesc.equals(newDesc); - } - - private String normalizeDescription(String description) { - return description == null ? "" : description.trim(); - } - - /** 按新字段声明顺序合并各变更类型 */ - private List mergeInOrder(List newFields, List renamed, - List modified, List added, - List removed) { - Map renamedByNewName = new LinkedHashMap<>(); - for (FieldChange change : renamed) { - renamedByNewName.put(change.getFieldName(), change); - } - - Map modifiedByName = new LinkedHashMap<>(); - for (FieldChange change : modified) { - modifiedByName.put(change.getFieldName(), change); - } - - Set emitted = new LinkedHashSet<>(); - List result = new ArrayList<>(); - - for (FieldInfo newField : newFields) { - String name = newField.getName(); - if (renamedByNewName.containsKey(name)) { - result.add(renamedByNewName.get(name)); - emitted.add(name); - } else if (modifiedByName.containsKey(name)) { - result.add(modifiedByName.get(name)); - emitted.add(name); - } else if (added.stream().anyMatch(f -> f.getName().equals(name))) { - result.add(FieldChange.added(newField)); - emitted.add(name); - } - } - - for (FieldInfo oldField : removed) { - result.add(FieldChange.removed(oldField)); - } - return result; - } - - /** 字段列表转 LinkedHashMap,保持声明顺序 */ - private Map toMap(List fields) { - Map map = new LinkedHashMap<>(); - for (FieldInfo field : fields) { - map.put(field.getName(), field); - } - return map; - } - - /** 构造类型变化描述,如 Integer → String */ - private String buildTypeDetail(FieldInfo oldField, FieldInfo newField) { - if (oldField.getType().equals(newField.getType())) { - return ""; - } - return oldField.getType() + " → " + newField.getType(); - } -} diff --git a/src/main/java/com/codechecker/analyzer/ImpactAnalyzer.java b/src/main/java/com/codechecker/analyzer/ImpactAnalyzer.java deleted file mode 100644 index f2db5e4..0000000 --- a/src/main/java/com/codechecker/analyzer/ImpactAnalyzer.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.codechecker.analyzer; - -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; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * 根据变更报告匹配受影响的 HTTP 接口与 Dto→Entity 转换目标。 - */ -public class ImpactAnalyzer { - private final ConversionParser conversionParser = new ConversionParser(); - - /** - * 填充 report 的影响列表;新旧类名均参与匹配;Entity/Model 不匹配接口。 - */ - public void analyze(ClassChangeReport report, Map endpointIndex, - 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); - } - - report.setObjectRoleLabels(NestedObjectRoleResolver.resolve(report, nestIndex, endpointIndex)); - - if (!config.isDtoEntityConversionEnabled()) { - return; - } - - analyzeConversion(report, config, repoRoot, newSource, oldSource, matchNames); - } - - /** 收集新旧类名及嵌套祖先 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; - } - - /** 在接口索引中匹配入参/返回类型 */ - private void matchEndpoints(ClassChangeReport report, Map endpointIndex, - Set matchNames) { - List inputImpacts = new ArrayList<>(); - List frontendImpacts = new ArrayList<>(); - - for (ApiEndpoint endpoint : endpointIndex.values()) { - if (matchesAnyType(endpoint.getParamTypes(), matchNames)) { - inputImpacts.add(endpoint); - } - if (matchesAnyType(endpoint.getReturnTypes(), matchNames)) { - frontendImpacts.add(endpoint); - } - } - - inputImpacts.forEach(report::addInputImpact); - frontendImpacts.forEach(report::addFrontendImpact); - } - - /** 扫描 convert 方法与 BeanUtils.copyProperties 关联的 Entity */ - private void analyzeConversion(ClassChangeReport report, AppConfig config, Path repoRoot, - String newSource, String oldSource, Set matchNames) throws IOException { - for (String name : matchNames) { - if (newSource != null && !newSource.isBlank()) { - conversionParser.findConvertTargetsInClass(newSource, name) - .forEach(report::addConversionEntity); - } - if (oldSource != null && !oldSource.isBlank() && !oldSource.equals(newSource)) { - conversionParser.findConvertTargetsInClass(oldSource, name) - .forEach(report::addConversionEntity); - } - for (String scanDir : config.getConversionScanDirs()) { - conversionParser.findBeanUtilsTargets(repoRoot.resolve(scanDir), name) - .forEach(report::addConversionEntity); - } - } - } - - /** 类型集合中是否包含任一目标类名 */ - private boolean matchesAnyType(Collection types, Set classNames) { - if (types == null) { - return false; - } - for (String type : types) { - if (classNames.contains(type)) { - return true; - } - } - return false; - } -} diff --git a/src/main/java/com/codechecker/analyzer/NestedObjectRoleResolver.java b/src/main/java/com/codechecker/analyzer/NestedObjectRoleResolver.java deleted file mode 100644 index 0944862..0000000 --- a/src/main/java/com/codechecker/analyzer/NestedObjectRoleResolver.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.codechecker.analyzer; - -import com.codechecker.model.ApiEndpoint; -import com.codechecker.model.ClassChangeReport; -import com.codechecker.model.ClassType; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * 判定 Dto/Vo 在类变更通知中的对象角色标签(方案 B:嵌套 + 可选顶层)。 - *

- * 仅当存在嵌套祖先时标注;纯顶层不标注;既嵌套又直接作接口根类型时同时标注。 - */ -public final class NestedObjectRoleResolver { - - private NestedObjectRoleResolver() { - } - - public static List resolve(ClassChangeReport report, DtoNestIndex nestIndex, - Map endpointIndex) { - if (report.getClassType() != ClassType.DTO && report.getClassType() != ClassType.VO) { - return List.of(); - } - if (nestIndex == null) { - return List.of(); - } - String className = report.getClassName(); - if (!nestIndex.hasAncestors(className)) { - return List.of(); - } - List labels = new ArrayList<>(); - labels.add("嵌套对象"); - if (isDirectEndpointType(className, endpointIndex)) { - labels.add("顶层对象"); - } - return List.copyOf(labels); - } - - /** 是否直接出现在接口入参或返回值类型(非仅经祖先传播) */ - private static boolean isDirectEndpointType(String className, Map endpointIndex) { - if (className == null || className.isBlank() || endpointIndex == null) { - return false; - } - for (ApiEndpoint endpoint : endpointIndex.values()) { - if (endpoint.getParamTypes().contains(className) - || endpoint.getReturnTypes().contains(className)) { - return true; - } - } - return false; - } -} diff --git a/src/main/java/com/codechecker/api/analyzer/ApiChangeAnalyzer.java b/src/main/java/com/codechecker/api/analyzer/ApiChangeAnalyzer.java deleted file mode 100644 index 4515d0d..0000000 --- a/src/main/java/com/codechecker/api/analyzer/ApiChangeAnalyzer.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.codechecker.api.analyzer; - -import com.codechecker.api.model.EndpointChangeReport; -import com.codechecker.api.model.EndpointSnapshot; -import com.codechecker.api.parser.EndpointSnapshotParser; -import com.codechecker.api.scanner.ApiFileChangeScanner; -import com.codechecker.config.AppConfig; -import com.codechecker.git.GitChangeScanner; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -/** - * API 变更分析编排(与 {@link com.codechecker.analyzer.ClassChangeAnalyzer} 平行、互不调用)。 - */ -public class ApiChangeAnalyzer { - private final GitChangeScanner gitScanner; - private final ApiFileChangeScanner fileScanner; - - public ApiChangeAnalyzer(GitChangeScanner gitScanner) { - this.gitScanner = gitScanner; - this.fileScanner = new ApiFileChangeScanner(gitScanner); - } - - public List analyze(Path repoRoot, AppConfig config, - String oldSha, String newSha) throws IOException { - List changedFiles = fileScanner.scanChangedFiles( - repoRoot, config.getAllApiScanDirs(), oldSha, newSha); - if (changedFiles.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 : changedFiles) { - 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)); - } - - return endpointDiffEngine.diff(oldSnapshots, newSnapshots); - } - - 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; - } -} diff --git a/src/main/java/com/codechecker/api/analyzer/DtoImpactedApiAnalyzer.java b/src/main/java/com/codechecker/api/analyzer/DtoImpactedApiAnalyzer.java deleted file mode 100644 index 3b0736d..0000000 --- a/src/main/java/com/codechecker/api/analyzer/DtoImpactedApiAnalyzer.java +++ /dev/null @@ -1,150 +0,0 @@ -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; - } -} diff --git a/src/main/java/com/codechecker/api/analyzer/EndpointDiffEngine.java b/src/main/java/com/codechecker/api/analyzer/EndpointDiffEngine.java deleted file mode 100644 index 84fd5f5..0000000 --- a/src/main/java/com/codechecker/api/analyzer/EndpointDiffEngine.java +++ /dev/null @@ -1,126 +0,0 @@ -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 java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * 接口快照对比:路径 / 方法 / 增删 / 参数(拆分报告,互不混合类型)。 - */ -public class EndpointDiffEngine { - private final ParameterDiffEngine parameterDiffEngine; - - public EndpointDiffEngine(ParameterDiffEngine parameterDiffEngine) { - this.parameterDiffEngine = parameterDiffEngine; - } - - public List diff(List oldSnapshots, - List newSnapshots) throws IOException { - Map oldMap = indexByFingerprint(oldSnapshots); - Map newMap = indexByFingerprint(newSnapshots); - List reports = new ArrayList<>(); - - for (String fp : newMap.keySet()) { - if (!oldMap.containsKey(fp)) { - EndpointSnapshot snap = newMap.get(fp); - reports.add(new EndpointChangeReport( - ApiChangeKind.NEW_ENDPOINT, - snap.getHttpMethod(), null, - snap.getUri(), null, - snap.getSourceFile(), snap.getControllerClass(), - snap.getMethodDescription())); - } - } - for (String fp : oldMap.keySet()) { - if (!newMap.containsKey(fp)) { - EndpointSnapshot snap = oldMap.get(fp); - reports.add(new EndpointChangeReport( - ApiChangeKind.REMOVED_ENDPOINT, - snap.getHttpMethod(), null, - snap.getUri(), null, - snap.getSourceFile(), snap.getControllerClass(), - snap.getMethodDescription())); - } - } - for (String fp : oldMap.keySet()) { - if (!newMap.containsKey(fp)) { - continue; - } - EndpointSnapshot oldSnap = oldMap.get(fp); - EndpointSnapshot newSnap = newMap.get(fp); - reports.addAll(diffMatched(oldSnap, newSnap)); - } - return reports; - } - - private List diffMatched(EndpointSnapshot oldSnap, - EndpointSnapshot newSnap) throws IOException { - List reports = new ArrayList<>(); - boolean pathChanged = !oldSnap.getUri().equals(newSnap.getUri()); - boolean methodChanged = !oldSnap.getHttpMethod().equalsIgnoreCase(newSnap.getHttpMethod()); - - if (pathChanged) { - reports.add(new EndpointChangeReport( - ApiChangeKind.PATH_CHANGED, - newSnap.getHttpMethod(), null, - newSnap.getUri(), oldSnap.getUri(), - newSnap.getSourceFile(), newSnap.getControllerClass(), - preferDescription(newSnap, oldSnap))); - } - if (methodChanged) { - reports.add(new EndpointChangeReport( - ApiChangeKind.METHOD_CHANGED, - newSnap.getHttpMethod(), oldSnap.getHttpMethod(), - newSnap.getUri(), null, - newSnap.getSourceFile(), newSnap.getControllerClass(), - preferDescription(newSnap, oldSnap))); - } - - List paramChanges = parameterDiffEngine.diff(oldSnap, newSnap); - if (!paramChanges.isEmpty()) { - if (pathChanged || methodChanged) { - EndpointChangeReport paramReport = new EndpointChangeReport( - ApiChangeKind.PARAM_CHANGED, - newSnap.getHttpMethod(), methodChanged ? oldSnap.getHttpMethod() : null, - newSnap.getUri(), pathChanged ? oldSnap.getUri() : null, - newSnap.getSourceFile(), newSnap.getControllerClass(), - preferDescription(newSnap, oldSnap)); - paramChanges.forEach(paramReport::addParameterChange); - reports.add(paramReport); - } else { - EndpointChangeReport paramReport = new EndpointChangeReport( - ApiChangeKind.PARAM_CHANGED, - newSnap.getHttpMethod(), null, - newSnap.getUri(), null, - newSnap.getSourceFile(), newSnap.getControllerClass(), - preferDescription(newSnap, oldSnap)); - paramChanges.forEach(paramReport::addParameterChange); - reports.add(paramReport); - } - } - return reports; - } - - private String preferDescription(EndpointSnapshot primary, EndpointSnapshot fallback) { - if (primary != null && primary.getMethodDescription() != null - && !primary.getMethodDescription().isBlank()) { - return primary.getMethodDescription(); - } - return fallback == null ? "" : fallback.getMethodDescription(); - } - - private Map indexByFingerprint(List snapshots) { - Map map = new LinkedHashMap<>(); - for (EndpointSnapshot snap : snapshots) { - map.putIfAbsent(snap.getFingerprint(), snap); - } - return map; - } -} diff --git a/src/main/java/com/codechecker/api/analyzer/ParameterDiffEngine.java b/src/main/java/com/codechecker/api/analyzer/ParameterDiffEngine.java deleted file mode 100644 index 4a241c5..0000000 --- a/src/main/java/com/codechecker/api/analyzer/ParameterDiffEngine.java +++ /dev/null @@ -1,234 +0,0 @@ -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; - } -} diff --git a/src/main/java/com/codechecker/api/model/ApiChangeKind.java b/src/main/java/com/codechecker/api/model/ApiChangeKind.java deleted file mode 100644 index e731ffc..0000000 --- a/src/main/java/com/codechecker/api/model/ApiChangeKind.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.codechecker.api.model; - -/** - * API 变更类型(与类变更 {@link com.codechecker.model.ClassChangeKind} 独立)。 - */ -public enum ApiChangeKind { - NEW_ENDPOINT, - REMOVED_ENDPOINT, - PATH_CHANGED, - METHOD_CHANGED, - PARAM_CHANGED -} diff --git a/src/main/java/com/codechecker/api/model/EndpointChangeReport.java b/src/main/java/com/codechecker/api/model/EndpointChangeReport.java deleted file mode 100644 index 3765c28..0000000 --- a/src/main/java/com/codechecker/api/model/EndpointChangeReport.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.codechecker.api.model; - -import java.util.ArrayList; -import java.util.List; - -/** - * 单条 API 变更报告(路径 / 方法 / 参数各自独立,不与其他类型混合)。 - */ -public class EndpointChangeReport { - private final ApiChangeKind changeKind; - private final String httpMethod; - private final String oldHttpMethod; - private final String uri; - private final String oldUri; - private final String sourceFile; - private final String controllerClass; - private final String endpointDescription; - private final boolean dtoFollowUp; - private final String relatedDtoClassName; - private final List 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; - this.uri = uri; - this.oldUri = oldUri; - 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() { - return changeKind; - } - - public String getHttpMethod() { - return httpMethod; - } - - public String getOldHttpMethod() { - return oldHttpMethod; - } - - public String getUri() { - return uri; - } - - public String getOldUri() { - return oldUri; - } - - public String getSourceFile() { - return sourceFile; - } - - public String getControllerClass() { - return controllerClass; - } - - public String getEndpointDescription() { - return endpointDescription; - } - - public List getParameterChanges() { - return parameterChanges; - } - - public void addParameterChange(ParameterChange change) { - parameterChanges.add(change); - } - - public boolean hasParameterChanges() { - return !parameterChanges.isEmpty(); - } - - public boolean isDtoFollowUp() { - return dtoFollowUp; - } - - public String getRelatedDtoClassName() { - return relatedDtoClassName; - } -} diff --git a/src/main/java/com/codechecker/api/model/EndpointSnapshot.java b/src/main/java/com/codechecker/api/model/EndpointSnapshot.java deleted file mode 100644 index 4087283..0000000 --- a/src/main/java/com/codechecker/api/model/EndpointSnapshot.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.codechecker.api.model; - -import java.util.ArrayList; -import java.util.List; - -/** - * 单个 HTTP/Feign 接口快照。 - */ -public class EndpointSnapshot { - private final String fingerprint; - private final String httpMethod; - private final String uri; - private final String sourceFile; - private final String controllerClass; - private final String methodName; - private final String methodDescription; - private final List parameters; - - public EndpointSnapshot(String fingerprint, String httpMethod, String uri, String sourceFile, - String controllerClass, String methodName, String methodDescription, - List parameters) { - this.fingerprint = fingerprint; - this.httpMethod = httpMethod; - this.uri = uri; - this.sourceFile = sourceFile; - this.controllerClass = controllerClass; - this.methodName = methodName; - this.methodDescription = methodDescription == null ? "" : methodDescription; - this.parameters = parameters == null ? List.of() : new ArrayList<>(parameters); - } - - /** 跨 commit 配对同一 Java 方法;不含参数信息,参数 diff 由 ParameterDiffEngine 负责 */ - public static String buildFingerprint(String sourceFile, String methodName) { - return sourceFile + "#" + methodName; - } - - public String getFingerprint() { - return fingerprint; - } - - public String getHttpMethod() { - return httpMethod; - } - - public String getUri() { - return uri; - } - - public String getSourceFile() { - return sourceFile; - } - - public String getControllerClass() { - return controllerClass; - } - - public String getMethodName() { - return methodName; - } - - public String getMethodDescription() { - return methodDescription; - } - - public List getParameters() { - return parameters; - } - - public String endpointKey() { - return httpMethod + " " + uri; - } -} diff --git a/src/main/java/com/codechecker/api/model/MethodParameterSnapshot.java b/src/main/java/com/codechecker/api/model/MethodParameterSnapshot.java deleted file mode 100644 index 718d1a5..0000000 --- a/src/main/java/com/codechecker/api/model/MethodParameterSnapshot.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.codechecker.api.model; - -/** - * 接口方法入参快照。 - */ -public class MethodParameterSnapshot { - private final String name; - private final String bindingName; - private final String type; - private final String source; - private final boolean required; - private final String description; - private final String dtoClassName; - - public MethodParameterSnapshot(String name, String bindingName, String type, String source, - boolean required, String description, String dtoClassName) { - this.name = name; - this.bindingName = bindingName == null || bindingName.isBlank() ? name : bindingName; - this.type = type; - this.source = source; - this.required = required; - this.description = description; - this.dtoClassName = dtoClassName; - } - - public String getName() { - return name; - } - - /** 对外绑定名(@PathVariable / @RequestParam 的 value/name,缺省为形参名) */ - public String getBindingName() { - return bindingName; - } - - public String getType() { - return type; - } - - /** body / path / query / simple */ - public String getSource() { - return source; - } - - public boolean isRequired() { - return required; - } - - public String getDescription() { - return description; - } - - public String getDtoClassName() { - return dtoClassName; - } - - /** path/query 按绑定名匹配,避免仅 Java 形参重命名误报 */ - public String identityKey() { - if ("path".equals(source) || "query".equals(source)) { - return source + ":" + bindingName; - } - return source + ":" + name; - } - - /** 通知展示名:path/query 展示绑定名 */ - public String displayName() { - if ("path".equals(source) || "query".equals(source)) { - return bindingName; - } - return name; - } -} diff --git a/src/main/java/com/codechecker/api/model/ParameterChange.java b/src/main/java/com/codechecker/api/model/ParameterChange.java deleted file mode 100644 index 3e0af5c..0000000 --- a/src/main/java/com/codechecker/api/model/ParameterChange.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.codechecker.api.model; - -/** - * API 参数或 RequestBody 嵌套字段变更。 - */ -public class ParameterChange { - public enum ChangeType { - ADDED, REMOVED, MODIFIED, RENAMED - } - - private final ChangeType changeType; - private final String paramName; - private final String oldName; - private final String paramType; - private final String description; - private final String oldDescription; - private final String source; - private final String bodyParamName; - private final String parentDto; - private final String fieldPath; - private final String detail; - - private ParameterChange(ChangeType changeType, String paramName, String oldName, - String paramType, String description, String oldDescription, - String source, String bodyParamName, String parentDto, - String fieldPath, String detail) { - this.changeType = changeType; - this.paramName = paramName; - this.oldName = oldName; - this.paramType = paramType; - this.description = description; - this.oldDescription = oldDescription; - this.source = source; - this.bodyParamName = bodyParamName; - this.parentDto = parentDto; - this.fieldPath = fieldPath; - this.detail = detail; - } - - public static ParameterChange added(String name, String type, String desc, String source, - String bodyParam, String dto, String fieldPath) { - return new ParameterChange(ChangeType.ADDED, name, null, type, desc, null, - source, bodyParam, dto, fieldPath, null); - } - - public static ParameterChange removed(String name, String type, String desc, String source, - String bodyParam, String dto, String fieldPath) { - return new ParameterChange(ChangeType.REMOVED, name, null, type, desc, null, - source, bodyParam, dto, fieldPath, null); - } - - public static ParameterChange modified(String name, String type, String desc, - String detail, String source, String bodyParam, - String dto, String fieldPath) { - return new ParameterChange(ChangeType.MODIFIED, name, null, type, desc, null, - source, bodyParam, dto, fieldPath, detail); - } - - public static ParameterChange renamed(String oldName, String newName, String type, String desc, - String source, String bodyParam, String dto, String fieldPath) { - return new ParameterChange(ChangeType.RENAMED, newName, oldName, type, desc, null, - source, bodyParam, dto, fieldPath, null); - } - - public ChangeType getChangeType() { - return changeType; - } - - public String getParamName() { - return paramName; - } - - public String getOldName() { - return oldName; - } - - public String getParamType() { - return paramType; - } - - public String getDescription() { - return description; - } - - public String getSource() { - return source; - } - - public String getBodyParamName() { - return bodyParamName; - } - - public String getParentDto() { - return parentDto; - } - - public String getFieldPath() { - return fieldPath; - } - - public String getDetail() { - return detail; - } - - public boolean isBodyField() { - return "body".equals(source); - } - - public String displayName() { - return fieldPath == null || fieldPath.isBlank() ? paramName : fieldPath; - } -} diff --git a/src/main/java/com/codechecker/api/notify/ApiChangeNotifier.java b/src/main/java/com/codechecker/api/notify/ApiChangeNotifier.java deleted file mode 100644 index f620d05..0000000 --- a/src/main/java/com/codechecker/api/notify/ApiChangeNotifier.java +++ /dev/null @@ -1,272 +0,0 @@ -package com.codechecker.api.notify; - -import com.codechecker.api.model.ApiChangeKind; -import com.codechecker.api.model.EndpointChangeReport; -import com.codechecker.api.model.ParameterChange; -import com.codechecker.common.MarkdownStyles; -import com.codechecker.common.WeComMarkdownSender; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * API 变更通知(路径 / 请求方式 / 参数分类型、分条发送,与类变更通知解耦)。 - */ -public class ApiChangeNotifier { - private final WeComMarkdownSender sender = new WeComMarkdownSender(); - - public int sendAll(String webhookUrl, List reports, - String modifier, String modifyTime, boolean wecomEnabled) { - if (reports == null || reports.isEmpty()) { - System.out.println("无 API 变更,不发送通知"); - return 0; - } - int sent = 0; - for (EndpointChangeReport report : reports) { - String markdown = buildMarkdown(report, modifier, modifyTime); - if (wecomEnabled) { - if (sender.send(webhookUrl, markdown)) { - sent++; - System.out.println("已发送 API 变更通知: " + report.getChangeKind() - + " " + report.getHttpMethod() + " " + report.getUri()); - } - } else { - sender.logPreview("API 变更 [" + report.getChangeKind() + "]", markdown); - sent++; - } - } - if (sent > 0) { - System.out.println("总共发送 " + sent + " 条 API 变更通知"); - } - return sent; - } - - public String buildMarkdown(EndpointChangeReport report, String modifier, String modifyTime) { - ApiChangeKind kind = report.getChangeKind(); - if (kind == ApiChangeKind.PATH_CHANGED - || kind == ApiChangeKind.NEW_ENDPOINT - || kind == ApiChangeKind.REMOVED_ENDPOINT) { - return buildPathMarkdown(report, modifier, modifyTime); - } - if (kind == ApiChangeKind.METHOD_CHANGED) { - return buildMethodMarkdown(report, modifier, modifyTime); - } - return buildParamMarkdown(report, modifier, modifyTime); - } - - private String buildPathMarkdown(EndpointChangeReport report, String modifier, String modifyTime) { - String changeLabel; - switch (report.getChangeKind()) { - case NEW_ENDPOINT: - changeLabel = "新增接口"; - break; - case REMOVED_ENDPOINT: - changeLabel = "删除接口"; - break; - default: - changeLabel = "修改路径"; - break; - } - StringBuilder sb = new StringBuilder(); - sb.append("# 【API路径变更通知】").append("\n\n"); - sb.append(MarkdownStyles.quoteKvBold("变更类型", MarkdownStyles.colorWarning(changeLabel))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("路径", - MarkdownStyles.colorInfo(MarkdownStyles.safe(report.getSourceFile())))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("修改人", MarkdownStyles.colorComment(modifier))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("时间", MarkdownStyles.colorComment(modifyTime))).append("\n"); - sb.append("\n## 【URI变更详情】").append("\n\n"); - sb.append(MarkdownStyles.quoteKvBold("接口说明", formatEndpointDescription(report))).append("\n"); - appendPathUriLines(sb, report, changeLabel); - return sb.toString(); - } - - private void appendPathUriLines(StringBuilder sb, EndpointChangeReport report, String changeLabel) { - if ("新增接口".equals(changeLabel)) { - sb.append(MarkdownStyles.quoteKvBold("原路径", "`-`")).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("新路径", - formatUriWithMethod(report.getHttpMethod(), report.getUri(), true) - + " " + MarkdownStyles.colorInfo("[新增]"))).append("\n"); - } else if ("删除接口".equals(changeLabel)) { - sb.append(MarkdownStyles.quoteKvBold("原路径", - formatUriWithMethod(report.getHttpMethod(), report.getUri(), false) - + " " + MarkdownStyles.colorWarning("[已删除]"))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("新路径", "`已删除`")).append("\n"); - } else { - sb.append(MarkdownStyles.quoteKvBold("原路径", - formatUriWithMethod(report.getHttpMethod(), report.getOldUri(), false) - + " " + MarkdownStyles.colorWarning("[旧路径]"))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("新路径", - formatUriWithMethod(report.getHttpMethod(), report.getUri(), true) - + " " + MarkdownStyles.colorInfo("[新路径]"))).append("\n"); - } - } - - private String buildMethodMarkdown(EndpointChangeReport report, String modifier, String modifyTime) { - StringBuilder sb = new StringBuilder(); - sb.append("# 【API请求方式变更通知】").append("\n\n"); - sb.append(MarkdownStyles.quoteKvBold("变更类型", MarkdownStyles.colorWarning("修改请求方式"))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("路径", - MarkdownStyles.colorInfo(MarkdownStyles.safe(report.getSourceFile())))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("修改人", MarkdownStyles.colorComment(modifier))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("时间", MarkdownStyles.colorComment(modifyTime))).append("\n"); - sb.append("\n## 【请求方式变更详情】").append("\n\n"); - sb.append(MarkdownStyles.quoteKvBold("接口说明", formatEndpointDescription(report))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("URI", MarkdownStyles.colorInfo(report.getUri()))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("原请求方式", - MarkdownStyles.colorWarning(report.getOldHttpMethod()))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("新请求方式", - MarkdownStyles.colorInfo(report.getHttpMethod()) + " " - + MarkdownStyles.colorInfo("[请求方式已变更]"))).append("\n"); - return sb.toString(); - } - - private String buildParamMarkdown(EndpointChangeReport report, String modifier, String modifyTime) { - StringBuilder sb = new StringBuilder(); - sb.append("# 【API参数变更通知】").append("\n\n"); - sb.append(MarkdownStyles.quoteKvBold("修改人", MarkdownStyles.colorComment(modifier))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("时间", MarkdownStyles.colorComment(modifyTime))).append("\n"); - //sb.append(MarkdownStyles.quoteKvBold("变更类型", MarkdownStyles.colorWarning("修改参数"))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("URI", - MarkdownStyles.colorInfo(report.getHttpMethod()) + " " - + MarkdownStyles.inlineCode(report.getUri()))).append("\n"); - sb.append(MarkdownStyles.quoteKvBold("路径", - MarkdownStyles.colorInfo(MarkdownStyles.safe(report.getSourceFile())))).append("\n"); - sb.append("\n## 【接口参数变动详情】").append("\n\n"); - appendParameterDetails(sb, report); - return sb.toString(); - } - - private void appendParameterDetails(StringBuilder sb, EndpointChangeReport report) { - List bodyChanges = new ArrayList<>(); - List regularChanges = new ArrayList<>(); - for (ParameterChange change : report.getParameterChanges()) { - if (change.isBodyField()) { - bodyChanges.add(change); - } else { - regularChanges.add(change); - } - } - if (!bodyChanges.isEmpty()) { - sb.append("**类对象变更(含嵌套对象字段)**").append("\n\n"); - appendBodyGroups(sb, bodyChanges); - sb.append("\n"); - } - if (!regularChanges.isEmpty()) { - sb.append("**普通参数变更**").append("\n\n"); - sb.append(MarkdownStyles.quoteLine("**共 " - + MarkdownStyles.colorWarning(String.valueOf(regularChanges.size())) - + " 项变更**")).append("\n\n"); - for (ParameterChange change : regularChanges) { - sb.append(formatParameterLine(change)).append("\n\n"); - } - } - if (bodyChanges.isEmpty() && regularChanges.isEmpty()) { - sb.append(MarkdownStyles.quoteLine(MarkdownStyles.colorComment("无"))).append("\n"); - } - } - - private void appendBodyGroups(StringBuilder sb, List bodyChanges) { - Map> groups = new LinkedHashMap<>(); - for (ParameterChange change : bodyChanges) { - String key = change.getParentDto() == null || change.getParentDto().isBlank() - ? (change.getBodyParamName() == null ? "body" : change.getBodyParamName()) - : change.getParentDto(); - groups.computeIfAbsent(key, k -> new ArrayList<>()).add(change); - } - int total = bodyChanges.size(); - sb.append(MarkdownStyles.quoteLine("**共 " - + MarkdownStyles.colorWarning(String.valueOf(groups.size())) - + " 个类对象 · " - + MarkdownStyles.colorWarning(String.valueOf(total)) - + " 项变更**")).append("\n\n"); - for (List group : groups.values()) { - ParameterChange first = group.get(0); - if (first.getParentDto() != null && !first.getParentDto().isBlank()) { - sb.append("**").append(MarkdownStyles.inlineCode(first.getParentDto())).append("**"); - } else if (first.getBodyParamName() != null && !first.getBodyParamName().isBlank()) { - sb.append("**").append(MarkdownStyles.inlineCode(first.getBodyParamName())).append("**"); - } - sb.append("\n\n"); - for (ParameterChange change : group) { - sb.append(formatParameterLine(change)).append("\n\n"); - } - } - } - - private String formatParameterLine(ParameterChange change) { - String tag; - switch (change.getChangeType()) { - case ADDED: - tag = MarkdownStyles.colorInfo("[新增]"); - break; - case REMOVED: - tag = MarkdownStyles.colorWarning("[删除]"); - break; - case RENAMED: - tag = MarkdownStyles.colorWarning("[重命名]"); - break; - case MODIFIED: - tag = MarkdownStyles.colorWarning("[类型变更]"); - break; - default: - tag = MarkdownStyles.colorWarning("[修改]"); - break; - } - String name = MarkdownStyles.inlineCode(MarkdownStyles.safe(change.displayName())); - String desc = change.getDescription() == null || change.getDescription().isBlank() - ? MarkdownStyles.colorComment("(无说明)") - : MarkdownStyles.colorComment(change.getDescription()); - StringBuilder line = new StringBuilder(); - if (change.getChangeType() == ParameterChange.ChangeType.RENAMED) { - line.append(tag).append(" ") - .append(MarkdownStyles.colorComment(MarkdownStyles.safe(change.getOldName()))).append(" → ") - .append(MarkdownStyles.colorInfo(MarkdownStyles.safe(change.getParamName()))) - .append(" 说明: ").append(desc); - } else { - line.append(tag).append(" ").append(name).append(" 说明: ").append(desc); - } - appendParameterType(line, change); - return MarkdownStyles.quoteLine(line.toString()); - } - - private void appendParameterType(StringBuilder line, ParameterChange change) { - String typePart = resolveTypePart(change); - if (!typePart.isBlank()) { - line.append(" 类型: ").append(typePart); - } - } - - private String formatUriWithMethod(String httpMethod, String uri, boolean isNew) { - String path = MarkdownStyles.inlineCode(MarkdownStyles.safe(uri)); - if (httpMethod == null || httpMethod.isBlank()) { - return path; - } - String methodPart = isNew - ? MarkdownStyles.colorInfo(httpMethod.toUpperCase()) - : MarkdownStyles.colorWarning(httpMethod.toUpperCase()); - return methodPart + " " + path; - } - - private String formatEndpointDescription(EndpointChangeReport report) { - String desc = report.getEndpointDescription(); - if (desc == null || desc.isBlank()) { - return MarkdownStyles.colorComment("(无说明)"); - } - return MarkdownStyles.colorComment(MarkdownStyles.safe(desc)); - } - - private String resolveTypePart(ParameterChange change) { - if (change.getChangeType() == ParameterChange.ChangeType.MODIFIED - && change.getDetail() != null && !change.getDetail().isBlank()) { - return MarkdownStyles.formatTypeChange(change.getDetail()); - } - if (change.getParamType() != null && !change.getParamType().isBlank()) { - boolean isNew = change.getChangeType() == ParameterChange.ChangeType.ADDED - || change.getChangeType() == ParameterChange.ChangeType.RENAMED; - return MarkdownStyles.formatSingleType(change.getParamType(), isNew); - } - return ""; - } -} diff --git a/src/main/java/com/codechecker/api/parser/EndpointSnapshotParser.java b/src/main/java/com/codechecker/api/parser/EndpointSnapshotParser.java deleted file mode 100644 index 7a92204..0000000 --- a/src/main/java/com/codechecker/api/parser/EndpointSnapshotParser.java +++ /dev/null @@ -1,313 +0,0 @@ -package com.codechecker.api.parser; - -import com.codechecker.api.model.EndpointSnapshot; -import com.codechecker.api.model.MethodParameterSnapshot; -import com.codechecker.parser.TypeNameUtils; -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.ast.CompilationUnit; -import com.github.javaparser.ast.NodeList; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.Parameter; -import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; -import com.github.javaparser.ast.type.Type; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -/** - * 解析 Controller / Feign 接口完整快照(含入参明细)。 - */ -public class EndpointSnapshotParser { - private static final Set MAPPING_ANNOTATIONS = Set.of( - "GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping" - ); - private static final Map MAPPING_DEFAULT_METHOD = Map.of( - "GetMapping", "GET", - "PostMapping", "POST", - "PutMapping", "PUT", - "DeleteMapping", "DELETE", - "PatchMapping", "PATCH" - ); - private static final Set FRAMEWORK_PARAM_TYPES = Set.of( - "HttpServletRequest", "HttpServletResponse", "BindingResult", "Principal", - "Authentication", "Model", "ModelMap", "UriComponentsBuilder", "WebRequest", - "NativeWebRequest", "Errors", "Locale" - ); - - private final boolean excludeFrameworkParams; - - public EndpointSnapshotParser(boolean excludeFrameworkParams) { - this.excludeFrameworkParams = excludeFrameworkParams; - } - - public List parseSource(String source, String sourceFile, boolean feignMode) { - if (source == null || source.isBlank()) { - return List.of(); - } - CompilationUnit cu = StaticJavaParser.parse(source); - List snapshots = new ArrayList<>(); - for (TypeDeclaration type : cu.getTypes()) { - if (!(type instanceof ClassOrInterfaceDeclaration)) { - continue; - } - ClassOrInterfaceDeclaration decl = (ClassOrInterfaceDeclaration) type; - if (feignMode && !isFeignClient(decl)) { - continue; - } - if (!feignMode && !isController(decl)) { - continue; - } - String basePath = feignMode - ? joinPaths(extractFeignBasePath(decl), extractTypeLevelPath(decl)) - : extractTypeLevelPath(decl); - String className = decl.getNameAsString(); - for (MethodDeclaration method : decl.getMethods()) { - if (feignMode && !decl.isInterface()) { - continue; - } - if (!feignMode && decl.isInterface()) { - continue; - } - snapshots.addAll(parseMethod(method, basePath, sourceFile, className)); - } - } - return snapshots; - } - - private List parseMethod(MethodDeclaration method, String basePath, - String sourceFile, String className) { - List result = new ArrayList<>(); - for (AnnotationExpr annotation : method.getAnnotations()) { - String annName = annotation.getNameAsString(); - if (!MAPPING_ANNOTATIONS.contains(annName)) { - continue; - } - List subPaths = readStringArray(annotation, "value", "path"); - List httpMethods = extractHttpMethods(annotation, annName); - List params = extractParameters(method); - String methodDescription = MethodDescriptionExtractor.extract(method); - String fingerprint = EndpointSnapshot.buildFingerprint(sourceFile, method.getNameAsString()); - for (String httpMethod : httpMethods) { - for (String subPath : subPaths) { - String uri = joinPaths(basePath, subPath); - result.add(new EndpointSnapshot(fingerprint, httpMethod, uri, sourceFile, - className, method.getNameAsString(), methodDescription, params)); - } - } - } - return result; - } - - private List extractParameters(MethodDeclaration method) { - Map paramDescriptions = MethodParamJavadocExtractor.extract(method); - List params = new ArrayList<>(); - for (Parameter parameter : method.getParameters()) { - String typeName = TypeNameUtils.typeToString(parameter.getType()); - String simple = TypeNameUtils.simpleName(typeName); - if (excludeFrameworkParams && FRAMEWORK_PARAM_TYPES.contains(simple)) { - continue; - } - String source = resolveParamSource(parameter); - String paramName = parameter.getNameAsString(); - String bindingName = resolveBindingName(parameter, source, paramName); - boolean required = resolveRequired(parameter, source); - String dtoName = "body".equals(source) ? simple : ""; - String description = paramDescriptions.getOrDefault(paramName, ""); - params.add(new MethodParameterSnapshot( - paramName, - bindingName, - typeName, - source, - required, - description, - dtoName - )); - } - return params; - } - - private String resolveBindingName(Parameter parameter, String source, String paramName) { - if (!"path".equals(source) && !"query".equals(source)) { - return paramName; - } - String annName = "path".equals(source) ? "PathVariable" : "RequestParam"; - for (AnnotationExpr ann : parameter.getAnnotations()) { - if (!annName.equals(ann.getNameAsString())) { - continue; - } - List bindings = readStringArray(ann, "value", "name"); - for (String binding : bindings) { - if (binding != null && !binding.isBlank()) { - return binding; - } - } - } - return paramName; - } - - private String resolveParamSource(Parameter parameter) { - for (AnnotationExpr ann : parameter.getAnnotations()) { - String name = ann.getNameAsString(); - if ("RequestBody".equals(name)) { - return "body"; - } - if ("PathVariable".equals(name)) { - return "path"; - } - if ("RequestParam".equals(name)) { - return "query"; - } - } - return "simple"; - } - - private boolean resolveRequired(Parameter parameter, String source) { - if ("query".equals(source)) { - for (AnnotationExpr ann : parameter.getAnnotations()) { - if ("RequestParam".equals(ann.getNameAsString()) && ann.isNormalAnnotationExpr()) { - for (var pair : ann.asNormalAnnotationExpr().getPairs()) { - if ("required".equals(pair.getNameAsString())) { - return !"false".equalsIgnoreCase(pair.getValue().toString().trim()); - } - } - } - } - } - return !"query".equals(source); - } - - private boolean isController(ClassOrInterfaceDeclaration decl) { - return decl.getAnnotations().stream() - .anyMatch(ann -> { - String n = ann.getNameAsString(); - return "RestController".equals(n) || "Controller".equals(n); - }); - } - - private boolean isFeignClient(ClassOrInterfaceDeclaration decl) { - return decl.isInterface() && decl.getAnnotations().stream() - .anyMatch(ann -> "FeignClient".equals(ann.getNameAsString())); - } - - private String extractTypeLevelPath(ClassOrInterfaceDeclaration decl) { - for (AnnotationExpr annotation : decl.getAnnotations()) { - if ("RequestMapping".equals(annotation.getNameAsString())) { - List paths = readStringArray(annotation, "value", "path"); - if (!paths.isEmpty()) { - return paths.get(0); - } - } - } - return ""; - } - - private String extractFeignBasePath(ClassOrInterfaceDeclaration decl) { - for (AnnotationExpr annotation : decl.getAnnotations()) { - if ("FeignClient".equals(annotation.getNameAsString())) { - List paths = readStringArray(annotation, "path"); - if (!paths.isEmpty()) { - return paths.get(0); - } - } - } - return ""; - } - - private List extractHttpMethods(AnnotationExpr annotation, String annName) { - if (!"RequestMapping".equals(annName)) { - return List.of(MAPPING_DEFAULT_METHOD.getOrDefault(annName, "GET")); - } - List methods = readEnumArray(annotation, "method"); - return methods.isEmpty() ? List.of("GET") : methods; - } - - private String joinPaths(String base, String sub) { - String normalizedBase = normalizePath(base); - String normalizedSub = normalizePath(sub); - if (normalizedBase.isEmpty()) { - return normalizedSub.isEmpty() ? "/" : normalizedSub; - } - if (normalizedSub.isEmpty()) { - return normalizedBase; - } - return (normalizedBase + "/" + normalizedSub.substring(1)).replaceAll("/+", "/"); - } - - private String normalizePath(String path) { - if (path == null || path.isBlank()) { - return ""; - } - String trimmed = path.trim(); - if (!trimmed.startsWith("/")) { - trimmed = "/" + trimmed; - } - return trimmed.replaceAll("/+", "/"); - } - - private List readStringArray(AnnotationExpr annotation, String... keys) { - NodeList values = readArrayValues(annotation, keys); - List result = new ArrayList<>(); - for (Object value : values) { - String text = value.toString().replace("\"", "").trim(); - if (!text.isBlank()) { - result.add(text); - } - } - if (result.isEmpty()) { - result.add(""); - } - return result; - } - - private List readEnumArray(AnnotationExpr annotation, String key) { - NodeList values = readArrayValues(annotation, key); - List result = new ArrayList<>(); - for (Object value : values) { - String text = value.toString().trim(); - if (text.contains(".")) { - text = text.substring(text.lastIndexOf('.') + 1); - } - result.add(text.toUpperCase(Locale.ROOT)); - } - return result; - } - - private NodeList readArrayValues(AnnotationExpr annotation, String... keys) { - if (annotation.isSingleMemberAnnotationExpr()) { - Expression value = annotation.asSingleMemberAnnotationExpr().getMemberValue(); - if (value.isArrayInitializerExpr()) { - return value.asArrayInitializerExpr().getValues(); - } - return new NodeList<>(value); - } - if (annotation.isNormalAnnotationExpr()) { - var pairs = annotation.asNormalAnnotationExpr().getPairs(); - for (var pair : pairs) { - for (String key : keys) { - if (pair.getNameAsString().equals(key)) { - if (pair.getValue().isArrayInitializerExpr()) { - return pair.getValue().asArrayInitializerExpr().getValues(); - } - return new NodeList<>(pair.getValue()); - } - } - } - for (var pair : pairs) { - if ("value".equals(pair.getNameAsString())) { - if (pair.getValue().isArrayInitializerExpr()) { - return pair.getValue().asArrayInitializerExpr().getValues(); - } - return new NodeList<>(pair.getValue()); - } - } - } - return new NodeList<>(); - } -} diff --git a/src/main/java/com/codechecker/api/parser/JavaSourceLocator.java b/src/main/java/com/codechecker/api/parser/JavaSourceLocator.java deleted file mode 100644 index 80cf6bc..0000000 --- a/src/main/java/com/codechecker/api/parser/JavaSourceLocator.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.codechecker.api.parser; - -import com.codechecker.git.GitChangeScanner; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; - -/** - * 按简单类名在仓库中定位 .java 源文件。 - */ -public class JavaSourceLocator { - private final Path repoRoot; - private final List searchDirs; - - public JavaSourceLocator(Path repoRoot, List searchDirs) { - this.repoRoot = repoRoot; - this.searchDirs = searchDirs; - } - - public Optional readSourceBySimpleName(String simpleClassName) throws IOException { - Optional path = findFile(simpleClassName); - if (path.isEmpty()) { - return Optional.empty(); - } - return Optional.of(Files.readString(path.get())); - } - - public Optional readSourceAtCommit(GitChangeScanner gitScanner, String sha, - String simpleClassName) throws IOException { - Optional 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 findRelativePath(String simpleClassName) throws IOException { - Optional path = findFile(simpleClassName); - return path.map(p -> repoRoot.relativize(p).toString().replace('\\', '/')); - } - - public Optional findFile(String simpleClassName) throws IOException { - String fileName = simpleClassName + ".java"; - for (String dir : searchDirs) { - Path root = repoRoot.resolve(dir.replace('\\', '/')); - if (!Files.exists(root)) { - continue; - } - try (Stream walk = Files.walk(root)) { - Optional found = walk - .filter(p -> p.getFileName().toString().equals(fileName)) - .findFirst(); - if (found.isPresent()) { - return found; - } - } - } - return Optional.empty(); - } -} diff --git a/src/main/java/com/codechecker/api/parser/MethodDescriptionExtractor.java b/src/main/java/com/codechecker/api/parser/MethodDescriptionExtractor.java deleted file mode 100644 index bbea177..0000000 --- a/src/main/java/com/codechecker/api/parser/MethodDescriptionExtractor.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.codechecker.api.parser; - -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.comments.JavadocComment; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; - -import java.util.Optional; - -/** - * 提取接口方法中文说明:@Operation(summary) > @Operation(description) > Javadoc 首段。 - */ -public final class MethodDescriptionExtractor { - private MethodDescriptionExtractor() { - } - - public static String extract(MethodDeclaration method) { - if (method == null) { - return ""; - } - for (AnnotationExpr annotation : method.getAnnotations()) { - if (!"Operation".equals(annotation.getNameAsString())) { - continue; - } - String summary = readAnnotationStringValue(annotation, "summary"); - if (!summary.isEmpty()) { - return summary; - } - String description = readAnnotationStringValue(annotation, "description"); - if (!description.isEmpty()) { - return description; - } - } - return extractMethodJavadoc(method); - } - - private static String extractMethodJavadoc(MethodDeclaration method) { - Optional javadoc = method.getJavadocComment(); - if (javadoc.isEmpty()) { - return ""; - } - String text = javadoc.get().parse().getDescription().toText(); - return text == null ? "" : text.trim().replaceAll("\\s+", " "); - } - - private static String readAnnotationStringValue(AnnotationExpr annotation, String attributeName) { - if (annotation.isNormalAnnotationExpr()) { - NormalAnnotationExpr normal = annotation.asNormalAnnotationExpr(); - for (var pair : normal.getPairs()) { - if (pair.getNameAsString().equals(attributeName)) { - return literalString(pair.getValue()); - } - } - return ""; - } - if (annotation.isSingleMemberAnnotationExpr()) { - SingleMemberAnnotationExpr single = annotation.asSingleMemberAnnotationExpr(); - if ("value".equals(attributeName) || "description".equals(attributeName) - || "summary".equals(attributeName)) { - return literalString(single.getMemberValue()); - } - } - return ""; - } - - private static String literalString(Expression expression) { - if (expression.isStringLiteralExpr()) { - return expression.asStringLiteralExpr().getValue().trim(); - } - return ""; - } -} diff --git a/src/main/java/com/codechecker/api/parser/MethodParamJavadocExtractor.java b/src/main/java/com/codechecker/api/parser/MethodParamJavadocExtractor.java deleted file mode 100644 index a3dbcd7..0000000 --- a/src/main/java/com/codechecker/api/parser/MethodParamJavadocExtractor.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.codechecker.api.parser; - -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.comments.JavadocComment; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * 从方法 Javadoc 的 @param 标签提取形参说明(按 Java 形参名匹配)。 - */ -public final class MethodParamJavadocExtractor { - private static final Pattern PARAM_TAG = Pattern.compile( - "@param\\s+(\\w+)\\s+(.+?)(?=\\s*@param\\s+|\\s*@return\\s+|\\s*@throws\\s+|\\s*@see\\s+|\\s*\\*/|$)", - Pattern.DOTALL); - - private MethodParamJavadocExtractor() { - } - - public static Map extract(MethodDeclaration method) { - Map descriptions = new HashMap<>(); - if (method == null) { - return descriptions; - } - Optional javadoc = method.getJavadocComment(); - if (javadoc.isEmpty()) { - return descriptions; - } - String raw = javadoc.get().getContent(); - if (raw == null || raw.isBlank()) { - return descriptions; - } - String normalized = raw.replace('\r', '\n'); - Matcher matcher = PARAM_TAG.matcher(normalized); - while (matcher.find()) { - String paramName = matcher.group(1).trim(); - String desc = cleanDescription(matcher.group(2)); - if (!paramName.isBlank()) { - descriptions.put(paramName, desc); - } - } - return descriptions; - } - - private static String cleanDescription(String text) { - if (text == null) { - return ""; - } - StringBuilder sb = new StringBuilder(); - for (String line : text.split("\n")) { - String trimmed = line.trim(); - if (trimmed.startsWith("*")) { - trimmed = trimmed.substring(1).trim(); - } - if (!trimmed.isEmpty()) { - if (sb.length() > 0) { - sb.append(' '); - } - sb.append(trimmed); - } - } - return sb.toString().trim(); - } -} diff --git a/src/main/java/com/codechecker/api/parser/NestedDtoFieldParser.java b/src/main/java/com/codechecker/api/parser/NestedDtoFieldParser.java deleted file mode 100644 index d44603f..0000000 --- a/src/main/java/com/codechecker/api/parser/NestedDtoFieldParser.java +++ /dev/null @@ -1,99 +0,0 @@ -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; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -/** - * 递归展开 Dto/Vo 嵌套字段(dot path),与类变更字段解析解耦但复用 ClassFieldParser。 - */ -public class NestedDtoFieldParser { - 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 ClassFieldParser classFieldParser = new ClassFieldParser(); - private final JavaSourceLocator sourceLocator; - 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, 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 { - return parseNestedFields(dtoClassName, oldSha); - } - - public List parseNestedFieldsAtNewCommit(String dtoClassName) throws IOException { - return parseNestedFields(dtoClassName, newSha); - } - - private List parseNestedFields(String dtoClassName, String sha) throws IOException { - Set visiting = new HashSet<>(); - List result = new ArrayList<>(); - collectFields(dtoClassName, "", visiting, result, sha, 1); - return result; - } - - private void collectFields(String className, String prefix, Set visiting, - List out, String sha, int depth) throws IOException { - if (className == null || className.isBlank() || visiting.contains(className) || depth > maxDepth) { - return; - } - visiting.add(className); - Optional source = readSource(className, sha); - if (source.isEmpty()) { - visiting.remove(className); - return; - } - List fields = classFieldParser.parseFields(source.get(), className); - for (FieldInfo field : fields) { - String path = prefix.isBlank() ? field.getName() : prefix + "." + field.getName(); - Set nestedTypes = TypeNameUtils.peelDirectTypeNames(field.getType()); - boolean expanded = false; - for (String nestedType : nestedTypes) { - if (isLeafType(nestedType) || nestedType.equals(className)) { - continue; - } - expanded = true; - // 嵌套字段路径用类型简单类名(如 UserSelfDto.nickName),不用成员名(userDtos.nickName) - collectFields(nestedType, nestedType, visiting, out, sha, depth + 1); - } - if (!expanded) { - out.add(new NestedFieldInfo(path, field.getType(), field.getDescription())); - } - } - visiting.remove(className); - } - - private Optional 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("[]"); - } -} diff --git a/src/main/java/com/codechecker/api/parser/NestedFieldInfo.java b/src/main/java/com/codechecker/api/parser/NestedFieldInfo.java deleted file mode 100644 index 870817e..0000000 --- a/src/main/java/com/codechecker/api/parser/NestedFieldInfo.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.codechecker.api.parser; - -/** - * DTO 嵌套字段扁平化条目(dot path)。 - */ -public class NestedFieldInfo { - private final String path; - private final String type; - private final String description; - - public NestedFieldInfo(String path, String type, String description) { - this.path = path; - this.type = type; - this.description = description; - } - - public String getPath() { - return path; - } - - public String getType() { - return type; - } - - public String getDescription() { - return description; - } -} diff --git a/src/main/java/com/codechecker/api/scanner/ApiFileChangeScanner.java b/src/main/java/com/codechecker/api/scanner/ApiFileChangeScanner.java deleted file mode 100644 index 40b3686..0000000 --- a/src/main/java/com/codechecker/api/scanner/ApiFileChangeScanner.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.codechecker.api.scanner; - -import com.codechecker.git.GitChangeScanner; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -/** - * 扫描 API 相关 Java 文件变更(Controller / Feign),与类变更扫描解耦。 - */ -public class ApiFileChangeScanner { - private final GitChangeScanner gitScanner; - - public ApiFileChangeScanner(GitChangeScanner gitScanner) { - this.gitScanner = gitScanner; - } - - /** 返回两次提交间变更的 .java 相对路径(位于 scanDirs 下) */ - public List scanChangedFiles(Path repoRoot, List scanDirs, - String oldSha, String newSha) throws IOException { - Set changed = new LinkedHashSet<>(); - List diffLines = gitScanner.diffNameOnly(oldSha, newSha); - for (String line : diffLines) { - String path = normalize(line); - if (!path.endsWith(".java")) { - continue; - } - if (isUnderScanDirs(path, scanDirs)) { - changed.add(path); - } - } - return new ArrayList<>(changed); - } - - private boolean isUnderScanDirs(String relativePath, List scanDirs) { - String normalized = relativePath.replace('\\', '/'); - for (String dir : scanDirs) { - String prefix = dir.replace('\\', '/'); - if (!prefix.endsWith("/")) { - prefix = prefix + "/"; - } - if (normalized.startsWith(prefix)) { - return true; - } - } - return false; - } - - private String normalize(String path) { - return path.replace('\\', '/').trim(); - } -} diff --git a/src/main/java/com/codechecker/common/MarkdownStyles.java b/src/main/java/com/codechecker/common/MarkdownStyles.java deleted file mode 100644 index 9a579f7..0000000 --- a/src/main/java/com/codechecker/common/MarkdownStyles.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.codechecker.common; - -/** - * 企微 Markdown v1 公共样式(类变更 / API 变更通知共用)。 - */ -public final class MarkdownStyles { - private MarkdownStyles() { - } - - public static String colorInfo(String text) { - return "" + text + ""; - } - - public static String colorComment(String text) { - return "" + safe(text) + ""; - } - - public static String colorWarning(String text) { - return "" + text + ""; - } - - public static String quoteKvBold(String key, String value) { - return "> **" + key + ": " + value + "**"; - } - - public static String quoteKv(String key, String value) { - return "> " + key + ": " + value; - } - - public static String quoteLine(String content) { - return "> " + content; - } - - public static String inlineCode(String text) { - return "`" + text.replace("`", "'") + "`"; - } - - /** 类型展示:泛型尖括号不转义 */ - public static String formatTypeChange(String detail) { - int arrow = detail.indexOf(" → "); - if (arrow < 0) { - return colorWarning(detail); - } - String oldType = detail.substring(0, arrow).trim(); - String newType = detail.substring(arrow + 3).trim(); - return colorWarning(oldType) + " → " + colorInfo(newType); - } - - public static String formatSingleType(String type, boolean isNew) { - if (type == null || type.isBlank()) { - return ""; - } - return isNew ? colorInfo(type) : colorWarning(type); - } - - public static String safe(String text) { - if (text == null) { - return ""; - } - return text.replace("&", "&").replace("<", "<").replace(">", ">"); - } -} diff --git a/src/main/java/com/codechecker/common/WeComMarkdownSender.java b/src/main/java/com/codechecker/common/WeComMarkdownSender.java deleted file mode 100644 index 3d6c56c..0000000 --- a/src/main/java/com/codechecker/common/WeComMarkdownSender.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.codechecker.common; - -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -/** - * 企微 Markdown 发送(与具体变更类型解耦)。 - */ -public class WeComMarkdownSender { - private static final int MAX_LENGTH = 3800; - private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); - - private final OkHttpClient client = new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build(); - - public boolean send(String webhookUrl, String content) { - return postMarkdown(webhookUrl, truncate(content)); - } - - public void logPreview(String title, String content) { - System.out.println("========== " + title + " =========="); - System.out.println(content); - System.out.println("========== 结束 =========="); - } - - private boolean postMarkdown(String webhookUrl, String content) { - if (webhookUrl == null || webhookUrl.isBlank() || webhookUrl.contains("YOUR_WECOM")) { - System.out.println("[警告] 未配置有效的企业微信 Webhook URL"); - System.out.println("--- 通知预览 ---"); - System.out.println(content.length() > 1000 ? content.substring(0, 1000) : content); - return false; - } - - String payload = "{\"msgtype\":\"markdown\",\"markdown\":{\"content\":" - + jsonEscape(content) + "}}"; - Request request = new Request.Builder() - .url(webhookUrl) - .post(RequestBody.create(payload, JSON)) - .build(); - try (Response response = client.newCall(request).execute()) { - if (response.isSuccessful() && response.body() != null) { - return response.body().string().contains("\"errcode\":0"); - } - System.out.println("[错误] 企微返回异常: " + response.code()); - return false; - } catch (IOException e) { - System.out.println("[错误] 发送企微消息失败: " + e.getMessage()); - return false; - } - } - - private String truncate(String text) { - if (text.length() <= MAX_LENGTH) { - return text; - } - return text.substring(0, MAX_LENGTH) + "\n\n... 消息过长,已截断"; - } - - private String jsonEscape(String text) { - String escaped = text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", ""); - return "\"" + escaped + "\""; - } -} diff --git a/src/main/java/com/codechecker/config/AppConfig.java b/src/main/java/com/codechecker/config/AppConfig.java deleted file mode 100644 index 6b35058..0000000 --- a/src/main/java/com/codechecker/config/AppConfig.java +++ /dev/null @@ -1,239 +0,0 @@ -package com.codechecker.config; - -import org.yaml.snakeyaml.Yaml; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * 读取 .gitea/workflows/code-check-config.yaml,提供检测开关、扫描目录、企微配置等。 - */ -public class AppConfig { - private boolean masterEnabled = true; - private boolean classCheckEnabled = true; - private boolean dtoEntityConversionEnabled = true; - private List modelDirs = new ArrayList<>(); - private List controllerScanDirs = new ArrayList<>(); - private List feignScanDirs = new ArrayList<>(); - private List conversionScanDirs = new ArrayList<>(); - private String wecomWebhookUrl = ""; - private boolean wecomEnabled = true; - 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<>(); - private List apiFeignScanDirs = new ArrayList<>(); - private DtoOverlapMode dtoOverlapMode = DtoOverlapMode.BOTH; - - /** 从 YAML 文件加载配置 */ - @SuppressWarnings("unchecked") - public static AppConfig load(Path configPath) throws IOException { - Yaml yaml = new Yaml(); - Map root; - try (InputStream in = Files.newInputStream(configPath)) { - root = yaml.load(in); - } - if (root == null) { - root = Map.of(); - } - - AppConfig config = new AppConfig(); - Map checker = mapOrEmpty(root.get("checker")); - config.masterEnabled = boolOrDefault(checker.get("enabled"), true); - - Map classCheck = mapOrEmpty(root.get("class_check")); - config.classCheckEnabled = boolOrDefault(classCheck.get("enabled"), true); - - 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); - - config.modelDirs = stringList(classCheck.get("model_dirs")); - Map endpointScan = mapOrEmpty(classCheck.get("endpoint_scan")); - config.controllerScanDirs = stringList(endpointScan.get("controllers")); - config.feignScanDirs = stringList(endpointScan.get("feign_apis")); - config.conversionScanDirs = stringList(classCheck.get("conversion_scan")); - - Map wecom = mapOrEmpty(root.get("wecom")); - config.wecomWebhookUrl = stringOrEmpty(wecom.get("webhook_url")); - config.wecomEnabled = boolOrDefault(wecom.get("enabled"), true); - - Map 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 apiCheck = mapOrEmpty(root.get("api_check")); - config.apiCheckEnabled = boolOrDefault(apiCheck.get("enabled"), true); - config.apiExcludeFrameworkParams = boolOrDefault(apiCheck.get("exclude_framework_params"), true); - Map apiEndpointScan = mapOrEmpty(apiCheck.get("endpoint_scan")); - config.apiControllerScanDirs = stringList(apiEndpointScan.get("controllers")); - config.apiFeignScanDirs = stringList(apiEndpointScan.get("feign_apis")); - if (config.apiControllerScanDirs.isEmpty()) { - config.apiControllerScanDirs = new ArrayList<>(config.controllerScanDirs); - } - if (config.apiFeignScanDirs.isEmpty()) { - config.apiFeignScanDirs = new ArrayList<>(config.feignScanDirs); - } - - return config; - } - - /** 安全转为 Map,非 Map 则返回空 Map */ - @SuppressWarnings("unchecked") - private static Map mapOrEmpty(Object value) { - if (value instanceof Map) { - return (Map) value; - } - return Map.of(); - } - - /** 安全转为字符串列表 */ - @SuppressWarnings("unchecked") - private static List stringList(Object value) { - if (value instanceof List) { - List list = (List) value; - List result = new ArrayList<>(); - for (Object item : list) { - if (item != null) { - result.add(item.toString()); - } - } - return result; - } - return new ArrayList<>(); - } - - /** 安全转为 boolean,缺省用 defaultValue */ - private static boolean boolOrDefault(Object value, boolean defaultValue) { - if (value instanceof Boolean) { - return (Boolean) value; - } - 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(); - } - - /** 变更检测总开关(checker.enabled,控制 class_check + api_check) */ - public boolean isMasterEnabled() { - return masterEnabled; - } - - /** 类变更检测开关(class_check.enabled) */ - public boolean isClassCheckEnabled() { - return classCheckEnabled; - } - - /** Dto→Entity 类转换检测开关 */ - public boolean isDtoEntityConversionEnabled() { - return dtoEntityConversionEnabled; - } - - /** 模型类目录(预留,当前扫描仍按类名后缀) */ - public List getModelDirs() { - return modelDirs; - } - - /** Controller 扫描目录 */ - public List getControllerScanDirs() { - return controllerScanDirs; - } - - /** Feign 接口扫描目录 */ - public List getFeignScanDirs() { - return feignScanDirs; - } - - /** BeanUtils / convert 扫描目录 */ - public List getConversionScanDirs() { - return conversionScanDirs; - } - - /** 企微 Webhook 地址 */ - public String getWecomWebhookUrl() { - return wecomWebhookUrl; - } - - /** 企微通知开关 */ - public boolean isWecomEnabled() { - return wecomEnabled; - } - - /** 无变更时是否打印提示后退出 */ - public boolean isOnlyOnChange() { - return onlyOnChange; - } - - /** Dto 类变更后是否继续检测受影响接口的 API 参数变更 */ - public boolean isDtoApiFollowUpEnabled() { - return dtoApiFollowUpEnabled; - } - - /** Dto/Vo 嵌套展开最大深度(默认 3,可按需调至 4、5) */ - public int getNestMaxDepth() { - return nestMaxDepth; - } - - /** API 变更检测总开关 */ - public boolean isApiCheckEnabled() { - return apiCheckEnabled; - } - - /** Dto 类变更与 API 参数变更重叠时的通知策略 */ - public DtoOverlapMode getDtoOverlapMode() { - return dtoOverlapMode; - } - - /** 是否排除 Spring 框架注入参数 */ - public boolean isApiExcludeFrameworkParams() { - return apiExcludeFrameworkParams; - } - - /** API 检测 Controller 扫描目录 */ - public List getApiControllerScanDirs() { - return apiControllerScanDirs; - } - - /** API 检测 Feign 扫描目录 */ - public List getApiFeignScanDirs() { - return apiFeignScanDirs; - } - - /** API 检测所有扫描目录(Controller + Feign) */ - public List getAllApiScanDirs() { - List dirs = new ArrayList<>(); - dirs.addAll(apiControllerScanDirs); - dirs.addAll(apiFeignScanDirs); - return dirs; - } -} diff --git a/src/main/java/com/codechecker/config/DtoOverlapMode.java b/src/main/java/com/codechecker/config/DtoOverlapMode.java deleted file mode 100644 index a62d1bb..0000000 --- a/src/main/java/com/codechecker/config/DtoOverlapMode.java +++ /dev/null @@ -1,24 +0,0 @@ -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; - } - } -} diff --git a/src/main/java/com/codechecker/git/GitChangeScanner.java b/src/main/java/com/codechecker/git/GitChangeScanner.java deleted file mode 100644 index f7716bf..0000000 --- a/src/main/java/com/codechecker/git/GitChangeScanner.java +++ /dev/null @@ -1,295 +0,0 @@ -package com.codechecker.git; - -import com.codechecker.model.ChangedClassFile; -import com.codechecker.model.ClassType; -import com.codechecker.parser.ClassDeclParser; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -/** - * 执行 git diff,识别 Dto/Vo/Entity/Model 的修改、删除、重命名(含 R* 与同目录 D+A 配对)。 - */ -public class GitChangeScanner { - private final Path repoRoot; - private final ClassDeclParser classDeclParser = new ClassDeclParser(); - - public GitChangeScanner(Path repoRoot) { - this.repoRoot = repoRoot; - } - - /** 扫描两次提交间的模型类变更 */ - public List scanChangedClasses(String oldSha, String newSha) throws IOException { - List lines = runGit("diff", "--name-status", oldSha, newSha); - List deletions = new ArrayList<>(); - Map additionsByParent = new LinkedHashMap<>(); - List result = new ArrayList<>(); - - for (String line : lines) { - if (line.isBlank()) { - continue; - } - String[] parts = line.split("\t"); - if (parts.length < 2) { - continue; - } - String status = parts[0].trim(); - - if (status.startsWith("R") && parts.length >= 3) { - ChangedClassFile renamed = buildRenamed(parts[1], parts[2], oldSha, newSha); - if (renamed != null) { - result.add(renamed); - } - continue; - } - - String path = normalizePath(parts[parts.length - 1]); - if (!path.endsWith(".java")) { - continue; - } - - String fallbackName = ClassDeclParser.classNameFromPath(path); - ClassType classType = ClassType.fromClassName(fallbackName); - if (classType == null) { - continue; - } - - if (status.equals("A")) { - String newSource = readFileAtCommit(newSha, path); - String className = classDeclParser.resolveClassName(newSource, fallbackName); - additionsByParent.computeIfAbsent(parentDir(path), k -> new PendingAdd()) - .add(path, className, classType, newSource); - continue; - } - - if (status.equals("D")) { - String oldSource = readFileAtCommit(oldSha, path); - String className = classDeclParser.resolveClassName(oldSource, fallbackName); - deletions.add(new ChangedClassFile(path, ChangedClassFile.ChangeStatus.DELETED, className, classType)); - continue; - } - - if (status.startsWith("M")) { - ChangedClassFile modified = buildModified(path, oldSha, newSha, fallbackName, classType); - if (modified != null) { - result.add(modified); - } - } - } - - pairDeleteAndAdd(deletions, additionsByParent, oldSha, result); - result.addAll(deletions); - return result; - } - - /** 处理 git R 状态:路径重命名 */ - private ChangedClassFile buildRenamed(String oldPathRaw, String newPathRaw, - String oldSha, String newSha) throws IOException { - String oldPath = normalizePath(oldPathRaw); - String newPath = normalizePath(newPathRaw); - if (!oldPath.endsWith(".java") || !newPath.endsWith(".java")) { - return null; - } - - String oldFallback = ClassDeclParser.classNameFromPath(oldPath); - String newFallback = ClassDeclParser.classNameFromPath(newPath); - ClassType classType = ClassType.fromClassName(newFallback); - if (classType == null) { - classType = ClassType.fromClassName(oldFallback); - } - if (classType == null) { - return null; - } - - String oldSource = readFileAtCommit(oldSha, oldPath); - String newSource = readFileAtCommit(newSha, newPath); - String oldClassName = classDeclParser.resolveClassName(oldSource, oldFallback); - String newClassName = classDeclParser.resolveClassName(newSource, newFallback); - - return new ChangedClassFile(newPath, oldPath, ChangedClassFile.ChangeStatus.RENAMED, - newClassName, oldClassName, classType); - } - - /** 处理 M 状态:同路径下对比 AST 类名判断是否重命名 */ - private ChangedClassFile buildModified(String path, String oldSha, String newSha, - String fallbackName, ClassType classType) throws IOException { - String oldSource = readFileAtCommit(oldSha, path); - String newSource = readFileAtCommit(newSha, path); - if (newSource == null || newSource.isBlank()) { - newSource = readFileAtHead(path); - } - String oldClassName = classDeclParser.resolveClassName(oldSource, fallbackName); - String newClassName = classDeclParser.resolveClassName(newSource, fallbackName); - - if (oldClassName.equals(newClassName)) { - return new ChangedClassFile(path, ChangedClassFile.ChangeStatus.MODIFIED, - newClassName, classType); - } - return new ChangedClassFile(path, path, ChangedClassFile.ChangeStatus.RENAMED, - newClassName, oldClassName, classType); - } - - /** 同目录 D+A 配对为 RENAMED(Git 未显式标记 R 时) */ - private void pairDeleteAndAdd(List deletions, - Map additionsByParent, - String oldSha, - List result) throws IOException { - List unpaired = new ArrayList<>(); - - for (ChangedClassFile deleted : deletions) { - String parent = parentDir(deleted.getRelativePath()); - PendingAdd pending = additionsByParent.get(parent); - if (pending == null || pending.isEmpty()) { - unpaired.add(deleted); - continue; - } - - PendingAdd.Candidate candidate = pending.poll(deleted.getClassType()); - if (candidate == null) { - unpaired.add(deleted); - continue; - } - - String oldSource = readFileAtCommit(oldSha, deleted.getRelativePath()); - String oldClassName = classDeclParser.resolveClassName(oldSource, deleted.getClassName()); - result.add(new ChangedClassFile(candidate.path(), deleted.getRelativePath(), - ChangedClassFile.ChangeStatus.RENAMED, - candidate.className(), oldClassName, deleted.getClassType())); - } - - deletions.clear(); - deletions.addAll(unpaired); - } - - /** 取路径父目录,用于 D+A 配对 */ - private static String parentDir(String path) { - int idx = path.lastIndexOf('/'); - return idx >= 0 ? path.substring(0, idx) : ""; - } - - /** 读取指定 commit 下的文件内容 */ - public String readFileAtCommit(String commitSha, String relativePath) throws IOException { - List lines = runGit("show", commitSha + ":" + relativePath); - if (lines.isEmpty()) { - return ""; - } - if (lines.size() == 1 && lines.get(0).startsWith("fatal:")) { - return ""; - } - return String.join("\n", lines); - } - - /** 读取工作区 HEAD 文件(commit 中缺失时的回退) */ - public String readFileAtHead(String relativePath) throws IOException { - Path file = repoRoot.resolve(relativePath); - if (!Files.exists(file)) { - return null; - } - return Files.readString(file, StandardCharsets.UTF_8); - } - - /** 两次提交间变更文件路径列表(--name-only) */ - public List diffNameOnly(String oldSha, String newSha) throws IOException { - return runGit("diff", "--name-only", oldSha, newSha); - } - - /** 在 repoRoot 下执行 git 命令并返回 stdout 行 */ - private List runGit(String... args) throws IOException { - String[] command = new String[args.length + 3]; - command[0] = "git"; - command[1] = "-C"; - command[2] = repoRoot.toString(); - System.arraycopy(args, 0, command, 3, args.length); - - ProcessBuilder builder = new ProcessBuilder(command); - builder.redirectErrorStream(true); - Process process = builder.start(); - - List output = new ArrayList<>(); - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - output.add(line); - } - } - - try { - int exitCode = process.waitFor(); - if (exitCode != 0 && !isBenignGitShowFailure(args, output)) { - throw new IOException("git 命令失败: " + String.join(" ", command) - + "\n" + String.join("\n", output)); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("git 命令被中断", e); - } - return output; - } - - /** git show 文件不存在等情况视为可忽略 */ - private boolean isBenignGitShowFailure(String[] args, List output) { - if (args.length > 0 && "show".equals(args[0])) { - String joined = String.join("\n", output).toLowerCase(Locale.ROOT); - return joined.contains("exists on disk") || joined.contains("bad object") - || joined.contains("path") && joined.contains("does not exist"); - } - return false; - } - - /** 统一路径分隔符为 / */ - private String normalizePath(String path) { - return path.replace("\\", "/"); - } - - /** 同目录新增文件缓冲,供 D+A 配对 */ - private static final class PendingAdd { - private final Map> byType = new HashMap<>(); - - void add(String path, String className, ClassType classType, String source) { - byType.computeIfAbsent(classType, k -> new ArrayList<>()) - .add(new Candidate(path, className)); - } - - boolean isEmpty() { - return byType.values().stream().allMatch(List::isEmpty); - } - - /** 按类型取出一个候选新增文件 */ - Candidate poll(ClassType classType) { - List list = byType.get(classType); - if (list == null || list.isEmpty()) { - return null; - } - return list.remove(0); - } - - private static final class Candidate { - private final String path; - private final String className; - - private Candidate(String path, String className) { - this.path = path; - this.className = className; - } - - private String path() { - return path; - } - - private String className() { - return className; - } - } - } -} diff --git a/src/main/java/com/codechecker/model/ApiEndpoint.java b/src/main/java/com/codechecker/model/ApiEndpoint.java deleted file mode 100644 index d07db6e..0000000 --- a/src/main/java/com/codechecker/model/ApiEndpoint.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.codechecker.model; - -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * 索引中的 HTTP/Feign 接口:方法、URI、入参/返回类型简单名。 - */ -public class ApiEndpoint { - private final String httpMethod; - private final String uri; - private final String sourceFile; - private final Set paramTypes; - private final Set returnTypes; - - public ApiEndpoint(String httpMethod, String uri, String sourceFile, - Set paramTypes, Set returnTypes) { - this.httpMethod = httpMethod; - this.uri = uri; - this.sourceFile = sourceFile; - this.paramTypes = paramTypes == null ? Set.of() : new LinkedHashSet<>(paramTypes); - this.returnTypes = returnTypes == null ? Set.of() : new LinkedHashSet<>(returnTypes); - } - - public String getHttpMethod() { - return httpMethod; - } - - public String getUri() { - return uri; - } - - /** 定义该接口的 Java 源文件相对路径 */ - public String getSourceFile() { - return sourceFile; - } - - /** 入参涉及的类型简单名集合 */ - public Set getParamTypes() { - return paramTypes; - } - - /** 返回值涉及的类型简单名集合(已剥离泛型包装) */ - public Set getReturnTypes() { - return returnTypes; - } - - /** 去重用键:METHOD + URI */ - public String endpointKey() { - return httpMethod + " " + uri; - } - - /** 通知展示行:GET /api/foo */ - public String displayLine() { - return httpMethod + " " + uri; - } -} diff --git a/src/main/java/com/codechecker/model/ChangedClassFile.java b/src/main/java/com/codechecker/model/ChangedClassFile.java deleted file mode 100644 index 09d38a3..0000000 --- a/src/main/java/com/codechecker/model/ChangedClassFile.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.codechecker.model; - -/** - * Git 扫描得到的单个 Java 模型类变更记录。 - */ -public class ChangedClassFile { - /** Git diff 状态:修改 / 删除 / 重命名 */ - public enum ChangeStatus { - MODIFIED, DELETED, RENAMED - } - - private final String relativePath; - private final String oldRelativePath; - private final ChangeStatus status; - private final String className; - private final String oldClassName; - private final ClassType classType; - - /** 修改或删除(无路径变化) */ - public ChangedClassFile(String relativePath, ChangeStatus status, String className, ClassType classType) { - this(relativePath, null, status, className, null, classType); - } - - /** 重命名或同路径类名变更 */ - public ChangedClassFile(String relativePath, String oldRelativePath, ChangeStatus status, - String className, String oldClassName, ClassType classType) { - this.relativePath = relativePath; - this.oldRelativePath = oldRelativePath; - this.status = status; - this.className = className; - this.oldClassName = oldClassName; - this.classType = classType; - } - - /** 新提交中的相对路径 */ - public String getRelativePath() { - return relativePath; - } - - /** 旧提交中的相对路径,未变路径则为 null */ - public String getOldRelativePath() { - return oldRelativePath; - } - - /** 读取旧版本源码时使用的路径 */ - public String pathForOldCommit() { - return oldRelativePath != null ? oldRelativePath : relativePath; - } - - public ChangeStatus getStatus() { - return status; - } - - /** 当前简单类名 */ - public String getClassName() { - return className; - } - - /** 重命名前简单类名 */ - public String getOldClassName() { - return oldClassName; - } - - public ClassType getClassType() { - return classType; - } -} diff --git a/src/main/java/com/codechecker/model/ClassChangeKind.java b/src/main/java/com/codechecker/model/ClassChangeKind.java deleted file mode 100644 index b3c1fbd..0000000 --- a/src/main/java/com/codechecker/model/ClassChangeKind.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.codechecker.model; - -/** - * 单次类变更的类型,决定通知内容与影响分析策略。 - */ -public enum ClassChangeKind { - /** 文件已删除 */ - DELETED, - /** 仅字段变更 */ - FIELDS_ONLY, - /** 仅类名变更,字段不变 */ - RENAME_ONLY, - /** 类名与字段同时变更 */ - RENAME_AND_FIELDS -} diff --git a/src/main/java/com/codechecker/model/ClassChangeReport.java b/src/main/java/com/codechecker/model/ClassChangeReport.java deleted file mode 100644 index 195632b..0000000 --- a/src/main/java/com/codechecker/model/ClassChangeReport.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.codechecker.model; - -import java.util.ArrayList; -import java.util.List; - -/** - * 单次类变更的完整报告:变更类型、字段 diff、接口/转换影响,供通知渲染。 - */ -public class ClassChangeReport { - private final String className; - private final String oldClassName; - private final ClassType classType; - private final ClassChangeKind changeKind; - private final String sourceFile; - private final String classDescription; - private final List fieldChanges = new ArrayList<>(); - private final List inputImpactEndpoints = new ArrayList<>(); - private final List conversionEntities = new ArrayList<>(); - private final List frontendImpactEndpoints = new ArrayList<>(); - private final boolean conversionCheckEnabled; - private List objectRoleLabels = List.of(); - - public ClassChangeReport(String className, String oldClassName, ClassType classType, - ClassChangeKind changeKind, String sourceFile, - boolean conversionCheckEnabled, String classDescription) { - this.className = className; - this.oldClassName = oldClassName; - this.classType = classType; - this.changeKind = changeKind; - this.sourceFile = sourceFile; - this.conversionCheckEnabled = conversionCheckEnabled; - this.classDescription = classDescription == null ? "" : classDescription.trim(); - } - - /** 当前(新)简单类名 */ - public String getClassName() { - return className; - } - - /** 重命名前的简单类名,未重命名则为 null */ - public String getOldClassName() { - return oldClassName; - } - - /** 是否发生类名变更 */ - public boolean isRenamed() { - return oldClassName != null && !oldClassName.equals(className); - } - - /** 是否仅类名变更、字段无变化 */ - public boolean isRenameOnly() { - return changeKind == ClassChangeKind.RENAME_ONLY; - } - - public ClassType getClassType() { - return classType; - } - - public ClassChangeKind getChangeKind() { - return changeKind; - } - - /** Git 相对路径,通知「文件路径」展示用 */ - public String getSourceFile() { - return sourceFile; - } - - /** 类级中文说明(@Schema / 类 Javadoc),无则空串 */ - public String getClassDescription() { - return classDescription; - } - - /** 是否整文件删除 */ - public boolean isDeleted() { - return changeKind == ClassChangeKind.DELETED; - } - - public List getFieldChanges() { - return fieldChanges; - } - - /** 入参引用该类的接口(request 影响) */ - public List getInputImpactEndpoints() { - return inputImpactEndpoints; - } - - /** Dto→Entity 转换目标类名列表 */ - public List getConversionEntities() { - return conversionEntities; - } - - /** 返回值引用该类的接口(response 影响) */ - public List getFrontendImpactEndpoints() { - return frontendImpactEndpoints; - } - - /** 是否启用类转换检测 */ - public boolean isConversionCheckEnabled() { - return conversionCheckEnabled; - } - - /** 对象角色标签(如「嵌套对象」「顶层对象」),仅 Dto/Vo 且存在嵌套时非空 */ - public List getObjectRoleLabels() { - return objectRoleLabels; - } - - public void setObjectRoleLabels(List labels) { - this.objectRoleLabels = labels == null || labels.isEmpty() ? List.of() : List.copyOf(labels); - } - - /** 追加一条字段变更 */ - public void addFieldChange(FieldChange change) { - fieldChanges.add(change); - } - - /** 追加 request 影响接口(按 endpointKey 去重) */ - public void addInputImpact(ApiEndpoint endpoint) { - if (inputImpactEndpoints.stream().noneMatch(e -> e.endpointKey().equals(endpoint.endpointKey()))) { - inputImpactEndpoints.add(endpoint); - } - } - - /** 追加关联 Entity 类名(去重) */ - public void addConversionEntity(String entityName) { - if (!conversionEntities.contains(entityName)) { - conversionEntities.add(entityName); - } - } - - /** 追加 response 影响接口(按 endpointKey 去重) */ - public void addFrontendImpact(ApiEndpoint endpoint) { - if (frontendImpactEndpoints.stream().noneMatch(e -> e.endpointKey().equals(endpoint.endpointKey()))) { - frontendImpactEndpoints.add(endpoint); - } - } -} diff --git a/src/main/java/com/codechecker/model/ClassType.java b/src/main/java/com/codechecker/model/ClassType.java deleted file mode 100644 index ee2fa8f..0000000 --- a/src/main/java/com/codechecker/model/ClassType.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.codechecker.model; - -/** - * 目标模型类后缀类型,决定通知模版中展示哪些影响段落。 - */ -public enum ClassType { - DTO("Dto"), - VO("Vo"), - ENTITY("Entity"), - MODEL("Model"); - - private final String label; - - ClassType(String label) { - this.label = label; - } - - /** 通知中展示的类型标签 */ - public String getLabel() { - return label; - } - - /** 根据简单类名后缀识别类型,不匹配则 null */ - public static ClassType fromClassName(String className) { - if (className.endsWith("Dto")) { - return DTO; - } - if (className.endsWith("VO")) { - return VO; - } - if (className.endsWith("Vo")) { - return VO; - } - if (className.endsWith("Entity")) { - return ENTITY; - } - if (className.endsWith("Model")) { - return MODEL; - } - return null; - } - - /** 判断类名是否属于当前类型 */ - public boolean isTargetSuffix(String className) { - return fromClassName(className) == this; - } -} diff --git a/src/main/java/com/codechecker/model/FieldChange.java b/src/main/java/com/codechecker/model/FieldChange.java deleted file mode 100644 index 0870d1f..0000000 --- a/src/main/java/com/codechecker/model/FieldChange.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.codechecker.model; - -/** - * 字段级 diff 结果,用于通知中的 [新增]/[删除]/[修改]/[重命名] 行。 - */ -public class FieldChange { - /** 字段变更种类 */ - public enum ChangeKind { - ADDED, REMOVED, MODIFIED, RENAMED - } - - private final ChangeKind kind; - private final String fieldName; - private final String oldFieldName; - private final String description; - private final String oldType; - private final String newType; - private final String oldDescription; - private final String detail; - - private FieldChange(ChangeKind kind, String fieldName, String oldFieldName, String description, - String oldType, String newType, String oldDescription, String detail) { - this.kind = kind; - this.fieldName = fieldName; - this.oldFieldName = oldFieldName; - this.description = description; - this.oldType = oldType; - this.newType = newType; - this.oldDescription = oldDescription; - this.detail = detail; - } - - /** 构造新增字段变更 */ - public static FieldChange added(FieldInfo field) { - return new FieldChange(ChangeKind.ADDED, field.getName(), null, field.getDescription(), - null, field.getType(), null, null); - } - - /** 构造删除字段变更 */ - public static FieldChange removed(FieldInfo field) { - return new FieldChange(ChangeKind.REMOVED, field.getName(), null, field.getDescription(), - field.getType(), null, field.getDescription(), null); - } - - /** 构造修改字段变更,detail 通常为类型变化描述 */ - public static FieldChange modified(FieldInfo oldField, FieldInfo newField, String detail) { - return new FieldChange(ChangeKind.MODIFIED, newField.getName(), null, newField.getDescription(), - oldField.getType(), newField.getType(), oldField.getDescription(), detail); - } - - /** 构造字段重命名;类型变化时 detail 为 oldType → newType */ - public static FieldChange renamed(FieldInfo oldField, FieldInfo newField) { - String typeDetail = oldField.getType().equals(newField.getType()) - ? null - : oldField.getType() + " → " + newField.getType(); - return new FieldChange(ChangeKind.RENAMED, newField.getName(), oldField.getName(), - newField.getDescription(), oldField.getType(), newField.getType(), - oldField.getDescription(), typeDetail); - } - - public ChangeKind getKind() { - return kind; - } - - public String getFieldName() { - return fieldName; - } - - /** 重命名前的字段名,仅 RENAMED 时有值 */ - public String getOldFieldName() { - return oldFieldName; - } - - /** 变更后的字段说明(通知「说明」段) */ - public String getDescription() { - return description; - } - - public String getOldType() { - return oldType; - } - - public String getNewType() { - return newType; - } - - public String getOldDescription() { - return oldDescription; - } - - /** 结构性变更详情,重命名时为类型变化描述 */ - public String getDetail() { - return detail; - } -} diff --git a/src/main/java/com/codechecker/model/FieldInfo.java b/src/main/java/com/codechecker/model/FieldInfo.java deleted file mode 100644 index 00d8055..0000000 --- a/src/main/java/com/codechecker/model/FieldInfo.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.codechecker.model; - -import java.util.Objects; - -/** - * 解析后的单个字段:名称、类型、业务说明(Schema/注释)。 - */ -public class FieldInfo { - private final String name; - private final String type; - private final String description; - - public FieldInfo(String name, String type, String description) { - this.name = name; - this.type = type; - this.description = description == null ? "" : description; - } - - /** 字段名 */ - public String getName() { - return name; - } - - /** 字段类型(简单名) */ - public String getType() { - return type; - } - - /** 字段说明文案 */ - public String getDescription() { - return description; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof FieldInfo)) { - return false; - } - FieldInfo other = (FieldInfo) o; - return Objects.equals(name, other.name) - && Objects.equals(type, other.type) - && Objects.equals(description, other.description); - } - - @Override - public int hashCode() { - return Objects.hash(name, type, description); - } -} diff --git a/src/main/java/com/codechecker/notify/OverlapNotificationFilter.java b/src/main/java/com/codechecker/notify/OverlapNotificationFilter.java deleted file mode 100644 index 948dd7c..0000000 --- a/src/main/java/com/codechecker/notify/OverlapNotificationFilter.java +++ /dev/null @@ -1,218 +0,0 @@ -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; -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 classReports; - private final List apiReports; - - public FilterResult(List classReports, List apiReports) { - this.classReports = classReports; - this.apiReports = apiReports; - } - - public List classReports() { - return classReports; - } - - public List apiReports() { - return apiReports; - } - } - - public static FilterResult apply(List classReports, - List apiReports, - DtoOverlapMode mode, - DtoNestIndex nestIndex) { - if (mode == DtoOverlapMode.BOTH) { - return new FilterResult(classReports, apiReports); - } - Set overlapKeys = buildOverlapKeys(classReports, nestIndex); - if (overlapKeys.isEmpty()) { - return new FilterResult(classReports, apiReports); - } - if (mode == DtoOverlapMode.CLASS_ONLY) { - return new FilterResult(classReports, filterApiReports(apiReports, overlapKeys)); - } - Set apiOverlapKeys = buildApiOverlapKeys(apiReports); - return new FilterResult(filterClassReportsForApiOnly(classReports, apiOverlapKeys, nestIndex), apiReports); - } - - /** - * 重叠键使用 @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) { - continue; - } - if (!hasDtoFieldChanges(report)) { - continue; - } - Set bodyRoots = requestBodyRoots(report, nestIndex); - for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) { - 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<>(); - for (EndpointChangeReport report : apiReports) { - if (!matchesOverlap(report, overlapKeys)) { - kept.add(report); - } - } - return kept; - } - - private static List filterClassReportsForApiOnly(List classReports, - Set apiOverlapKeys, - DtoNestIndex nestIndex) { - List kept = new ArrayList<>(); - for (ClassChangeReport report : classReports) { - if (!shouldSuppressClassForApiOnly(report, apiOverlapKeys, nestIndex)) { - kept.add(report); - } - } - return kept; - } - - private static Set buildApiOverlapKeys(List apiReports) { - Set 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 apiOverlapKeys, - DtoNestIndex nestIndex) { - if (report.getClassType() != ClassType.DTO || !hasDtoFieldChanges(report)) { - return false; - } - Set bodyRoots = requestBodyRoots(report, nestIndex); - for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) { - for (String rootDto : bodyRoots) { - if (apiOverlapKeys.contains(new OverlapKey(rootDto, endpoint.endpointKey()))) { - return true; - } - } - } - return false; - } - - private static boolean matchesOverlap(EndpointChangeReport report, Set 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 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(); - } - } -} diff --git a/src/main/java/com/codechecker/notify/WeComNotifier.java b/src/main/java/com/codechecker/notify/WeComNotifier.java deleted file mode 100644 index e5085fa..0000000 --- a/src/main/java/com/codechecker/notify/WeComNotifier.java +++ /dev/null @@ -1,394 +0,0 @@ -package com.codechecker.notify; - -import com.codechecker.model.ApiEndpoint; -import com.codechecker.model.ClassChangeReport; -import com.codechecker.model.ClassType; -import com.codechecker.model.FieldChange; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.TimeUnit; - -/** - * 将 ClassChangeReport 渲染为企业微信 Markdown 并发送(或仅日志输出)。 - *

- * 使用 webhook {@code markdown}(v1):引用块 + 换行排版,三色 font(info/comment/warning)。 - * v1 不支持无序列表,各项以 {@code >标签: 值} 分行展示(冒号后两空格)。 - */ -public class WeComNotifier { - private static final int MAX_LENGTH = 3800; - private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); - - private final OkHttpClient client = new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build(); - - /** 逐条发送企微通知,返回成功条数 */ - public int sendAll(String webhookUrl, List reports, String modifier, String modifyTime) { - if (reports == null || reports.isEmpty()) { - System.out.println("无类变更,不发送到企业微信"); - return 0; - } - - int sent = 0; - for (ClassChangeReport report : reports) { - String markdown = buildMarkdown(report, modifier, modifyTime); - if (postMarkdown(webhookUrl, markdown)) { - sent++; - System.out.println("已发送类变更通知: " + report.getClassName()); - } - } - if (sent > 0) { - System.out.println("总共发送 " + sent + " 条类变更通知到企业微信"); - } - return sent; - } - - /** 企微关闭时打印 Markdown 到控制台 */ - public void logAll(List reports, String modifier, String modifyTime) { - if (reports == null || reports.isEmpty()) { - System.out.println("无类变更,无日志输出"); - return; - } - - System.out.println("企业微信通知已关闭(wecom.enabled=false),以下结果仅输出到日志:"); - for (int i = 0; i < reports.size(); i++) { - ClassChangeReport report = reports.get(i); - System.out.println("========== 类变更 [" + (i + 1) + "/" + reports.size() - + "]: " + report.getClassName() + " =========="); - System.out.println(buildMarkdown(report, modifier, modifyTime)); - System.out.println("========== 结束 =========="); - } - System.out.println("共 " + reports.size() + " 条类变更结果(未发送到企业微信)"); - } - - /** 组装完整 Markdown 正文(引用块 + 换行,每项独立一行) */ - public String buildMarkdown(ClassChangeReport report, String modifier, String modifyTime) { - StringBuilder sb = new StringBuilder(); - sb.append("# 【类变更通知】").append("\n\n"); - appendHeader(sb, report, modifier, modifyTime); - - sb.append("\n## 【对象变更细节】").append("\n\n"); - appendChangeDetails(sb, report); - - sb.append("\n## 【影响范围】").append("\n\n"); - appendImpactSections(sb, report); - return truncate(sb.toString()); - } - - /** 变更对象行:类名(绿)+ 可选中文说明 + 嵌套角色标签(灰,整行加粗) */ - private String formatChangeTarget(ClassChangeReport report) { - StringBuilder line = new StringBuilder(colorInfo(safe(report.getClassName()))); - String description = report.getClassDescription(); - if (description != null && !description.isBlank()) { - line.append("(").append(colorComment(description)).append(")"); - } - for (String role : report.getObjectRoleLabels()) { - line.append("(").append(colorComment(role)).append(")"); - } - return line.toString(); - } - - /** 头部元信息,每项一行引用(加粗) */ - private void appendHeader(StringBuilder sb, ClassChangeReport report, - String modifier, String modifyTime) { - sb.append(quoteKvBold("变更对象", formatChangeTarget(report))).append("\n"); - sb.append(quoteKvBold("修改人", colorComment(modifier))).append("\n"); - sb.append(quoteKvBold("时间", colorComment(modifyTime))).append("\n"); - sb.append(quoteKvBold("路径", colorComment(report.getSourceFile()))).append("\n"); - } - - /** 渲染删除 / 重命名 / 字段变更 */ - private void appendChangeDetails(StringBuilder sb, ClassChangeReport report) { - if (report.isDeleted()) { - sb.append(quoteLine(colorWarning("[已删除]") + " " - + colorComment("该类文件已被移除"))).append("\n"); - return; - } - - if (report.isRenamed()) { - sb.append(quoteLine(colorWarning("[类名变更]") + " " - + colorComment(safe(report.getOldClassName())) + " → " - + colorInfo(safe(report.getClassName())))).append("\n"); - } - - if (report.isRenameOnly()) { - sb.append(quoteLine(colorComment("字段无变化"))).append("\n"); - return; - } - - if (!report.getFieldChanges().isEmpty()) { - int count = report.getFieldChanges().size(); - sb.append(quoteLine("**共 " + colorWarning(String.valueOf(count)) + " 项变更**")) - .append("\n\n"); - for (int i = 0; i < report.getFieldChanges().size(); i++) { - if (i > 0) { - sb.append("\n"); - } - sb.append(formatFieldChange(report.getFieldChanges().get(i))); - } - sb.append("\n"); - } - } - - /** 按类类型选择影响段落 */ - private void appendImpactSections(StringBuilder sb, ClassChangeReport report) { - appendImpactByType(sb, report); - } - - /** Dto/Vo 均展示 request + response(二者可能交叉);Entity/Model 仅类转换 */ - private void appendImpactByType(StringBuilder sb, ClassChangeReport report) { - switch (report.getClassType()) { - case DTO: - case VO: - appendSectionIfNeeded(sb, report, true, true, true); - break; - case ENTITY: - case MODEL: - appendSectionIfNeeded(sb, report, false, false, true); - break; - default: - appendSectionIfNeeded(sb, report, true, true, true); - } - } - - /** 按需追加 request / response / 类转换三个小节 */ - private void appendSectionIfNeeded(StringBuilder sb, ClassChangeReport report, - boolean showRequest, boolean showResponse, boolean showConversion) { - if (showRequest) { - sb.append("### 影响 request 接口").append("\n"); - appendEndpointList(sb, report.getInputImpactEndpoints()); - sb.append("\n"); - } - if (showResponse) { - sb.append("### 影响 response 接口").append("\n"); - appendEndpointList(sb, report.getFrontendImpactEndpoints()); - sb.append("\n"); - } - if (showConversion && report.isConversionCheckEnabled()) { - sb.append("### 类转换影响").append("\n"); - appendConversionList(sb, report); - } - } - - /** 渲染关联 Entity,每项一行 */ - private void appendConversionList(StringBuilder sb, ClassChangeReport report) { - if (report.getConversionEntities().isEmpty()) { - sb.append(quoteLine(colorComment("无"))).append("\n"); - return; - } - for (String entity : report.getConversionEntities()) { - sb.append(quoteKv("Entity", colorInfo(safe(entity)))).append("\n"); - } - } - - /** 渲染接口,每项一行 */ - private void appendEndpointList(StringBuilder sb, List endpoints) { - if (endpoints == null || endpoints.isEmpty()) { - sb.append(quoteLine(colorComment("无"))).append("\n"); - return; - } - for (ApiEndpoint endpoint : endpoints) { - sb.append(formatEndpointLine(endpoint)).append("\n"); - } - } - - /** 接口行:> POST `/path` */ - private String formatEndpointLine(ApiEndpoint endpoint) { - String line = endpoint.displayLine(); - int space = line.indexOf(' '); - if (space > 0) { - String method = line.substring(0, space).trim(); - String path = line.substring(space).trim(); - return quoteLine(colorInfo(method) + " " + inlineCode(path)); - } - return quoteLine(inlineCode(safe(line))); - } - - /** 单条字段变更:标签、说明、类型合并为一行,字段间空行分隔 */ - private String formatFieldChange(FieldChange change) { - String fieldName = inlineCode(safe(change.getFieldName())); - String desc = change.getDescription() == null ? "" : change.getDescription(); - String descPart = desc.isBlank() - ? colorComment("(无说明)") - : colorComment(desc); - - switch (change.getKind()) { - case ADDED: { - StringBuilder line = new StringBuilder(); - line.append(tagAdded()).append(" ").append(fieldName) - .append(" 说明: ").append(descPart); - appendFieldType(line, change); - return quoteLine(line.toString()); - } - case REMOVED: { - StringBuilder line = new StringBuilder(); - line.append(tagRemoved()).append(" ").append(fieldName) - .append(" 说明: ").append(descPart); - appendFieldType(line, change); - return quoteLine(line.toString()); - } - case RENAMED: { - StringBuilder renameLine = new StringBuilder(); - renameLine.append(tagRenamed()).append(" ") - .append(colorComment(safe(change.getOldFieldName()))).append(" → ") - .append(colorInfo(safe(change.getFieldName()))) - .append(" 说明: ").append(descPart); - appendFieldType(renameLine, change); - return quoteLine(renameLine.toString()); - } - case MODIFIED: - default: { - StringBuilder line = new StringBuilder(); - line.append(tagModified()).append(" ").append(fieldName) - .append(" 说明: ").append(descPart); - appendFieldType(line, change); - return quoteLine(line.toString()); - } - } - } - - /** 追加字段类型:新增/重命名(仅改名)用 info,删除用 warning,修改/重命名(改类型)用 old → new */ - private void appendFieldType(StringBuilder line, FieldChange change) { - if (change.getKind() == FieldChange.ChangeKind.RENAMED - || change.getKind() == FieldChange.ChangeKind.MODIFIED) { - String typeDetail = change.getDetail(); - if (typeDetail != null && !typeDetail.isBlank()) { - line.append(" 类型: ").append(formatTypeChange(typeDetail)); - return; - } - } - String singleType = change.getKind() == FieldChange.ChangeKind.REMOVED - ? change.getOldType() - : change.getNewType(); - if (singleType == null || singleType.isBlank()) { - return; - } - line.append(" 类型: "); - if (change.getKind() == FieldChange.ChangeKind.REMOVED) { - line.append(colorWarning(singleType)); - } else { - line.append(colorInfo(singleType)); - } - } - - /** 类型变化:旧 warning → 新 info;泛型尖括号原样展示,不做 HTML 转义 */ - private String formatTypeChange(String detail) { - int arrow = detail.indexOf(" → "); - if (arrow < 0) { - return colorWarning(detail); - } - String oldType = detail.substring(0, arrow).trim(); - String newType = detail.substring(arrow + 3).trim(); - return colorWarning(oldType) + " → " + colorInfo(newType); - } - - private String tagAdded() { - return colorInfo("[新增]"); - } - - private String tagRemoved() { - return colorWarning("[删除]"); - } - - private String tagModified() { - return colorWarning("[修改]"); - } - - private String tagRenamed() { - return colorWarning("[重命名]"); - } - - /** 引用行:{@code >标签: 值}(冒号后两空格) */ - private String quoteKv(String key, String value) { - return "> " + key + ": " + value; - } - - /** 加粗引用行:用于类变更通知头部 */ - private String quoteKvBold(String key, String value) { - return "> **" + key + ": " + value + "**"; - } - - /** 纯引用行 */ - private String quoteLine(String content) { - return "> " + content; - } - - /** 行内代码 */ - private String inlineCode(String text) { - return "`" + text.replace("`", "'") + "`"; - } - - private String colorInfo(String text) { - return "" + text + ""; - } - - private String colorComment(String text) { - return "" + safe(text) + ""; - } - - private String colorWarning(String text) { - return "" + text + ""; - } - - /** 转义 HTML 特殊字符,避免破坏 font 标签 */ - private String safe(String text) { - if (text == null) { - return ""; - } - return text.replace("&", "&").replace("<", "<").replace(">", ">"); - } - - /** POST 企微 Webhook(markdown v1) */ - private boolean postMarkdown(String webhookUrl, String content) { - if (webhookUrl == null || webhookUrl.isBlank() || webhookUrl.contains("YOUR_WECOM")) { - System.out.println("[警告] 未配置有效的企业微信 Webhook URL"); - System.out.println("--- 通知预览 ---"); - System.out.println(content.length() > 1000 ? content.substring(0, 1000) : content); - return false; - } - - String payload = "{\"msgtype\":\"markdown\",\"markdown\":{\"content\":" - + jsonEscape(content) + "}}"; - Request request = new Request.Builder() - .url(webhookUrl) - .post(RequestBody.create(payload, JSON)) - .build(); - try (Response response = client.newCall(request).execute()) { - if (response.isSuccessful() && response.body() != null) { - String body = response.body().string(); - return body.contains("\"errcode\":0"); - } - System.out.println("[错误] 企微返回异常: " + response.code() - + (response.body() != null ? " " + response.body().string() : "")); - return false; - } catch (IOException e) { - System.out.println("[错误] 发送企微消息失败: " + e.getMessage()); - return false; - } - } - - /** 超长消息截断(企微上限 4096 字节 UTF-8) */ - private String truncate(String text) { - if (text.length() <= MAX_LENGTH) { - return text; - } - return text.substring(0, MAX_LENGTH) + "\n\n... 消息过长,已截断"; - } - - /** JSON 字符串转义 */ - private String jsonEscape(String text) { - String escaped = text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", ""); - return "\"" + escaped + "\""; - } -} diff --git a/src/main/java/com/codechecker/parser/ClassDeclParser.java b/src/main/java/com/codechecker/parser/ClassDeclParser.java deleted file mode 100644 index eafa1b2..0000000 --- a/src/main/java/com/codechecker/parser/ClassDeclParser.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.codechecker.parser; - -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.ast.CompilationUnit; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.comments.JavadocComment; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; - -import java.util.Optional; - -/** - * 从 Java 源文件路径或 AST 解析类名(简单名 / 全限定名)及类级中文说明。 - */ -public class ClassDeclParser { - - /** - * 从源码 AST 提取主类名;解析失败或未找到时回退为路径推导的类名。 - */ - public String resolveClassName(String source, String fallbackFromPath) { - if (source == null || source.isBlank()) { - return fallbackFromPath; - } - try { - CompilationUnit cu = StaticJavaParser.parse(source); - for (TypeDeclaration type : cu.getTypes()) { - if (type instanceof ClassOrInterfaceDeclaration) { - return type.getNameAsString(); - } - } - } catch (Exception ignored) { - // 回退路径类名 - } - return fallbackFromPath; - } - - /** 从 .java 路径提取文件名(无扩展名)作为类名 */ - public static String classNameFromPath(String path) { - String fileName = path.substring(path.lastIndexOf('/') + 1); - if (!fileName.endsWith(".java")) { - return fileName; - } - return fileName.substring(0, fileName.length() - 5); - } - - /** - * 全限定类名:package + 类名;源码无 package 时从文件路径推断。 - */ - public String resolveQualifiedClassName(String source, String relativePath, String fallbackClassName) { - String simpleName = resolveClassName(source, fallbackClassName); - if (source != null && !source.isBlank()) { - try { - CompilationUnit cu = StaticJavaParser.parse(source); - String packageName = cu.getPackageDeclaration() - .map(p -> p.getNameAsString()) - .orElse(""); - if (!packageName.isBlank()) { - return packageName + "." + simpleName; - } - } catch (Exception ignored) { - // 回退路径推断 - } - } - return inferQualifiedFromPath(relativePath, simpleName); - } - - /** - * 提取类级中文说明:@Schema(description/title) > 类 Javadoc 首段。 - */ - public String extractClassDescription(String source, String expectedClassName) { - if (source == null || source.isBlank()) { - return ""; - } - try { - CompilationUnit cu = StaticJavaParser.parse(source); - ClassOrInterfaceDeclaration classDecl = findClass(cu, expectedClassName); - if (classDecl == null) { - return ""; - } - String fromSchema = readSchemaDescription(classDecl); - if (!fromSchema.isEmpty()) { - return fromSchema; - } - return extractClassJavadoc(classDecl); - } catch (Exception ignored) { - return ""; - } - } - - private ClassOrInterfaceDeclaration findClass(CompilationUnit cu, String expectedClassName) { - if (expectedClassName != null && !expectedClassName.isBlank()) { - for (TypeDeclaration type : cu.getTypes()) { - if (type instanceof ClassOrInterfaceDeclaration) { - ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) type; - if (classDecl.getNameAsString().equals(expectedClassName)) { - return classDecl; - } - } - } - } - for (TypeDeclaration type : cu.getTypes()) { - if (type instanceof ClassOrInterfaceDeclaration) { - return (ClassOrInterfaceDeclaration) type; - } - } - return null; - } - - private String readSchemaDescription(ClassOrInterfaceDeclaration classDecl) { - for (AnnotationExpr annotation : classDecl.getAnnotations()) { - if (!"Schema".equals(annotation.getNameAsString())) { - continue; - } - String description = readAnnotationStringValue(annotation, "description"); - if (!description.isEmpty()) { - return description; - } - String title = readAnnotationStringValue(annotation, "title"); - if (!title.isEmpty()) { - return title; - } - } - return ""; - } - - private String readAnnotationStringValue(AnnotationExpr annotation, String attributeName) { - if (annotation.isNormalAnnotationExpr()) { - NormalAnnotationExpr normal = annotation.asNormalAnnotationExpr(); - for (var pair : normal.getPairs()) { - if (pair.getNameAsString().equals(attributeName)) { - return literalString(pair.getValue()); - } - } - return ""; - } - if (annotation.isSingleMemberAnnotationExpr()) { - SingleMemberAnnotationExpr single = annotation.asSingleMemberAnnotationExpr(); - if ("value".equals(attributeName) || "description".equals(attributeName)) { - return literalString(single.getMemberValue()); - } - } - return ""; - } - - private String literalString(Expression expression) { - if (expression.isStringLiteralExpr()) { - return expression.asStringLiteralExpr().getValue().trim(); - } - return ""; - } - - private String extractClassJavadoc(ClassOrInterfaceDeclaration classDecl) { - Optional javadoc = classDecl.getJavadocComment(); - if (javadoc.isEmpty()) { - return ""; - } - String text = javadoc.get().parse().getDescription().toText(); - return text == null ? "" : text.trim().replaceAll("\\s+", " "); - } - - /** 从 src/main/java/ 后的路径推断 package.className */ - public static String inferQualifiedFromPath(String relativePath, String className) { - if (relativePath == null || relativePath.isBlank()) { - return className; - } - String normalized = relativePath.replace('\\', '/'); - String marker = "src/main/java/"; - int idx = normalized.indexOf(marker); - if (idx < 0) { - return className; - } - String subPath = normalized.substring(idx + marker.length()); - int lastSlash = subPath.lastIndexOf('/'); - if (lastSlash <= 0) { - return className; - } - String packageName = subPath.substring(0, lastSlash).replace('/', '.'); - return packageName + "." + className; - } -} diff --git a/src/main/java/com/codechecker/parser/ClassFieldParser.java b/src/main/java/com/codechecker/parser/ClassFieldParser.java deleted file mode 100644 index 15be0c1..0000000 --- a/src/main/java/com/codechecker/parser/ClassFieldParser.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.codechecker.parser; - -import com.codechecker.model.FieldInfo; -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.ast.CompilationUnit; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.body.VariableDeclarator; -import com.github.javaparser.ast.comments.JavadocComment; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * 解析模型类字段:名称、类型、业务说明(注解或 Javadoc)。 - */ -public class ClassFieldParser { - - /** 解析指定类的实例字段列表 */ - public List parseFields(String source, String expectedClassName) { - if (source == null || source.isBlank()) { - return List.of(); - } - CompilationUnit cu = StaticJavaParser.parse(source); - ClassOrInterfaceDeclaration classDecl = findClass(cu, expectedClassName); - if (classDecl == null) { - return List.of(); - } - return parseClassFields(classDecl); - } - - /** 按类名查找类声明,找不到则取第一个类 */ - private ClassOrInterfaceDeclaration findClass(CompilationUnit cu, String expectedClassName) { - if (expectedClassName != null && !expectedClassName.isBlank()) { - for (TypeDeclaration type : cu.getTypes()) { - if (type instanceof ClassOrInterfaceDeclaration) { - ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) type; - if (classDecl.getNameAsString().equals(expectedClassName)) { - return classDecl; - } - } - } - } - for (TypeDeclaration type : cu.getTypes()) { - if (type instanceof ClassOrInterfaceDeclaration) { - return (ClassOrInterfaceDeclaration) type; - } - } - return null; - } - - /** 提取非 static final 字段,跳过常量 */ - private List parseClassFields(ClassOrInterfaceDeclaration classDecl) { - Map fields = new LinkedHashMap<>(); - for (FieldDeclaration fieldDecl : classDecl.getFields()) { - if (fieldDecl.isStatic() && fieldDecl.isFinal()) { - continue; - } - String type = TypeNameUtils.typeToString(fieldDecl.getElementType()); - String description = extractFieldLabel(fieldDecl); - for (VariableDeclarator variable : fieldDecl.getVariables()) { - fields.put(variable.getNameAsString(), new FieldInfo(variable.getNameAsString(), type, description)); - } - } - return new ArrayList<>(fields.values()); - } - - /** - * 字段说明:@Schema(description) > @ApiModelProperty > Javadoc,均无则空串。 - */ - String extractFieldLabel(FieldDeclaration fieldDecl) { - for (AnnotationExpr annotation : fieldDecl.getAnnotations()) { - String annName = annotation.getNameAsString(); - if ("Schema".equals(annName)) { - String description = readAnnotationStringValue(annotation, "description"); - if (!description.isEmpty()) { - return description; - } - } - if ("ApiModelProperty".equals(annName)) { - String value = readAnnotationStringValue(annotation, "value"); - if (!value.isEmpty()) { - return value; - } - } - } - return extractJavadoc(fieldDecl); - } - - /** 读取注解中的字符串属性值 */ - private String readAnnotationStringValue(AnnotationExpr annotation, String attributeName) { - if (annotation.isNormalAnnotationExpr()) { - NormalAnnotationExpr normal = annotation.asNormalAnnotationExpr(); - for (var pair : normal.getPairs()) { - if (pair.getNameAsString().equals(attributeName)) { - return literalString(pair.getValue()); - } - } - return ""; - } - if (annotation.isSingleMemberAnnotationExpr()) { - SingleMemberAnnotationExpr single = annotation.asSingleMemberAnnotationExpr(); - if ("value".equals(attributeName) || "description".equals(attributeName)) { - return literalString(single.getMemberValue()); - } - } - return ""; - } - - /** 提取字符串字面量值 */ - private String literalString(Expression expression) { - if (expression.isStringLiteralExpr()) { - return expression.asStringLiteralExpr().getValue().trim(); - } - return ""; - } - - /** 从字段 Javadoc 提取首段描述 */ - private String extractJavadoc(FieldDeclaration fieldDecl) { - Optional javadoc = fieldDecl.getJavadocComment(); - if (javadoc.isEmpty()) { - return ""; - } - String text = javadoc.get().parse().getDescription().toText(); - return text == null ? "" : text.trim().replaceAll("\\s+", " "); - } -} diff --git a/src/main/java/com/codechecker/parser/ConversionParser.java b/src/main/java/com/codechecker/parser/ConversionParser.java deleted file mode 100644 index 6bfc1ec..0000000 --- a/src/main/java/com/codechecker/parser/ConversionParser.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.codechecker.parser; - -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.ast.CompilationUnit; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.expr.MethodCallExpr; -import com.github.javaparser.ast.visitor.VoidVisitorAdapter; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.stream.Stream; - -/** - * 扫描 Dto→Entity 转换关系:convert 方法返回值、BeanUtils.copyProperties 调用。 - */ -public class ConversionParser { - - /** 在类内查找 convert 方法,收集返回 Entity 的类型名 */ - public Set findConvertTargetsInClass(String source, String className) { - Set entities = new LinkedHashSet<>(); - if (source == null || source.isBlank()) { - return entities; - } - CompilationUnit cu = StaticJavaParser.parse(source); - for (TypeDeclaration type : cu.getTypes()) { - if (type instanceof ClassOrInterfaceDeclaration) { - ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) type; - if (!classDecl.getNameAsString().equals(className)) { - continue; - } - for (MethodDeclaration method : classDecl.getMethods()) { - if (!"convert".equals(method.getNameAsString())) { - continue; - } - String returnType = TypeNameUtils.simpleName(TypeNameUtils.typeToString(method.getType())); - if (returnType.endsWith("Entity")) { - entities.add(returnType); - } - } - } - } - return entities; - } - - /** 递归扫描目录,查找 BeanUtils.copyProperties(sourceClass, *Entity) */ - public Set findBeanUtilsTargets(Path rootDir, String sourceClassName) throws IOException { - Set entities = new LinkedHashSet<>(); - if (!Files.exists(rootDir)) { - return entities; - } - try (Stream paths = Files.walk(rootDir)) { - paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> { - try { - String source = Files.readString(path, StandardCharsets.UTF_8); - entities.addAll(scanBeanUtilsInSource(source, sourceClassName)); - } catch (IOException ignored) { - // 跳过 - } - }); - } - return entities; - } - - /** 在单文件源码中扫描 BeanUtils.copyProperties 调用 */ - private Set scanBeanUtilsInSource(String source, String sourceClassName) { - Set entities = new LinkedHashSet<>(); - CompilationUnit cu = StaticJavaParser.parse(source); - cu.accept(new VoidVisitorAdapter() { - @Override - public void visit(MethodCallExpr call, Void arg) { - super.visit(call, arg); - if (!call.getNameAsString().equals("copyProperties")) { - return; - } - if (call.getScope().isEmpty()) { - return; - } - String scope = call.getScope().get().toString(); - if (!scope.endsWith("BeanUtils")) { - return; - } - if (call.getArguments().size() < 2) { - return; - } - String firstArg = TypeNameUtils.simpleName(call.getArguments().get(0).toString()); - String secondArg = TypeNameUtils.simpleName(call.getArguments().get(1).toString()); - if (sourceClassName.equals(firstArg) && secondArg.endsWith("Entity")) { - entities.add(secondArg); - } - } - }, null); - return entities; - } -} diff --git a/src/main/java/com/codechecker/parser/EndpointParser.java b/src/main/java/com/codechecker/parser/EndpointParser.java deleted file mode 100644 index 8dee22f..0000000 --- a/src/main/java/com/codechecker/parser/EndpointParser.java +++ /dev/null @@ -1,301 +0,0 @@ -package com.codechecker.parser; - -import com.codechecker.model.ApiEndpoint; -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.ast.CompilationUnit; -import com.github.javaparser.ast.NodeList; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.Parameter; -import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.type.Type; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -/** - * 扫描 Controller / Feign 接口,提取 HTTP 方法、URI、入参/返回类型。 - */ -public class EndpointParser { - private static final Set MAPPING_ANNOTATIONS = Set.of( - "GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping" - ); - private static final Map MAPPING_DEFAULT_METHOD = Map.of( - "GetMapping", "GET", - "PostMapping", "POST", - "PutMapping", "PUT", - "DeleteMapping", "DELETE", - "PatchMapping", "PATCH" - ); - - /** 扫描 @RestController / @Controller 目录 */ - public List scanControllerDirectory(Path rootDir, String relativePrefix) throws IOException { - return scanDirectory(rootDir, relativePrefix, ScanMode.CONTROLLER); - } - - /** 扫描 @FeignClient 接口目录 */ - public List scanFeignDirectory(Path rootDir, String relativePrefix) throws IOException { - return scanDirectory(rootDir, relativePrefix, ScanMode.FEIGN); - } - - /** 递归 walk 目录下 .java 并解析 */ - private List scanDirectory(Path rootDir, String relativePrefix, ScanMode mode) throws IOException { - if (!Files.exists(rootDir)) { - return List.of(); - } - List endpoints = new ArrayList<>(); - try (Stream paths = Files.walk(rootDir)) { - paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> { - try { - String source = Files.readString(path, StandardCharsets.UTF_8); - String relativePath = toRelativePath(relativePrefix, rootDir, path); - endpoints.addAll(parseCompilationUnit(source, relativePath, mode)); - } catch (IOException ignored) { - // 跳过无法读取的文件 - } - }); - } - return endpoints; - } - - /** 解析单个编译单元,过滤 Controller 或 Feign */ - private List parseCompilationUnit(String source, String relativePath, ScanMode mode) { - CompilationUnit cu = StaticJavaParser.parse(source); - List endpoints = new ArrayList<>(); - - for (TypeDeclaration type : cu.getTypes()) { - if (!(type instanceof ClassOrInterfaceDeclaration)) { - continue; - } - ClassOrInterfaceDeclaration declaration = (ClassOrInterfaceDeclaration) type; - if (mode == ScanMode.CONTROLLER && !isController(declaration)) { - continue; - } - if (mode == ScanMode.FEIGN && !isFeignClient(declaration)) { - continue; - } - - String basePath = mode == ScanMode.FEIGN - ? joinPaths(extractFeignBasePath(declaration), extractTypeLevelPath(declaration)) - : extractTypeLevelPath(declaration); - for (MethodDeclaration method : declaration.getMethods()) { - if (mode == ScanMode.FEIGN && declaration.isInterface()) { - endpoints.addAll(parseMethod(method, basePath, relativePath)); - } else if (mode == ScanMode.CONTROLLER && !declaration.isInterface()) { - endpoints.addAll(parseMethod(method, basePath, relativePath)); - } - } - } - return endpoints; - } - - /** 解析方法上的 Mapping 注解,生成 ApiEndpoint */ - private List parseMethod(MethodDeclaration method, String basePath, String sourceFile) { - List endpoints = new ArrayList<>(); - for (AnnotationExpr annotation : method.getAnnotations()) { - String annName = annotation.getNameAsString(); - if (!MAPPING_ANNOTATIONS.contains(annName)) { - continue; - } - List subPaths = extractPaths(annotation); - List httpMethods = extractHttpMethods(annotation, annName); - for (String httpMethod : httpMethods) { - for (String subPath : subPaths) { - String uri = joinPaths(basePath, subPath); - Set paramTypes = extractParamTypes(method); - Set returnTypes = TypeNameUtils.peelDirectTypeNames(method.getType()); - endpoints.add(new ApiEndpoint(httpMethod, uri, sourceFile, paramTypes, returnTypes)); - } - } - } - return endpoints; - } - - /** 收集方法入参类型简单名 */ - private Set extractParamTypes(MethodDeclaration method) { - Set paramTypes = new LinkedHashSet<>(); - for (Parameter parameter : method.getParameters()) { - Type type = parameter.getType(); - paramTypes.add(TypeNameUtils.simpleName(TypeNameUtils.typeToString(type))); - paramTypes.addAll(TypeNameUtils.peelDirectTypeNames(type)); - } - return paramTypes; - } - - /** 是否 Spring Controller */ - private boolean isController(ClassOrInterfaceDeclaration declaration) { - return declaration.getAnnotations().stream() - .anyMatch(ann -> { - String name = ann.getNameAsString(); - return "RestController".equals(name) || "Controller".equals(name); - }); - } - - /** 是否 Feign 客户端接口 */ - private boolean isFeignClient(ClassOrInterfaceDeclaration declaration) { - return declaration.isInterface() && declaration.getAnnotations().stream() - .anyMatch(ann -> "FeignClient".equals(ann.getNameAsString())); - } - - /** 类级 @RequestMapping 路径 */ - private String extractTypeLevelPath(ClassOrInterfaceDeclaration declaration) { - for (AnnotationExpr annotation : declaration.getAnnotations()) { - if ("RequestMapping".equals(annotation.getNameAsString())) { - List paths = extractPaths(annotation); - if (!paths.isEmpty()) { - return paths.get(0); - } - } - } - return ""; - } - - /** @FeignClient(path=...) 基础路径 */ - private String extractFeignBasePath(ClassOrInterfaceDeclaration declaration) { - for (AnnotationExpr annotation : declaration.getAnnotations()) { - if ("FeignClient".equals(annotation.getNameAsString())) { - List paths = AnnotationValueReader.readStringArray(annotation, "path"); - if (!paths.isEmpty()) { - return paths.get(0); - } - } - } - return ""; - } - - /** 从 Mapping 注解读取 value/path */ - private List extractPaths(AnnotationExpr annotation) { - return AnnotationValueReader.readStringArray(annotation, "value", "path"); - } - - /** 推断 HTTP 方法;RequestMapping 无 method 时默认 GET */ - private List extractHttpMethods(AnnotationExpr annotation, String annName) { - if (!"RequestMapping".equals(annName)) { - return List.of(MAPPING_DEFAULT_METHOD.getOrDefault(annName, "GET")); - } - List methods = AnnotationValueReader.readEnumArray(annotation, "method"); - if (methods.isEmpty()) { - return List.of("GET"); - } - return methods; - } - - /** 拼接类级与方法级路径 */ - private String joinPaths(String base, String sub) { - String normalizedBase = normalizePath(base); - String normalizedSub = normalizePath(sub); - if (normalizedBase.isEmpty()) { - return normalizedSub.isEmpty() ? "/" : normalizedSub; - } - if (normalizedSub.isEmpty()) { - return normalizedBase; - } - String joined = normalizedBase + "/" + normalizedSub.substring(1); - return joined.replaceAll("/+", "/"); - } - - /** 规范化 URI 路径 */ - private String normalizePath(String path) { - if (path == null || path.isBlank()) { - return ""; - } - String trimmed = path.trim(); - if (!trimmed.startsWith("/")) { - trimmed = "/" + trimmed; - } - return trimmed.replaceAll("/+", "/"); - } - - /** 生成相对仓库根的路径 */ - private String toRelativePath(String relativePrefix, Path rootDir, Path file) { - String relative = rootDir.relativize(file).toString().replace("\\", "/"); - if (relativePrefix == null || relativePrefix.isBlank()) { - return relative; - } - String prefix = relativePrefix.endsWith("/") - ? relativePrefix.substring(0, relativePrefix.length() - 1) - : relativePrefix; - return prefix + "/" + relative; - } - - private enum ScanMode { - CONTROLLER, FEIGN - } - - /** 从注解 AST 读取字符串或枚举数组 */ - static final class AnnotationValueReader { - private AnnotationValueReader() { - } - - static List readStringArray(AnnotationExpr annotation, String... keys) { - NodeList values = readArrayValues(annotation, keys); - List result = new ArrayList<>(); - for (Object value : values) { - String text = value.toString().replace("\"", "").trim(); - if (!text.isBlank()) { - result.add(text); - } - } - if (result.isEmpty()) { - result.add(""); - } - return result; - } - - static List readEnumArray(AnnotationExpr annotation, String key) { - NodeList values = readArrayValues(annotation, key); - List result = new ArrayList<>(); - for (Object value : values) { - String text = value.toString().trim(); - if (text.contains(".")) { - text = text.substring(text.lastIndexOf('.') + 1); - } - result.add(text.toUpperCase(Locale.ROOT)); - } - return result; - } - - private static NodeList readArrayValues(AnnotationExpr annotation, String... keys) { - if (annotation.isSingleMemberAnnotationExpr()) { - Expression value = annotation.asSingleMemberAnnotationExpr().getMemberValue(); - if (value.isArrayInitializerExpr()) { - return value.asArrayInitializerExpr().getValues(); - } - return new NodeList<>(value); - } - if (annotation.isNormalAnnotationExpr()) { - var pairs = annotation.asNormalAnnotationExpr().getPairs(); - for (var pair : pairs) { - for (String key : keys) { - if (pair.getNameAsString().equals(key)) { - if (pair.getValue().isArrayInitializerExpr()) { - return pair.getValue().asArrayInitializerExpr().getValues(); - } - return new NodeList<>(pair.getValue()); - } - } - } - for (var pair : pairs) { - if ("value".equals(pair.getNameAsString())) { - if (pair.getValue().isArrayInitializerExpr()) { - return pair.getValue().asArrayInitializerExpr().getValues(); - } - return new NodeList<>(pair.getValue()); - } - } - } - return new NodeList<>(); - } - } -} diff --git a/src/main/java/com/codechecker/parser/TypeNameUtils.java b/src/main/java/com/codechecker/parser/TypeNameUtils.java deleted file mode 100644 index 27ed236..0000000 --- a/src/main/java/com/codechecker/parser/TypeNameUtils.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.codechecker.parser; - -import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.ast.type.Type; - -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -/** - * Java 类型名工具:转字符串、取简单名、剥离 ActionResult/List 等泛型包装。 - */ -public final class TypeNameUtils { - /** 需要向内层继续剥离的包装类型 */ - private static final Set WRAPPER_TYPES = Set.of( - "ActionResult", "List", "PageListVO", "Set", "Collection", "Iterable", "Optional" - ); - - private TypeNameUtils() { - } - - /** Type 转无空白字符串 */ - public static String typeToString(Type type) { - if (type == null) { - return "Object"; - } - return type.toString().replaceAll("\\s+", ""); - } - - /** 取类型简单名,去掉包名与泛型 */ - public static String simpleName(String typeName) { - if (typeName == null || typeName.isBlank()) { - return ""; - } - String cleaned = typeName.replaceAll("\\s+", ""); - int genericStart = cleaned.indexOf('<'); - String base = genericStart >= 0 ? cleaned.substring(0, genericStart) : cleaned; - int dot = base.lastIndexOf('.'); - return dot >= 0 ? base.substring(dot + 1) : base; - } - - /** 从 Type AST 收集实际业务类型简单名(穿透包装泛型) */ - public static Set peelDirectTypeNames(Type type) { - Set result = new LinkedHashSet<>(); - collectPeelTargets(type, result); - return result; - } - - /** 从类型字符串收集实际业务类型简单名 */ - public static Set peelDirectTypeNames(String typeName) { - Set result = new LinkedHashSet<>(); - collectPeelTargets(typeName, result); - return result; - } - - /** 递归收集:包装类型则进入泛型参数,否则记录简单名 */ - private static void collectPeelTargets(Type type, Set result) { - if (type == null) { - return; - } - if (type.isClassOrInterfaceType()) { - ClassOrInterfaceType classType = type.asClassOrInterfaceType(); - String name = simpleName(classType.getNameAsString()); - if (WRAPPER_TYPES.contains(name) && classType.getTypeArguments().isPresent()) { - for (Type arg : classType.getTypeArguments().get()) { - collectPeelTargets(arg, result); - } - return; - } - result.add(name); - return; - } - result.add(simpleName(typeToString(type))); - } - - /** 字符串版递归收集 */ - private static void collectPeelTargets(String typeName, Set result) { - String cleaned = typeName.replaceAll("\\s+", ""); - int genericStart = cleaned.indexOf('<'); - if (genericStart < 0) { - result.add(simpleName(cleaned)); - return; - } - String outer = simpleName(cleaned.substring(0, genericStart)); - String inner = cleaned.substring(genericStart + 1, cleaned.lastIndexOf('>')); - if (WRAPPER_TYPES.contains(outer)) { - for (String part : splitGenericArgs(inner)) { - collectPeelTargets(part, result); - } - return; - } - result.add(outer); - } - - /** 按逗号分割泛型参数,支持嵌套 <> */ - private static List splitGenericArgs(String inner) { - List parts = new java.util.ArrayList<>(); - int depth = 0; - StringBuilder current = new StringBuilder(); - for (char ch : inner.toCharArray()) { - if (ch == '<') { - depth++; - } else if (ch == '>') { - depth--; - } else if (ch == ',' && depth == 0) { - parts.add(current.toString().trim()); - current.setLength(0); - continue; - } - current.append(ch); - } - if (current.length() > 0) { - parts.add(current.toString().trim()); - } - return parts; - } -}