feat: 效果修改
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped

This commit is contained in:
2026-07-14 11:32:30 +08:00
parent d0f10e5479
commit 4da632bd5c
3 changed files with 439 additions and 38 deletions

View File

@@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
@@ -53,34 +54,32 @@ public class ReportBuilder {
appendKeyLine(sb, kc.getKeyPattern());
String oldJson = nvl(kc.getOldSkeletonJson());
String newJson = nvl(kc.getNewSkeletonJson());
Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails());
Set<String> newHighlight = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails());
String oldRendered = oldJson.isEmpty()
? "" : SkeletonAnnotator.annotateAsCodeBlock(oldJson, oldHighlight);
String newRendered = newJson.isEmpty()
? "" : SkeletonAnnotator.annotateAsCodeBlock(newJson, newHighlight);
if (oldJson.isEmpty() && !newJson.isEmpty()) {
sb.append(" value 新增为 ").append(fontWarning(newJson)).append("\n\n");
sb.append(" value 新增为 ").append(newRendered).append("\n\n");
} else if (!oldJson.isEmpty() && newJson.isEmpty()) {
sb.append(" value 原结构 ").append(fontInfo(oldJson)).append(" 已删除写入\n\n");
sb.append(" value 原结构 ").append(oldRendered).append(" 已删除写入\n\n");
} else {
sb.append(" value 值从 ").append(fontInfo(oldJson)).append("\n");
sb.append(" 变更为 ").append(fontWarning(newJson)).append("\n\n");
sb.append(" value 值从 ").append(oldRendered).append('\n');
sb.append(" 变更为 ").append(newRendered).append("\n\n");
}
}
return sb.toString();
}
/**
* Key 行:加粗「Key -->+ 加粗 key 文本
* 避免 key 末尾 * markdown ** 冲突,结尾加零宽空格再闭合
* Key 行Key --> + 橙红色 key(企微 warning
* 末尾加零宽空格,避免 key 中的 * 干扰后续 markdown。
*/
private void appendKeyLine(StringBuilder sb, String keyPattern) {
sb.append("- **Key --> ").append(nvl(keyPattern)).append("\u200b**\n");
}
/** 企微 markdown绿色旧骨架。 */
private String fontInfo(String text) {
return "<font color=\"info\">" + text + "</font>";
}
/** 企微 markdownwarning 橙红(新骨架)。 */
private String fontWarning(String text) {
return "<font color=\"warning\">" + text + "</font>";
sb.append("- Key --> <font color=\"warning\">")
.append(nvl(keyPattern))
.append("\u200b</font>\n");
}
/**

View File

@@ -0,0 +1,351 @@
package com.codechecker.cache.report;
import com.codechecker.cache.diff.ChangeType;
import com.codechecker.cache.diff.SchemaChange;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* 在骨架 JSON 中为改动字段加企微 warning 标注。
* <ul>
* <li>删除字段 → 标在旧骨架</li>
* <li>新增字段 / 新增包装层 → 标在新骨架</li>
* <li>路径迁移 → 旧路径标旧骨架、新路径标新骨架</li>
* </ul>
*/
final class SkeletonAnnotator {
private static final String FONT_OPEN = "<font color=\"warning\">";
private static final String FONT_CLOSE = "</font>";
private SkeletonAnnotator() {
}
static Set<String> pathsForOldSkeleton(List<SchemaChange> details) {
Set<String> paths = new LinkedHashSet<>();
if (details == null) {
return paths;
}
for (SchemaChange c : details) {
if (c == null || c.getChangeType() == null) {
continue;
}
if (c.getChangeType() == ChangeType.FIELD_REMOVED) {
addIfPresent(paths, c.getFieldPath());
} else if (c.getChangeType() == ChangeType.FIELD_PATH_MOVED) {
addIfPresent(paths, c.getOldValue());
}
}
return paths;
}
static Set<String> pathsForNewSkeleton(List<SchemaChange> details) {
Set<String> paths = new LinkedHashSet<>();
if (details == null) {
return paths;
}
for (SchemaChange c : details) {
if (c == null || c.getChangeType() == null) {
continue;
}
if (c.getChangeType() == ChangeType.FIELD_ADDED
|| c.getChangeType() == ChangeType.WRAPPER_ADDED) {
addIfPresent(paths, c.getFieldPath());
} else if (c.getChangeType() == ChangeType.FIELD_PATH_MOVED) {
addIfPresent(paths, c.getFieldPath());
if (c.getNewValue() != null && !c.getNewValue().equals(c.getFieldPath())) {
addIfPresent(paths, c.getNewValue());
}
}
}
return paths;
}
/**
* 标注路径对应的 {@code "field":value} 片段,并包成可与代码块混排的企微 Markdown。
* 企微代码块内不解析 font故整段用反引号分段包裹改动项单独用 warning+反引号。
*/
static String annotateAsCodeBlock(String json, Set<String> highlightPaths) {
if (json == null || json.isEmpty()) {
return "``";
}
String annotated = annotate(json, highlightPaths);
return toMixedCodeMarkdown(annotated);
}
static String annotate(String json, Set<String> highlightPaths) {
if (json == null || json.isEmpty() || highlightPaths == null || highlightPaths.isEmpty()) {
return json == null ? "" : json;
}
String result = json;
List<String> sorted = new ArrayList<>(highlightPaths);
sorted.sort(Comparator.comparingInt(String::length).reversed());
for (String path : sorted) {
result = wrapOnePath(result, path);
}
return result;
}
/**
* 将已含 {@code <font>} 的字符串拆成:普通段走反引号代码块,改动段走 warning+反引号。
*/
static String toMixedCodeMarkdown(String annotated) {
if (annotated == null || annotated.isEmpty()) {
return "``";
}
if (!annotated.contains(FONT_OPEN)) {
return "`" + annotated + "`";
}
StringBuilder out = new StringBuilder();
int i = 0;
boolean inCode = false;
while (i < annotated.length()) {
int start = annotated.indexOf(FONT_OPEN, i);
if (start < 0) {
if (!inCode) {
out.append('`');
inCode = true;
}
out.append(annotated.substring(i));
out.append('`');
return out.toString();
}
if (start > i) {
if (!inCode) {
out.append('`');
inCode = true;
}
out.append(annotated, i, start);
out.append('`');
inCode = false;
} else if (inCode) {
out.append('`');
inCode = false;
}
int contentStart = start + FONT_OPEN.length();
int end = annotated.indexOf(FONT_CLOSE, contentStart);
if (end < 0) {
if (!inCode) {
out.append('`');
}
out.append(annotated.substring(start)).append('`');
return out.toString();
}
String frag = annotated.substring(contentStart, end);
out.append(FONT_OPEN).append('`').append(frag).append('`').append(FONT_CLOSE);
i = end + FONT_CLOSE.length();
}
return out.toString();
}
private static void addIfPresent(Set<String> paths, String path) {
if (path != null && !path.trim().isEmpty()) {
paths.add(path.trim());
}
}
private static String wrapOnePath(String json, String path) {
if (path == null || path.isEmpty()) {
return json;
}
// 已标注过则跳过(避免重复)
String key = lastSegment(path);
if (key.isEmpty()) {
return json;
}
int propStart = findPropertyStart(json, path);
if (propStart < 0) {
return json;
}
// 已在 font 内则跳过
int fontBefore = json.lastIndexOf(FONT_OPEN, propStart);
int fontCloseBefore = json.lastIndexOf(FONT_CLOSE, propStart);
if (fontBefore > fontCloseBefore) {
return json;
}
String needle = "\"" + key + "\"";
int keyEnd = propStart + needle.length();
int valueEnd = findValueEnd(json, keyEnd);
if (valueEnd <= propStart) {
return json;
}
String frag = json.substring(propStart, valueEnd);
if (frag.contains(FONT_OPEN)) {
return json;
}
return json.substring(0, propStart)
+ FONT_OPEN + frag + FONT_CLOSE
+ json.substring(valueEnd);
}
/**
* 按点分路径定位属性名起始下标(指向开头引号)。
*/
private static int findPropertyStart(String json, String path) {
List<String> segs = splitPath(path);
if (segs.isEmpty()) {
return -1;
}
int searchFrom = 0;
int scopeEnd = json.length();
for (int s = 0; s < segs.size(); s++) {
String seg = segs.get(s);
boolean last = s == segs.size() - 1;
String needle = "\"" + seg + "\"";
int idx = indexOfPropertyInScope(json, needle, searchFrom, scopeEnd);
if (idx < 0) {
return -1;
}
if (last) {
return idx;
}
int keyEnd = idx + needle.length();
int valueStart = skipColon(json, keyEnd);
if (valueStart >= json.length()) {
return -1;
}
char c = json.charAt(valueStart);
if (c == '{') {
searchFrom = valueStart + 1;
scopeEnd = findMatchingBracket(json, valueStart);
if (scopeEnd < 0) {
return -1;
}
} else if (c == '[') {
// 数组:进入第一个元素对象
int arrEnd = findMatchingBracket(json, valueStart);
int elemObj = json.indexOf('{', valueStart);
if (elemObj < 0 || elemObj >= arrEnd) {
searchFrom = valueStart + 1;
scopeEnd = arrEnd;
} else {
searchFrom = elemObj + 1;
scopeEnd = findMatchingBracket(json, elemObj);
}
} else {
return -1;
}
}
return -1;
}
private static int indexOfPropertyInScope(String json, String needle, int from, int to) {
int idx = from;
while (idx >= 0 && idx < to) {
idx = json.indexOf(needle, idx);
if (idx < 0 || idx >= to) {
return -1;
}
// 粗略校验:前面应是 { 或 ,
int p = idx - 1;
while (p >= from && Character.isWhitespace(json.charAt(p))) {
p--;
}
if (p < from || json.charAt(p) == '{' || json.charAt(p) == ',' || json.charAt(p) == '[') {
return idx;
}
idx += needle.length();
}
return -1;
}
private static int skipColon(String json, int afterKey) {
int i = afterKey;
while (i < json.length() && (json.charAt(i) == ':' || Character.isWhitespace(json.charAt(i)))) {
i++;
}
return i;
}
private static int findMatchingBracket(String json, int openIdx) {
if (openIdx < 0 || openIdx >= json.length()) {
return -1;
}
char open = json.charAt(openIdx);
char close = open == '{' ? '}' : ']';
int depth = 0;
for (int i = openIdx; i < json.length(); i++) {
char ch = json.charAt(i);
if (ch == '"') {
i++;
while (i < json.length()) {
char x = json.charAt(i++);
if (x == '\\' && i < json.length()) {
i++;
} else if (x == '"') {
break;
}
}
i--;
continue;
}
if (ch == open) {
depth++;
} else if (ch == close) {
depth--;
if (depth == 0) {
return i;
}
}
}
return -1;
}
private static int findValueEnd(String json, int afterKey) {
int i = skipColon(json, afterKey);
if (i >= json.length()) {
return json.length();
}
char c = json.charAt(i);
if (c == '"') {
i++;
while (i < json.length()) {
char ch = json.charAt(i++);
if (ch == '\\' && i < json.length()) {
i++;
} else if (ch == '"') {
break;
}
}
return i;
}
if (c == '{' || c == '[') {
int end = findMatchingBracket(json, i);
return end < 0 ? json.length() : end + 1;
}
while (i < json.length() && json.charAt(i) != ',' && json.charAt(i) != '}' && json.charAt(i) != ']') {
i++;
}
return i;
}
private static List<String> splitPath(String path) {
List<String> segs = new ArrayList<>();
if (path == null || path.isEmpty()) {
return segs;
}
for (String raw : path.split("\\.")) {
if (raw.isEmpty() || "[]".equals(raw)) {
continue;
}
if (raw.endsWith("[]")) {
String name = raw.substring(0, raw.length() - 2);
if (!name.isEmpty()) {
segs.add(name);
}
} else {
segs.add(raw);
}
}
return segs;
}
private static String lastSegment(String path) {
List<String> segs = splitPath(path);
return segs.isEmpty() ? "" : segs.get(segs.size() - 1);
}
}