feat: V1.1 - 注释补充
This commit is contained in:
@@ -12,6 +12,7 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 扫描工作树中受包含/排除模块限制的 {@code src/main/java} 下的所有 Java 源文件。
|
* 扫描工作树中受包含/排除模块限制的 {@code src/main/java} 下的所有 Java 源文件。
|
||||||
|
* <p>返回路径 → 源码内容映射,供 {@link SchemaCheckAnalyzer} 构建新旧 {@code SourceIndex}。</p>
|
||||||
*/
|
*/
|
||||||
public class FileScanner {
|
public class FileScanner {
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ public final class GlobMatcher {
|
|||||||
private GlobMatcher() {
|
private GlobMatcher() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param glob glob 模式(支持 {@code *} / {@code **})
|
||||||
|
* @param input 待匹配字符串
|
||||||
|
* @return 是否整串匹配
|
||||||
|
*/
|
||||||
public static boolean matches(String glob, String input) {
|
public static boolean matches(String glob, String input) {
|
||||||
if (glob == null || input == null) {
|
if (glob == null || input == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ public class SchemaCheckAnalyzer {
|
|||||||
this.repoRoot = repoRoot;
|
this.repoRoot = repoRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对比两个提交之间的序列化结构变更。
|
||||||
|
*
|
||||||
|
* @param oldSha 基准提交
|
||||||
|
* @param newSha 当前提交(通常与工作树一致)
|
||||||
|
* @return 检测报告(含字段明细与按 Key/Topic 聚合结果)
|
||||||
|
* @throws GitException git 命令失败时
|
||||||
|
*/
|
||||||
public CheckReport analyze(String oldSha, String newSha) throws GitException {
|
public CheckReport analyze(String oldSha, String newSha) throws GitException {
|
||||||
GitDiffScanner scanner = new GitDiffScanner(repoRoot);
|
GitDiffScanner scanner = new GitDiffScanner(repoRoot);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import java.util.List;
|
|||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 命令行入口。退出码:0 通过 / 1 阻断 / 2 执行错误。
|
* 序列化结构检测工具的命令行入口(picocli)。
|
||||||
|
* <p>加载配置 → 对比 {@code old-sha}/{@code new-sha} → 控制台输出报告 → 可选企微通知。</p>
|
||||||
|
* <p>退出码:{@code 0} 通过或跳过;{@code 1} block 模式检测到变更;{@code 2} 执行错误。</p>
|
||||||
*/
|
*/
|
||||||
@Command(name = "serialization-schema-checker",
|
@Command(name = "serialization-schema-checker",
|
||||||
mixinStandardHelpOptions = true,
|
mixinStandardHelpOptions = true,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class CheckerConfig {
|
|||||||
|
|
||||||
private List<String> excludeModules = new ArrayList<>();
|
private List<String> excludeModules = new ArrayList<>();
|
||||||
|
|
||||||
|
/** 企微通知相关配置。 */
|
||||||
public static class Notify {
|
public static class Notify {
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
/** 企微机器人 Webhook 完整 URL */
|
/** 企微机器人 Webhook 完整 URL */
|
||||||
@@ -76,6 +77,7 @@ public class CheckerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 忽略规则:key / 文件路径 / 写入方法 / MQ destination。 */
|
||||||
public static class Ignore {
|
public static class Ignore {
|
||||||
private List<String> keyPatterns = new ArrayList<>();
|
private List<String> keyPatterns = new ArrayList<>();
|
||||||
private List<String> filePatterns = new ArrayList<>();
|
private List<String> filePatterns = new ArrayList<>();
|
||||||
@@ -116,6 +118,7 @@ public class CheckerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 检测模式与推断参数(Redis W*、MQ MQ*、读侧补强开关等)。 */
|
||||||
public static class Detection {
|
public static class Detection {
|
||||||
private List<String> patterns = new ArrayList<>();
|
private List<String> patterns = new ArrayList<>();
|
||||||
/** MQ 投递检测模式:MQ01~MQ05、MQ-K01~MQ-K04 */
|
/** MQ 投递检测模式:MQ01~MQ05、MQ-K01~MQ-K04 */
|
||||||
@@ -176,6 +179,9 @@ public class CheckerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人工补充映射:在自动推断不准时指定写入方法对应的 key 模式与 value 类型。
|
||||||
|
*/
|
||||||
public static class ManualMapping {
|
public static class ManualMapping {
|
||||||
private String id;
|
private String id;
|
||||||
private String writerMethod;
|
private String writerMethod;
|
||||||
@@ -224,6 +230,9 @@ public class CheckerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抑制规则:对已知误报按 key 模式和/或变更类型跳过告警。
|
||||||
|
*/
|
||||||
public static class Suppression {
|
public static class Suppression {
|
||||||
private String id;
|
private String id;
|
||||||
private String writerMethod;
|
private String writerMethod;
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ public final class ConfigLoader {
|
|||||||
private ConfigLoader() {
|
private ConfigLoader() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载并合并配置。
|
||||||
|
*
|
||||||
|
* @param businessConfigPath 业务仓 YAML;为 {@code null} 或不存在时仅使用 jar 内默认配置
|
||||||
|
* @return 绑定后的运行配置
|
||||||
|
*/
|
||||||
public static CheckerConfig load(Path businessConfigPath) {
|
public static CheckerConfig load(Path businessConfigPath) {
|
||||||
Map<String, Object> merged = loadDefault();
|
Map<String, Object> merged = loadDefault();
|
||||||
if (businessConfigPath != null && Files.exists(businessConfigPath)) {
|
if (businessConfigPath != null && Files.exists(businessConfigPath)) {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import java.util.HashSet;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无业务 JSON 结构的标量 / 裸字节类型(Redis / MQ 共用)。这些类型非序列化,直接跳过。
|
* 无业务 JSON 结构的标量 / 裸字节类型判定(Redis / MQ 检测共用)。
|
||||||
|
* <p>命中则不应建立写入点,避免对 String/数字等无结构缓存误报。</p>
|
||||||
*/
|
*/
|
||||||
final class BareValueTypes {
|
final class BareValueTypes {
|
||||||
|
|
||||||
@@ -21,10 +22,21 @@ final class BareValueTypes {
|
|||||||
private BareValueTypes() {
|
private BareValueTypes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个 Java 类型是否属于「无业务结构」的标量/裸类型集合。
|
||||||
|
*
|
||||||
|
* @param simple simple
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
static boolean isBareSimpleName(String simple) {
|
static boolean isBareSimpleName(String simple) {
|
||||||
return simple != null && BARE.contains(simple);
|
return simple != null && BARE.contains(simple);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 AST Type 取出简单名后调用
|
||||||
|
* @param type type
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
static boolean isBareType(Type type) {
|
static boolean isBareType(Type type) {
|
||||||
if (type == null) {
|
if (type == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -44,6 +56,11 @@ final class BareValueTypes {
|
|||||||
return "byte[]".equals(asString) || isBareSimpleName(asString);
|
return "byte[]".equals(asString) || isBareSimpleName(asString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 FQN 截取最后一段再调用
|
||||||
|
* @param fqn fqn
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
static boolean isBareFqn(String fqn) {
|
static boolean isBareFqn(String fqn) {
|
||||||
if (fqn == null || fqn.isEmpty()) {
|
if (fqn == null || fqn.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ public class CacheReadHintDetector {
|
|||||||
"parseObject", "parse", "getJsonToBean", "toJavaObject", "readValue"));
|
"parseObject", "parse", "getJsonToBean", "toJavaObject", "readValue"));
|
||||||
private static final Set<String> ARRAY_PARSE = new HashSet<>(Arrays.asList(
|
private static final Set<String> ARRAY_PARSE = new HashSet<>(Arrays.asList(
|
||||||
"parseArray", "getJsonToList", "parseArrayObject"));
|
"parseArray", "getJsonToList", "parseArrayObject"));
|
||||||
private static final Set<String> REDIS_GET = new HashSet<>(Arrays.asList(
|
|
||||||
"get", "getString", "opsForValue"));
|
|
||||||
|
|
||||||
private final SourceIndex index;
|
private final SourceIndex index;
|
||||||
private final RedisKeyResolver keyResolver;
|
private final RedisKeyResolver keyResolver;
|
||||||
@@ -48,6 +46,13 @@ public class CacheReadHintDetector {
|
|||||||
this.keyResolver = new RedisKeyResolver(index);
|
this.keyResolver = new RedisKeyResolver(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描读侧反序列化调用,提取可用于补强写入点的类型提示。
|
||||||
|
*
|
||||||
|
* @param filePath 仓库内相对路径
|
||||||
|
* @param content 源码全文
|
||||||
|
* @return 提示列表(不单独告警)
|
||||||
|
*/
|
||||||
public List<CacheReadHint> detect(String filePath, String content) {
|
public List<CacheReadHint> detect(String filePath, String content) {
|
||||||
List<CacheReadHint> result = new ArrayList<>();
|
List<CacheReadHint> result = new ArrayList<>();
|
||||||
if (content == null || content.isEmpty()) {
|
if (content == null || content.isEmpty()) {
|
||||||
@@ -69,6 +74,13 @@ public class CacheReadHintDetector {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试从单次方法调用提取读侧类型提示;非目标 API 或无法解析类型时返回 null。
|
||||||
|
* <p>
|
||||||
|
* 识别 {@code parseObject/parseArray/...} 且第 2 参为 {@code Xxx.class} 的调用,
|
||||||
|
* 解析出业务类型 FQN;若第 1 参能追溯到 Redis get,则附带 key 并将置信度提至 0.85,
|
||||||
|
* 否则仅保留类型提示(置信度 0.7)。结果供写入点类型补强,不单独告警。
|
||||||
|
*/
|
||||||
private CacheReadHint tryParseHint(MethodCallExpr mce, String filePath) {
|
private CacheReadHint tryParseHint(MethodCallExpr mce, String filePath) {
|
||||||
String name = mce.getNameAsString();
|
String name = mce.getNameAsString();
|
||||||
boolean array = ARRAY_PARSE.contains(name);
|
boolean array = ARRAY_PARSE.contains(name);
|
||||||
@@ -98,6 +110,7 @@ public class CacheReadHintDetector {
|
|||||||
: enclosing.getFullyQualifiedName().orElse(enclosing.getNameAsString());
|
: enclosing.getFullyQualifiedName().orElse(enclosing.getNameAsString());
|
||||||
SourceIndex.IndexedType context = index.get(enclosingFqn);
|
SourceIndex.IndexedType context = index.get(enclosingFqn);
|
||||||
|
|
||||||
|
// 优先带包前缀解析(如 com.foo.Bar.class),失败再退回简单类名 + import/同包
|
||||||
String fqn = index.resolveFqn(((ClassOrInterfaceType) type).getNameWithScope(), context);
|
String fqn = index.resolveFqn(((ClassOrInterfaceType) type).getNameWithScope(), context);
|
||||||
if (fqn == null) {
|
if (fqn == null) {
|
||||||
fqn = index.resolveFqn(((ClassOrInterfaceType) type).getNameAsString(), context);
|
fqn = index.resolveFqn(((ClassOrInterfaceType) type).getNameAsString(), context);
|
||||||
@@ -116,12 +129,12 @@ public class CacheReadHintDetector {
|
|||||||
hint.setRootArray(array || "parseArray".equals(name) || "getJsonToList".equals(name));
|
hint.setRootArray(array || "parseArray".equals(name) || "getJsonToList".equals(name));
|
||||||
hint.setConfidence(0.7);
|
hint.setConfidence(0.7);
|
||||||
|
|
||||||
|
// 第 1 参:原始 JSON 字符串;能关联到 redis.get(key) 时补 key,便于与写入点按 key 匹配
|
||||||
Expression rawExpr = mce.getArgument(0);
|
Expression rawExpr = mce.getArgument(0);
|
||||||
Optional<RedisGetRef> getRef = findRedisGetForVar(rawExpr, mce);
|
Optional<RedisGetRef> getRef = findRedisGetForVar(rawExpr, mce);
|
||||||
if (getRef.isPresent()) {
|
if (getRef.isPresent()) {
|
||||||
hint.setKeyExpression(getRef.get().keyExpr.toString());
|
hint.setKeyExpression(getRef.get().keyExpr.toString());
|
||||||
hint.setResolvedKeyPattern(keyResolver.resolve(
|
hint.setResolvedKeyPattern(keyResolver.resolve(getRef.get().keyExpr, enclosing, context));
|
||||||
getRef.get().keyExpr, enclosing, context));
|
|
||||||
hint.setConfidence(0.85);
|
hint.setConfidence(0.85);
|
||||||
}
|
}
|
||||||
return hint;
|
return hint;
|
||||||
@@ -155,27 +168,30 @@ public class CacheReadHintDetector {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断调用是否为 Redis 读 key,是则取出第 1 参作为 key 表达式。
|
||||||
|
* <p>
|
||||||
|
* 覆盖常见形态:{@code redisUtil.get/getString(key)}、
|
||||||
|
* {@code redisTemplate.opsForValue().get(key)}、{@code boundValueOps.get(key)}。
|
||||||
|
* {@code getString} 即使接收者名不含 redis 也接受(RedisUtil 习惯命名);
|
||||||
|
* 普通 {@code get} 依赖 scope 命中 redis / opsForValue / boundValueOps 启发式。
|
||||||
|
* 无法识别时返回 empty,不抛错。
|
||||||
|
*/
|
||||||
private Optional<RedisGetRef> extractGetKey(MethodCallExpr call) {
|
private Optional<RedisGetRef> extractGetKey(MethodCallExpr call) {
|
||||||
String name = call.getNameAsString();
|
String name = call.getNameAsString();
|
||||||
|
// 接收者字符串小写后做子串匹配,避免绑定具体类型
|
||||||
String scope = call.getScope().map(Expression::toString).orElse("").toLowerCase(Locale.ROOT);
|
String scope = call.getScope().map(Expression::toString).orElse("").toLowerCase(Locale.ROOT);
|
||||||
boolean redisScope = scope.contains("redis") || scope.contains("opsforvalue")
|
boolean redisScope = scope.contains("redis") || scope.contains("opsforvalue") || scope.contains("boundvalueops");
|
||||||
|| scope.contains("boundvalueops");
|
|
||||||
if ("get".equals(name) || "getString".equals(name)) {
|
if ("get".equals(name) || "getString".equals(name)) {
|
||||||
if (!redisScope && !REDIS_GET.contains(name)) {
|
// 非 Redis scope:仅放行 getString(RedisUtil 习惯);裸 get(如 map.get)丢弃
|
||||||
// getString 也常见于 RedisUtil
|
if (!redisScope && !"getString".equals(name)) {
|
||||||
if (!"getString".equals(name)) {
|
return Optional.empty();
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (call.getArguments().isEmpty()) {
|
if (call.getArguments().isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
return Optional.of(new RedisGetRef(call.getArgument(0)));
|
return Optional.of(new RedisGetRef(call.getArgument(0)));
|
||||||
}
|
}
|
||||||
// redisTemplate.opsForValue().get(key)
|
|
||||||
if ("get".equals(name) && scope.contains("opsforvalue") && !call.getArguments().isEmpty()) {
|
|
||||||
return Optional.of(new RedisGetRef(call.getArgument(0)));
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ public class MqReadHintDetector {
|
|||||||
this.keyResolver = new RedisKeyResolver(index);
|
this.keyResolver = new RedisKeyResolver(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描 MQ 消费侧(Listener / parse),提取 destination → 消息体类型提示。
|
||||||
|
*
|
||||||
|
* @param filePath 仓库内相对路径
|
||||||
|
* @param content 源码全文
|
||||||
|
* @return 提示列表(用于补强生产侧投递点,不单独告警)
|
||||||
|
*/
|
||||||
public List<CacheReadHint> detect(String filePath, String content) {
|
public List<CacheReadHint> detect(String filePath, String content) {
|
||||||
List<CacheReadHint> result = new ArrayList<>();
|
List<CacheReadHint> result = new ArrayList<>();
|
||||||
if (content == null || content.isEmpty()) {
|
if (content == null || content.isEmpty()) {
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ public class MqWritePointDetector {
|
|||||||
this.keyResolver = new RedisKeyResolver(index);
|
this.keyResolver = new RedisKeyResolver(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描单个 Java 源文件中的 RocketMQ / Kafka 生产侧投递点。
|
||||||
|
*
|
||||||
|
* @param filePath 仓库内相对路径(仅写入报告)
|
||||||
|
* @param content 源码全文
|
||||||
|
* @return 命中的投递点列表(可能为空)
|
||||||
|
*/
|
||||||
public List<WritePoint> detect(String filePath, String content) {
|
public List<WritePoint> detect(String filePath, String content) {
|
||||||
List<WritePoint> result = new ArrayList<>();
|
List<WritePoint> result = new ArrayList<>();
|
||||||
if (content == null || content.isEmpty() || enabledPatterns.isEmpty()) {
|
if (content == null || content.isEmpty() || enabledPatterns.isEmpty()) {
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ public class RedisWritePointDetector {
|
|||||||
this.keyResolver = new RedisKeyResolver(index);
|
this.keyResolver = new RedisKeyResolver(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描单个 Java 源文件中的 Redis 写入点。
|
||||||
|
*
|
||||||
|
* @param filePath 仓库内相对路径(仅写入报告)
|
||||||
|
* @param content 源码全文
|
||||||
|
* @return 命中的写入点列表(可能为空)
|
||||||
|
*/
|
||||||
public List<WritePoint> detect(String filePath, String content) {
|
public List<WritePoint> detect(String filePath, String content) {
|
||||||
List<WritePoint> result = new ArrayList<>();
|
List<WritePoint> result = new ArrayList<>();
|
||||||
if (content == null || content.isEmpty()) {
|
if (content == null || content.isEmpty()) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class WritePoint {
|
|||||||
return keyExpression == null ? "" : keyExpression.replaceAll("\\s+", "");
|
return keyExpression == null ? "" : keyExpression.replaceAll("\\s+", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return 是否为 MQ 通道(RocketMQ / Kafka) */
|
||||||
public boolean isMq() {
|
public boolean isMq() {
|
||||||
return CHANNEL_ROCKETMQ.equals(channel) || CHANNEL_KAFKA.equals(channel);
|
return CHANNEL_ROCKETMQ.equals(channel) || CHANNEL_KAFKA.equals(channel);
|
||||||
}
|
}
|
||||||
@@ -136,6 +137,9 @@ public class WritePoint {
|
|||||||
this.channel = channel == null || channel.isEmpty() ? CHANNEL_REDIS : channel;
|
this.channel = channel == null || channel.isEmpty() ? CHANNEL_REDIS : channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 展示用写入位置,形如 {@code SimpleClass#method:line}
|
||||||
|
*/
|
||||||
public String location() {
|
public String location() {
|
||||||
String simpleClass = enclosingClass;
|
String simpleClass = enclosingClass;
|
||||||
if (simpleClass != null && simpleClass.contains(".")) {
|
if (simpleClass != null && simpleClass.contains(".")) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.codechecker.cache.diff;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 结构变更类型及其默认严重级别。
|
* 结构变更类型及其默认严重级别。
|
||||||
|
* <p>用于 Diff 结果分类与报告展示标签;实际严重级别可被 {@code severity_overrides} 覆盖。</p>
|
||||||
|
* @author DD
|
||||||
*/
|
*/
|
||||||
public enum ChangeType {
|
public enum ChangeType {
|
||||||
FIELD_REMOVED(Severity.P0, "字段删除"),
|
FIELD_REMOVED(Severity.P0, "字段删除"),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package com.codechecker.cache.diff;
|
package com.codechecker.cache.diff;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一条结构变更记录。
|
* 一条结构变更记录:关联 key/destination、写入位置、字段路径与前后取值说明。
|
||||||
|
* <p>由 {@link SchemaDiffer} 或分析器在「新增/删除写入点」时生成,再进入报告明细。</p>
|
||||||
*/
|
*/
|
||||||
public class SchemaChange {
|
public class SchemaChange {
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
package com.codechecker.cache.diff;
|
package com.codechecker.cache.diff;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 变更严重级别。
|
* 变更严重级别(默认由 {@link ChangeType} 给出,可被配置覆盖)。
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #P0} — 高危:字段删除、类型变更、包装层/路径迁移等</li>
|
||||||
|
* <li>{@link #P1} — 中危:新增字段、删除写入点等</li>
|
||||||
|
* <li>{@link #P2} — 低危/提示:新增写入点、低置信度变更等</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public enum Severity {
|
public enum Severity {
|
||||||
|
/** 高危结构破坏 */
|
||||||
P0,
|
P0,
|
||||||
|
/** 中危兼容性风险 */
|
||||||
P1,
|
P1,
|
||||||
|
/** 低危或需人工确认 */
|
||||||
P2
|
P2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package com.codechecker.cache.git;
|
package com.codechecker.cache.git;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Git 操作异常。
|
* Git 命令执行失败时抛出的受检异常(如 {@code git diff}/{@code git show} 非零退出)。
|
||||||
*/
|
*/
|
||||||
public class GitException extends Exception {
|
public class GitException extends Exception {
|
||||||
|
|
||||||
|
/** @param message 失败说明(通常含命令与 stderr 摘要) */
|
||||||
public GitException(String message) {
|
public GitException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param message 失败说明 @param cause 底层原因 */
|
||||||
public GitException(String message, Throwable cause) {
|
public GitException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ public class RedisKeyResolver {
|
|||||||
this.index = index;
|
this.index = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 key / destination 表达式推断为静态模式字符串。
|
||||||
|
*
|
||||||
|
* @param keyExpr AST 表达式(字面量、常量拼接、format 等)
|
||||||
|
* @param enclosingClass 所在类(用于查常量/方法)
|
||||||
|
* @param context 类型索引上下文,可为 {@code null}
|
||||||
|
* @return 规范化模式;动态段为 {@code *},无法推断时多为 {@code *} 或 {@code unknown-key} 上游处理
|
||||||
|
*/
|
||||||
public String resolve(Expression keyExpr, ClassOrInterfaceDeclaration enclosingClass,
|
public String resolve(Expression keyExpr, ClassOrInterfaceDeclaration enclosingClass,
|
||||||
SourceIndex.IndexedType context) {
|
SourceIndex.IndexedType context) {
|
||||||
String raw = resolveExpr(keyExpr, enclosingClass, context, 0);
|
String raw = resolveExpr(keyExpr, enclosingClass, context, 0);
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企业微信群机器人通知(markdown 消息)。
|
* 企业微信群机器人通知:发送 markdown 消息(单条或多条按序发送)。
|
||||||
|
* <p>Webhook 为空时跳过;HTTP/errcode 失败时打 stderr 日志并返回 false。</p>
|
||||||
*/
|
*/
|
||||||
public class WeComNotifier {
|
public class WeComNotifier {
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一次检测的完整结果。
|
* 一次检测的完整结果:元信息、字段级 {@link SchemaChange} 列表、按 Key/Topic 聚合的
|
||||||
|
* {@link KeyStructureChange},以及是否阻断流水线与建议退出码。
|
||||||
*/
|
*/
|
||||||
public class CheckReport {
|
public class CheckReport {
|
||||||
|
|
||||||
@@ -24,10 +25,12 @@ public class CheckReport {
|
|||||||
private boolean blocked;
|
private boolean blocked;
|
||||||
private int exitCode;
|
private int exitCode;
|
||||||
|
|
||||||
|
/** @return 是否存在字段级或 Key 级结构变更 */
|
||||||
public boolean hasChanges() {
|
public boolean hasChanges() {
|
||||||
return !changes.isEmpty() || !keyChanges.isEmpty();
|
return !changes.isEmpty() || !keyChanges.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return 字段明细中指定严重级别的条数 */
|
||||||
public long count(Severity severity) {
|
public long count(Severity severity) {
|
||||||
return changes.stream().filter(c -> c.getSeverity() == severity).count();
|
return changes.stream().filter(c -> c.getSeverity() == severity).count();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,12 @@ public class KeyStructureChange {
|
|||||||
this.channel = channel == null || channel.isEmpty() ? "REDIS" : channel;
|
this.channel = channel == null || channel.isEmpty() ? "REDIS" : channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return 是否为 MQ 通道 */
|
||||||
public boolean isMq() {
|
public boolean isMq() {
|
||||||
return "ROCKETMQ".equals(channel) || "KAFKA".equals(channel);
|
return "ROCKETMQ".equals(channel) || "KAFKA".equals(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return 企微展示用通道名(RocketMQ / Kafka / Redis) */
|
||||||
public String channelDisplay() {
|
public String channelDisplay() {
|
||||||
if ("ROCKETMQ".equals(channel)) {
|
if ("ROCKETMQ".equals(channel)) {
|
||||||
return "RocketMQ";
|
return "RocketMQ";
|
||||||
@@ -117,6 +119,11 @@ public class KeyStructureChange {
|
|||||||
return fieldDetails;
|
return fieldDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用更严重的级别抬升当前聚合严重度(ordinal 更小者优先)。
|
||||||
|
*
|
||||||
|
* @param candidate 候选级别;为 null 时忽略
|
||||||
|
*/
|
||||||
public void raiseSeverity(Severity candidate) {
|
public void raiseSeverity(Severity candidate) {
|
||||||
if (candidate == null) {
|
if (candidate == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
package com.codechecker.cache.schema;
|
package com.codechecker.cache.schema;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 序列化后 JSON 值的粗粒度类型。
|
* 序列化后 JSON 值的粗粒度类型,用于 Schema 展开与骨架占位符选择。
|
||||||
*/
|
*/
|
||||||
public enum JsonType {
|
public enum JsonType {
|
||||||
|
/** JSON 对象 */
|
||||||
OBJECT,
|
OBJECT,
|
||||||
|
/** JSON 数组 */
|
||||||
ARRAY,
|
ARRAY,
|
||||||
|
/** Map 结构 */
|
||||||
MAP,
|
MAP,
|
||||||
|
/** 字符串 */
|
||||||
STRING,
|
STRING,
|
||||||
|
/** 数值 */
|
||||||
NUMBER,
|
NUMBER,
|
||||||
|
/** 布尔 */
|
||||||
BOOLEAN,
|
BOOLEAN,
|
||||||
|
/** 无法判定 */
|
||||||
UNKNOWN
|
UNKNOWN
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ public class SkeletonJsonRenderer {
|
|||||||
/** 单侧骨架默认最大长度(企微 markdown 总长约 4096)。 */
|
/** 单侧骨架默认最大长度(企微 markdown 总长约 4096)。 */
|
||||||
public static final int DEFAULT_MAX_LEN = 1500;
|
public static final int DEFAULT_MAX_LEN = 1500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染完整骨架 JSON,不做长度截断。
|
||||||
|
*
|
||||||
|
* @param schema 扁平 Schema;空或 null 时返回 {@code "{}"}
|
||||||
|
*/
|
||||||
public String render(TypeSchema schema) {
|
public String render(TypeSchema schema) {
|
||||||
return render(schema, null, Integer.MAX_VALUE);
|
return render(schema, null, Integer.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ public class TypeSchema {
|
|||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按字段 path 放入(同 path 后者覆盖)。 */
|
||||||
public void add(FieldSchema field) {
|
public void add(FieldSchema field) {
|
||||||
fields.put(field.getPath(), field);
|
fields.put(field.getPath(), field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return 是否无任何可展开字段(空 Schema / 无法解析类型时为 true) */
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return fields.isEmpty();
|
return fields.isEmpty();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user