feat: 项目整体命名修改cache-schema-checker
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped

This commit is contained in:
2026-07-14 11:03:34 +08:00
parent 114b053733
commit 110beb79c0
42 changed files with 197 additions and 199 deletions

View File

@@ -0,0 +1,414 @@
package com.codechecker.cache.analyze;
import com.codechecker.cache.config.CheckerConfig;
import com.codechecker.cache.detector.RedisWritePointDetector;
import com.codechecker.cache.detector.WritePoint;
import com.codechecker.cache.diff.ChangeType;
import com.codechecker.cache.diff.SchemaChange;
import com.codechecker.cache.diff.SchemaDiffer;
import com.codechecker.cache.diff.Severity;
import com.codechecker.cache.git.GitDiffScanner;
import com.codechecker.cache.git.GitException;
import com.codechecker.cache.report.CheckReport;
import com.codechecker.cache.report.KeyStructureChange;
import com.codechecker.cache.schema.JavaSchemaExtractor;
import com.codechecker.cache.schema.SkeletonJsonRenderer;
import com.codechecker.cache.schema.SourceIndex;
import com.codechecker.cache.schema.TypeSchema;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.TypeDeclaration;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* 端到端分析编排git diff → 定位写入点 → 双版本 Schema 提取 → 结构对比 → 生成报告。
*/
public class SchemaCheckAnalyzer {
private final CheckerConfig config;
private final Path repoRoot;
public SchemaCheckAnalyzer(CheckerConfig config, Path repoRoot) {
this.config = config;
this.repoRoot = repoRoot;
}
public CheckReport analyze(String oldSha, String newSha) throws GitException {
GitDiffScanner scanner = new GitDiffScanner(repoRoot);
List<String> changedRaw = scanner.changedJavaFiles(oldSha, newSha);
Set<String> changedFiles = new LinkedHashSet<>();
FileScanner fileScanner = new FileScanner(repoRoot, config.getIncludeModules(), config.getExcludeModules());
for (String path : changedRaw) {
if (path.contains("/src/main/java/") && fileScanner.moduleAllowed(path) && !isFileIgnored(path)) {
changedFiles.add(path);
}
}
// 当前工作树(= newSha 检出)内容
Map<String, String> newContents = fileScanner.scan();
// 旧版本内容:以工作树为基线,用 old 版本覆盖变更文件
Map<String, String> oldContents = new LinkedHashMap<>(newContents);
for (String path : changedFiles) {
String oldContent = scanner.fileContentAt(oldSha, path);
if (oldContent == null) {
oldContents.remove(path); // 新增文件在旧版本不存在
} else {
oldContents.put(path, oldContent);
}
}
SourceIndex newIndex = buildIndex(newContents.values());
SourceIndex oldIndex = buildIndex(oldContents.values());
// 变更涉及的类型简单名(用于扩展候选写入点文件)
Set<String> changedTypeNames = new HashSet<>();
for (String path : changedFiles) {
collectTypeNames(newContents.get(path), changedTypeNames);
collectTypeNames(oldContents.get(path), changedTypeNames);
}
Set<String> candidates = new LinkedHashSet<>(changedFiles);
Pattern typePattern = buildTypePattern(changedTypeNames);
if (typePattern != null) {
for (Map.Entry<String, String> e : newContents.entrySet()) {
if (!candidates.contains(e.getKey()) && typePattern.matcher(e.getValue()).find()) {
candidates.add(e.getKey());
}
}
}
Set<String> patterns = new HashSet<>(config.getDetection().getPatterns());
RedisWritePointDetector detectorNew = new RedisWritePointDetector(newIndex, patterns);
RedisWritePointDetector detectorOld = new RedisWritePointDetector(oldIndex, patterns);
JavaSchemaExtractor extractorNew = new JavaSchemaExtractor(newIndex, config.getDetection().getMaxFieldDepth());
JavaSchemaExtractor extractorOld = new JavaSchemaExtractor(oldIndex, config.getDetection().getMaxFieldDepth());
SchemaDiffer differ = new SchemaDiffer();
SkeletonJsonRenderer skeletonRenderer = new SkeletonJsonRenderer();
List<SchemaChange> allChanges = new ArrayList<>();
Map<String, KeyStructureChange> keyChanges = new LinkedHashMap<>();
for (String path : candidates) {
String newContent = newContents.get(path);
if (newContent == null) {
continue;
}
boolean fileChanged = changedFiles.contains(path);
String oldContent = fileChanged ? oldContents.get(path) : newContent;
List<WritePoint> newWps = detectorNew.detect(path, newContent);
List<WritePoint> oldWps = oldContent == null
? new ArrayList<>() : detectorOld.detect(path, oldContent);
Map<String, WritePoint> oldBySig = new LinkedHashMap<>();
for (WritePoint wp : oldWps) {
oldBySig.put(wp.signature(), wp);
}
Set<String> newSigs = new HashSet<>();
for (WritePoint nw : newWps) {
newSigs.add(nw.signature());
if (isKeyIgnored(nw.getResolvedKeyPattern()) || isWriterIgnored(nw)) {
continue;
}
WritePoint ow = oldBySig.get(nw.signature());
if (ow != null) {
TypeSchema oldSchema = extractorOld.extract(ow.getResolvedValueType(), ow.isRootArray());
TypeSchema newSchema = extractorNew.extract(nw.getResolvedValueType(), nw.isRootArray());
List<SchemaChange> changes = differ.diff(oldSchema, newSchema);
if (changes.isEmpty()) {
continue;
}
double confidence = min(nw.getConfidence(), ow.getConfidence(),
oldSchema.getConfidence(), newSchema.getConfidence());
enrich(changes, nw, confidence);
allChanges.addAll(changes);
mergeKeyChange(keyChanges, nw.getResolvedKeyPattern(), changes,
skeletonRenderer.render(oldSchema, protectedPaths(changes),
SkeletonJsonRenderer.DEFAULT_MAX_LEN),
skeletonRenderer.render(newSchema, protectedPaths(changes),
SkeletonJsonRenderer.DEFAULT_MAX_LEN));
} else if (fileChanged) {
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_ADDED);
c.setKeyPattern(nw.getResolvedKeyPattern());
c.setWriteLocation(nw.location());
c.setMessage("新增缓存写入点value 类型: " + shortType(nw.getResolvedValueType()));
allChanges.add(c);
TypeSchema newSchema = extractorNew.extract(nw.getResolvedValueType(), nw.isRootArray());
mergeKeyChange(keyChanges, nw.getResolvedKeyPattern(),
Collections.singletonList(c),
"",
skeletonRenderer.render(newSchema, protectedPaths(
Collections.singletonList(c)),
SkeletonJsonRenderer.DEFAULT_MAX_LEN));
}
}
if (fileChanged) {
for (WritePoint ow : oldWps) {
if (!newSigs.contains(ow.signature())
&& !isKeyIgnored(ow.getResolvedKeyPattern()) && !isWriterIgnored(ow)) {
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_REMOVED);
c.setKeyPattern(ow.getResolvedKeyPattern());
c.setWriteLocation(ow.location());
c.setMessage("删除缓存写入点,原 value 类型: " + shortType(ow.getResolvedValueType()));
allChanges.add(c);
TypeSchema oldSchema = extractorOld.extract(ow.getResolvedValueType(), ow.isRootArray());
mergeKeyChange(keyChanges, ow.getResolvedKeyPattern(),
Collections.singletonList(c),
skeletonRenderer.render(oldSchema, protectedPaths(
Collections.singletonList(c)),
SkeletonJsonRenderer.DEFAULT_MAX_LEN),
"");
}
}
}
}
List<SchemaChange> finalChanges = postProcess(allChanges);
// 按最终字段明细重新对齐 key 摘要(过滤被 suppress/dedup 掉的)
Map<String, KeyStructureChange> finalKeys = filterKeyChanges(keyChanges, finalChanges);
return buildReport(oldSha, newSha, finalChanges, finalKeys);
}
private void mergeKeyChange(Map<String, KeyStructureChange> keyChanges, String keyPattern,
List<SchemaChange> changes, String oldSkeleton, String newSkeleton) {
String key = keyPattern == null ? "<unknown>" : keyPattern;
KeyStructureChange kc = keyChanges.computeIfAbsent(key, k -> {
KeyStructureChange n = new KeyStructureChange();
n.setKeyPattern(k);
return n;
});
kc.getFieldDetails().addAll(changes);
for (SchemaChange c : changes) {
kc.raiseSeverity(c.getSeverity());
}
if (oldSkeleton != null && !oldSkeleton.isEmpty()) {
kc.setOldSkeletonJson(oldSkeleton);
}
if (newSkeleton != null && !newSkeleton.isEmpty()) {
kc.setNewSkeletonJson(newSkeleton);
}
if (kc.getOldSkeletonJson() == null) {
kc.setOldSkeletonJson(oldSkeleton == null ? "" : oldSkeleton);
}
if (kc.getNewSkeletonJson() == null) {
kc.setNewSkeletonJson(newSkeleton == null ? "" : newSkeleton);
}
}
private Set<String> protectedPaths(List<SchemaChange> changes) {
Set<String> paths = new LinkedHashSet<>();
for (SchemaChange c : changes) {
if (c.getFieldPath() != null && !c.getFieldPath().isEmpty()) {
paths.add(c.getFieldPath());
}
// 路径迁移时 oldValue 为旧路径
if (c.getChangeType() == ChangeType.FIELD_PATH_MOVED
&& c.getOldValue() != null && !c.getOldValue().isEmpty()) {
paths.add(c.getOldValue());
}
if (c.getChangeType() == ChangeType.WRAPPER_ADDED
&& c.getFieldPath() != null) {
paths.add(c.getFieldPath());
}
}
return paths;
}
private Map<String, KeyStructureChange> filterKeyChanges(
Map<String, KeyStructureChange> keyChanges, List<SchemaChange> finalChanges) {
Set<String> liveKeys = new HashSet<>();
for (SchemaChange c : finalChanges) {
liveKeys.add(c.getKeyPattern() == null ? "<unknown>" : c.getKeyPattern());
}
Map<String, KeyStructureChange> result = new LinkedHashMap<>();
for (Map.Entry<String, KeyStructureChange> e : keyChanges.entrySet()) {
if (liveKeys.contains(e.getKey())) {
KeyStructureChange kc = e.getValue();
// 仅保留最终明细中仍存在的字段变更
List<SchemaChange> retained = new ArrayList<>();
Set<String> finalDedup = new HashSet<>();
for (SchemaChange c : finalChanges) {
String k = c.getKeyPattern() == null ? "<unknown>" : c.getKeyPattern();
if (!k.equals(e.getKey())) {
continue;
}
String dedupKey = c.getChangeType() + "|" + c.getKeyPattern() + "|"
+ c.getWriteLocation() + "|" + c.getFieldPath();
if (finalDedup.add(dedupKey)) {
retained.add(c);
}
}
kc.getFieldDetails().clear();
kc.getFieldDetails().addAll(retained);
Severity max = Severity.P2;
for (SchemaChange c : retained) {
if (c.getSeverity() != null && c.getSeverity().ordinal() < max.ordinal()) {
max = c.getSeverity();
}
}
kc.setSeverity(max);
result.put(e.getKey(), kc);
}
}
return result;
}
private void enrich(List<SchemaChange> changes, WritePoint wp, double confidence) {
boolean lowConfidence = confidence < config.getDetection().getMinConfidence();
for (SchemaChange c : changes) {
c.setKeyPattern(wp.getResolvedKeyPattern());
c.setWriteLocation(wp.location());
if (lowConfidence) {
c.setSeverity(Severity.P2);
c.setMessage(c.getMessage() + "(低置信度,建议人工确认)");
}
}
}
private List<SchemaChange> postProcess(List<SchemaChange> changes) {
List<SchemaChange> result = new ArrayList<>();
Set<String> seen = new HashSet<>();
for (SchemaChange c : changes) {
applySeverityOverride(c);
if (isSuppressed(c)) {
continue;
}
String dedupKey = c.getChangeType() + "|" + c.getKeyPattern() + "|"
+ c.getWriteLocation() + "|" + c.getFieldPath();
if (seen.add(dedupKey)) {
result.add(c);
}
}
return result;
}
private void applySeverityOverride(SchemaChange c) {
String override = config.getSeverityOverrides().get(c.getChangeType().name());
if (override != null) {
try {
c.setSeverity(Severity.valueOf(override.trim().toUpperCase()));
} catch (IllegalArgumentException ignored) {
// 无效级别忽略
}
}
}
private boolean isSuppressed(SchemaChange c) {
for (CheckerConfig.Suppression s : config.getSuppressions()) {
boolean keyMatch = s.getKeyPattern() == null
|| s.getKeyPattern().equals(c.getKeyPattern());
boolean typeMatch = s.getChangeTypes() == null || s.getChangeTypes().isEmpty()
|| s.getChangeTypes().contains(c.getChangeType().name());
if (keyMatch && typeMatch && (s.getKeyPattern() != null
|| (s.getChangeTypes() != null && !s.getChangeTypes().isEmpty()))) {
return true;
}
}
return false;
}
private CheckReport buildReport(String oldSha, String newSha, List<SchemaChange> changes,
Map<String, KeyStructureChange> keyChanges) {
CheckReport report = new CheckReport();
report.setOldSha(oldSha);
report.setNewSha(newSha);
report.setMode(config.getMode());
report.getChanges().addAll(changes);
report.getKeyChanges().addAll(keyChanges.values());
boolean blocked = config.isBlockMode() && !changes.isEmpty();
report.setBlocked(blocked);
report.setExitCode(blocked ? 1 : 0);
return report;
}
private SourceIndex buildIndex(Iterable<String> contents) {
SourceIndex index = new SourceIndex();
for (String content : contents) {
index.addSource(content);
}
return index;
}
private void collectTypeNames(String content, Set<String> out) {
if (content == null || content.isEmpty()) {
return;
}
try {
CompilationUnit cu = StaticJavaParser.parse(content);
for (TypeDeclaration<?> type : cu.findAll(TypeDeclaration.class)) {
out.add(type.getNameAsString());
}
} catch (RuntimeException ignored) {
// 解析失败跳过
}
}
private Pattern buildTypePattern(Set<String> typeNames) {
List<String> valid = new ArrayList<>();
for (String name : typeNames) {
if (name != null && name.matches("[A-Za-z_][A-Za-z0-9_]*")) {
valid.add(Pattern.quote(name));
}
}
if (valid.isEmpty()) {
return null;
}
return Pattern.compile("\\b(" + String.join("|", valid) + ")\\b");
}
private boolean isFileIgnored(String path) {
for (String glob : config.getIgnore().getFilePatterns()) {
if (GlobMatcher.matches(glob, path)) {
return true;
}
}
return false;
}
private boolean isKeyIgnored(String keyPattern) {
if (keyPattern == null) {
return false;
}
for (String glob : config.getIgnore().getKeyPatterns()) {
if (GlobMatcher.matches(glob, keyPattern)) {
return true;
}
}
return false;
}
private boolean isWriterIgnored(WritePoint wp) {
String sig = wp.getEnclosingClass() + "#" + wp.getEnclosingMethod();
return config.getIgnore().getWriterMethods().contains(sig);
}
private String shortType(String fqn) {
if (fqn == null) {
return "<未解析>";
}
return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
}
private double min(double... values) {
double m = 1.0;
for (double v : values) {
m = Math.min(m, v);
}
return m;
}
}