feat: w06 类型推断增强辅助
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
92
src/main/java/com/codechecker/cache/detector/CacheReadHint.java
vendored
Normal file
92
src/main/java/com/codechecker/cache/detector/CacheReadHint.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
189
src/main/java/com/codechecker/cache/detector/CacheReadHintDetector.java
vendored
Normal file
189
src/main/java/com/codechecker/cache/detector/CacheReadHintDetector.java
vendored
Normal 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<Foo> 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)) {
|
||||
// 部分 API:parseObject(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user