feat: w06 类型推断增强辅助
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped

This commit is contained in:
2026-07-14 18:05:08 +08:00
parent c780b269d6
commit 89a48a98aa
9 changed files with 494 additions and 3 deletions

View File

@@ -1,6 +1,8 @@
package com.codechecker.cache.analyze;
import com.codechecker.cache.config.CheckerConfig;
import com.codechecker.cache.detector.CacheReadHint;
import com.codechecker.cache.detector.CacheReadHintDetector;
import com.codechecker.cache.detector.RedisWritePointDetector;
import com.codechecker.cache.detector.WritePoint;
import com.codechecker.cache.diff.ChangeType;
@@ -111,6 +113,10 @@ public class SchemaCheckAnalyzer {
List<WritePoint> newWps = detectorNew.detect(path, newContent);
List<WritePoint> oldWps = oldContent == null
? new ArrayList<>() : detectorOld.detect(path, oldContent);
applyReadHints(newWps, path, newContent, newIndex);
if (oldContent != null) {
applyReadHints(oldWps, path, oldContent, oldIndex);
}
newWps.forEach(this::applyManualMappings);
oldWps.forEach(this::applyManualMappings);
@@ -425,6 +431,92 @@ public class SchemaCheckAnalyzer {
}
}
/**
* W06用同文件读侧反序列化类型提示补强低置信度 / 缺类型的写入点。
* manual_mappings 仍在其后执行,可覆盖本补强结果。
*/
private void applyReadHints(List<WritePoint> writePoints, String path, String content,
SourceIndex index) {
if (!config.getDetection().isReadHintsEnabled()
|| writePoints == null || writePoints.isEmpty()) {
return;
}
List<CacheReadHint> hints = new CacheReadHintDetector(index).detect(path, content);
if (hints.isEmpty()) {
return;
}
for (WritePoint wp : writePoints) {
enrichWritePointFromHints(wp, hints);
}
}
private void enrichWritePointFromHints(WritePoint wp, List<CacheReadHint> hints) {
boolean needType = wp.getResolvedValueType() == null || wp.getResolvedValueType().isEmpty()
|| wp.getConfidence() < config.getDetection().getMinConfidence();
boolean needKey = isUnresolvedKey(wp.getResolvedKeyPattern());
if (!needType && !needKey) {
return;
}
CacheReadHint best = null;
int bestScore = -1;
for (CacheReadHint hint : hints) {
int score = scoreHint(wp, hint);
if (score > bestScore) {
bestScore = score;
best = hint;
}
}
if (best == null || bestScore <= 0) {
return;
}
if (needType && best.getResolvedValueType() != null) {
wp.setResolvedValueType(best.getResolvedValueType());
wp.setRootArray(best.isRootArray());
wp.setConfidence(Math.max(wp.getConfidence(), best.getConfidence()));
}
if (needKey && best.getResolvedKeyPattern() != null
&& !isUnresolvedKey(best.getResolvedKeyPattern())) {
wp.setResolvedKeyPattern(best.getResolvedKeyPattern());
if (best.getKeyExpression() != null) {
// 保留写入侧原始表达式,仅补 key 模式
}
}
}
/** 匹配得分:同 key > 同类同方法 > 同类;无交集则 0。 */
private int scoreHint(WritePoint wp, CacheReadHint hint) {
if (hint.getResolvedValueType() == null) {
return 0;
}
boolean sameClass = wp.getEnclosingClass() != null
&& wp.getEnclosingClass().equals(hint.getEnclosingClass());
if (!sameClass) {
// 跨文件仅允许 key 模式已解析且一致
if (wp.getResolvedKeyPattern() != null
&& wp.getResolvedKeyPattern().equals(hint.getResolvedKeyPattern())
&& !isUnresolvedKey(wp.getResolvedKeyPattern())) {
return 40;
}
return 0;
}
int score = 10;
if (wp.getEnclosingMethod() != null
&& wp.getEnclosingMethod().equals(hint.getEnclosingMethod())) {
score += 20;
}
if (wp.getResolvedKeyPattern() != null
&& wp.getResolvedKeyPattern().equals(hint.getResolvedKeyPattern())
&& !isUnresolvedKey(wp.getResolvedKeyPattern())) {
score += 50;
} else if (hint.getResolvedKeyPattern() != null
&& !isUnresolvedKey(hint.getResolvedKeyPattern())
&& isUnresolvedKey(wp.getResolvedKeyPattern())) {
score += 30;
}
return score;
}
private void collectTypeNames(String content, Set<String> out) {
if (content == null || content.isEmpty()) {
return;

View File

@@ -110,6 +110,8 @@ public class CheckerConfig {
private List<String> patterns = new ArrayList<>();
private double minConfidence = 0.6;
private int maxFieldDepth = 8;
/** W06是否启用读侧反序列化类型辅助补强 */
private boolean readHintsEnabled = true;
public List<String> getPatterns() {
return patterns;
@@ -134,6 +136,14 @@ public class CheckerConfig {
public void setMaxFieldDepth(int maxFieldDepth) {
this.maxFieldDepth = maxFieldDepth;
}
public boolean isReadHintsEnabled() {
return readHintsEnabled;
}
public void setReadHintsEnabled(boolean readHintsEnabled) {
this.readHintsEnabled = readHintsEnabled;
}
}
public static class ManualMapping {

View File

@@ -100,6 +100,7 @@ public final class ConfigLoader {
d.setPatterns(strList(detection.get("patterns")));
d.setMinConfidence(dbl(detection, "min_confidence", 0.6));
d.setMaxFieldDepth((int) lng(detection, "max_field_depth", 8));
d.setReadHintsEnabled(bool(detection, "read_hints_enabled", true));
Map<String, Object> severityOverrides = asMap(map.get("severity_overrides"));
Map<String, String> so = new LinkedHashMap<>();

View File

@@ -0,0 +1,92 @@
package com.codechecker.cache.detector;
/**
* 读侧反序列化类型提示W06从 parseObject / getJsonToBean 等推断「缓存 value 被当成什么类型用」。
* 不单独产生告警,仅用于补强同文件/同 key 写入点的 value 类型。
*/
public class CacheReadHint {
private String filePath;
private int lineNumber;
private String enclosingClass;
private String enclosingMethod;
/** 推断出的 key 模式;无法关联 redis get 时为 null */
private String resolvedKeyPattern;
private String keyExpression;
/** value 元素/对象 FQN */
private String resolvedValueType;
private boolean rootArray;
private double confidence = 0.7;
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public int getLineNumber() {
return lineNumber;
}
public void setLineNumber(int lineNumber) {
this.lineNumber = lineNumber;
}
public String getEnclosingClass() {
return enclosingClass;
}
public void setEnclosingClass(String enclosingClass) {
this.enclosingClass = enclosingClass;
}
public String getEnclosingMethod() {
return enclosingMethod;
}
public void setEnclosingMethod(String enclosingMethod) {
this.enclosingMethod = enclosingMethod;
}
public String getResolvedKeyPattern() {
return resolvedKeyPattern;
}
public void setResolvedKeyPattern(String resolvedKeyPattern) {
this.resolvedKeyPattern = resolvedKeyPattern;
}
public String getKeyExpression() {
return keyExpression;
}
public void setKeyExpression(String keyExpression) {
this.keyExpression = keyExpression;
}
public String getResolvedValueType() {
return resolvedValueType;
}
public void setResolvedValueType(String resolvedValueType) {
this.resolvedValueType = resolvedValueType;
}
public boolean isRootArray() {
return rootArray;
}
public void setRootArray(boolean rootArray) {
this.rootArray = rootArray;
}
public double getConfidence() {
return confidence;
}
public void setConfidence(double confidence) {
this.confidence = confidence;
}
}

View File

@@ -0,0 +1,189 @@
package com.codechecker.cache.detector;
import com.codechecker.cache.key.RedisKeyResolver;
import com.codechecker.cache.schema.SourceIndex;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.CallableDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.ClassExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
/**
* W06扫描读侧反序列化提取「缓存字符串 → 业务类型」提示,供写入点类型补强。
* <p>典型模式:</p>
* <pre>
* String raw = redisUtil.getString(key);
* Foo vo = JSON.parseObject(raw, Foo.class);
* List&lt;Foo&gt; list = JSON.parseArray(raw, Foo.class);
* </pre>
*/
public class CacheReadHintDetector {
private static final Set<String> OBJECT_PARSE = new HashSet<>(Arrays.asList(
"parseObject", "parse", "getJsonToBean", "toJavaObject", "readValue"));
private static final Set<String> ARRAY_PARSE = new HashSet<>(Arrays.asList(
"parseArray", "getJsonToList", "parseArrayObject"));
private static final Set<String> REDIS_GET = new HashSet<>(Arrays.asList(
"get", "getString", "opsForValue"));
private final SourceIndex index;
private final RedisKeyResolver keyResolver;
public CacheReadHintDetector(SourceIndex index) {
this.index = index;
this.keyResolver = new RedisKeyResolver(index);
}
public List<CacheReadHint> detect(String filePath, String content) {
List<CacheReadHint> result = new ArrayList<>();
if (content == null || content.isEmpty()) {
return result;
}
CompilationUnit cu;
try {
cu = StaticJavaParser.parse(content);
} catch (RuntimeException e) {
return result;
}
for (MethodCallExpr mce : cu.findAll(MethodCallExpr.class)) {
CacheReadHint hint = tryParseHint(mce, filePath);
if (hint != null) {
result.add(hint);
}
}
return result;
}
private CacheReadHint tryParseHint(MethodCallExpr mce, String filePath) {
String name = mce.getNameAsString();
boolean array = ARRAY_PARSE.contains(name);
boolean object = OBJECT_PARSE.contains(name);
if (!array && !object) {
return null;
}
if (mce.getArguments().size() < 2) {
return null;
}
Expression classArg = mce.getArgument(1);
// readValue(str, TypeReference) 等暂不支持;要求 ClassLiteral Xxx.class
if (!(classArg instanceof ClassExpr)) {
// 部分 APIparseObject(str, Xxx.class, Feature...) 仍是第 2 参
return null;
}
Type type = ((ClassExpr) classArg).getType();
if (!(type instanceof ClassOrInterfaceType)) {
return null;
}
ClassOrInterfaceDeclaration enclosing = mce
.findAncestor(ClassOrInterfaceDeclaration.class).orElse(null);
String enclosingFqn = enclosing == null
? "<unknown>"
: enclosing.getFullyQualifiedName().orElse(enclosing.getNameAsString());
SourceIndex.IndexedType context = index.get(enclosingFqn);
String fqn = index.resolveFqn(((ClassOrInterfaceType) type).getNameWithScope(), context);
if (fqn == null) {
fqn = index.resolveFqn(((ClassOrInterfaceType) type).getNameAsString(), context);
}
if (fqn == null) {
return null;
}
CacheReadHint hint = new CacheReadHint();
hint.setFilePath(filePath);
hint.setLineNumber(mce.getBegin().map(p -> p.line).orElse(0));
hint.setEnclosingClass(enclosingFqn);
hint.setEnclosingMethod(mce.findAncestor(CallableDeclaration.class)
.map(CallableDeclaration::getNameAsString).orElse("<unknown>"));
hint.setResolvedValueType(fqn);
hint.setRootArray(array || "parseArray".equals(name) || "getJsonToList".equals(name));
hint.setConfidence(0.7);
Expression rawExpr = mce.getArgument(0);
Optional<RedisGetRef> getRef = findRedisGetForVar(rawExpr, mce);
if (getRef.isPresent()) {
hint.setKeyExpression(getRef.get().keyExpr.toString());
hint.setResolvedKeyPattern(keyResolver.resolve(
getRef.get().keyExpr, enclosing, context));
hint.setConfidence(0.85);
}
return hint;
}
/**
* 若 parse 的第 1 参是局部变量,追溯其是否来自 redis get(key)。
*/
private Optional<RedisGetRef> findRedisGetForVar(Expression rawExpr, MethodCallExpr parseCall) {
if (!(rawExpr instanceof NameExpr)) {
// 直接 parseObject(redis.get(key), Xxx.class)
if (rawExpr instanceof MethodCallExpr) {
return extractGetKey((MethodCallExpr) rawExpr);
}
return Optional.empty();
}
String varName = ((NameExpr) rawExpr).getNameAsString();
Optional<CallableDeclaration> callable = parseCall.findAncestor(CallableDeclaration.class);
if (!callable.isPresent()) {
return Optional.empty();
}
for (VariableDeclarator var : callable.get().findAll(VariableDeclarator.class)) {
if (!var.getNameAsString().equals(varName) || !var.getInitializer().isPresent()) {
continue;
}
Expression init = var.getInitializer().get();
if (init instanceof MethodCallExpr) {
return extractGetKey((MethodCallExpr) init);
}
}
return Optional.empty();
}
private Optional<RedisGetRef> extractGetKey(MethodCallExpr call) {
String name = call.getNameAsString();
String scope = call.getScope().map(Expression::toString).orElse("").toLowerCase(Locale.ROOT);
boolean redisScope = scope.contains("redis") || scope.contains("opsforvalue")
|| scope.contains("boundvalueops");
if ("get".equals(name) || "getString".equals(name)) {
if (!redisScope && !REDIS_GET.contains(name)) {
// getString 也常见于 RedisUtil
if (!"getString".equals(name)) {
return Optional.empty();
}
}
if (call.getArguments().isEmpty()) {
return Optional.empty();
}
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();
}
private static final class RedisGetRef {
final Expression keyExpr;
RedisGetRef(Expression keyExpr) {
this.keyExpr = keyExpr;
}
}
}

View File

@@ -44,7 +44,9 @@ detection:
- W03 # stringRedisTemplate.opsForValue().set(key, JsonUtil.getObjectToString(x), ...)
- W04 # redisTemplate.opsForValue().set(key, obj, ...)
- W05 # redisTemplate.opsForHash().put(key, field, obj)
# 类型推断最低置信度,低于此值降级为 P2 提示
# W06 读侧辅助:用 parseObject / getJsonToBean 等补强写入点 value 类型(非写入模式)
read_hints_enabled: true
# 类型推断最低置信度,低于此值标记为低置信度提示
min_confidence: 0.6
# 字段展开最大深度(防止循环引用)
max_field_depth: 8