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) 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)); continue; } ClassChangeReport report = analyzeModifiedOrRenamed(changedFile, config, repoRoot, oldSha, newSha, endpointIndex); if (report != null) { reports.add(report); } } return reports; } /** 处理删除:标记 DELETED 并分析影响(基于旧源码) */ private ClassChangeReport analyzeDeleted(ChangedClassFile changedFile, AppConfig config, Path repoRoot, String oldSha, Map endpointIndex) 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); return report; } /** 处理修改/重命名:字段 diff → 判定 changeKind → 影响分析 */ private ClassChangeReport analyzeModifiedOrRenamed(ChangedClassFile changedFile, AppConfig config, Path repoRoot, String oldSha, String newSha, Map endpointIndex) 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); return report; } }