clazz = contextCall.findAncestor(ClassOrInterfaceDeclaration.class);
if (clazz.isPresent()) {
for (MethodDeclaration md : clazz.get().getMethods()) {
if (md.getNameAsString().equals(call.getNameAsString())) {
diff --git a/src/main/java/com/codechecker/cache/detector/RedisWritePointDetector.java b/src/main/java/com/codechecker/cache/detector/RedisWritePointDetector.java
index 8212d41..210bf08 100644
--- a/src/main/java/com/codechecker/cache/detector/RedisWritePointDetector.java
+++ b/src/main/java/com/codechecker/cache/detector/RedisWritePointDetector.java
@@ -9,7 +9,6 @@ import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
-import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.BinaryExpr;
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
@@ -34,6 +33,8 @@ import java.util.Set;
/**
* 从单个 Java 源文件中检测 Redis value 写入点(W01~W05)。
+ *
+ * V1.1 收敛:忽略无结构标量;局部 JSON 字符串 unwrap;W04/W05 仅保留本仓库可展开业务类型。
*/
public class RedisWritePointDetector {
@@ -48,6 +49,7 @@ public class RedisWritePointDetector {
"randomUUID", "toString", "valueOf"));
private static final Set COLLECTION_SIMPLE = new HashSet<>(Arrays.asList(
"List", "ArrayList", "LinkedList", "Set", "HashSet", "Collection"));
+ private static final Set DIRECT_OBJECT_PATTERNS = new HashSet<>(Arrays.asList("W04", "W05"));
private final SourceIndex index;
private final Set enabledPatterns;
@@ -97,12 +99,24 @@ public class RedisWritePointDetector {
Expression valueArg = mce.getArgument(1);
Expression serialized = unwrapSerializer(valueArg);
+ String serializerName = null;
+ if (serialized != null) {
+ serializerName = ((MethodCallExpr) valueArg).getNameAsString();
+ } else {
+ serialized = unwrapJsonStringVar(valueArg, mce);
+ if (serialized != null) {
+ serializerName = serializerMethodOfLocal(valueArg, mce);
+ }
+ }
+
String pattern;
Expression typeExpr;
if (serialized != null) {
- pattern = classifySerialized(method, valueArg);
+ pattern = classifySerialized(method, serializerName);
typeExpr = serialized;
- } else if (enabledPatterns.contains("W04") && !isTrivialValue(valueArg)) {
+ } else if (enabledPatterns.contains("W04")
+ && !isTrivialValue(valueArg)
+ && !isBareScalarTypeExpr(valueArg, mce)) {
pattern = "W04";
typeExpr = valueArg;
} else {
@@ -111,6 +125,9 @@ public class RedisWritePointDetector {
if (!enabledPatterns.contains(pattern)) {
return null;
}
+ if (isBareScalarTypeExpr(typeExpr, mce)) {
+ return null;
+ }
return buildWritePoint(mce, filePath, pattern, mce.getArgument(0), valueArg, typeExpr);
}
@@ -126,7 +143,13 @@ public class RedisWritePointDetector {
return null;
}
Expression serialized = unwrapSerializer(valueArg);
+ if (serialized == null) {
+ serialized = unwrapJsonStringVar(valueArg, mce);
+ }
Expression typeExpr = serialized != null ? serialized : valueArg;
+ if (isBareScalarTypeExpr(typeExpr, mce)) {
+ return null;
+ }
return buildWritePoint(mce, filePath, "W05", mce.getArgument(0), valueArg, typeExpr);
}
@@ -152,9 +175,23 @@ public class RedisWritePointDetector {
} else {
wp.setConfidence(0.4);
}
+
+ if (DIRECT_OBJECT_PATTERNS.contains(pattern) && !hasResolvableBusinessType(inferred)) {
+ return null;
+ }
return wp;
}
+ private boolean hasResolvableBusinessType(InferredType inferred) {
+ if (inferred == null || inferred.fqn == null || inferred.fqn.isEmpty()) {
+ return false;
+ }
+ if (BareValueTypes.isBareFqn(inferred.fqn)) {
+ return false;
+ }
+ return index.get(inferred.fqn) != null;
+ }
+
private boolean isValueOpsScope(MethodCallExpr mce) {
String scope = mce.getScope().map(Expression::toString).orElse("");
String lower = scope.toLowerCase();
@@ -192,9 +229,36 @@ public class RedisWritePointDetector {
}
}
if (expr instanceof ObjectCreationExpr) {
- ObjectCreationExpr oce = (ObjectCreationExpr) expr;
- String typeName = oce.getType().getNameAsString();
- return "UUID".equals(typeName);
+ return "UUID".equals(((ObjectCreationExpr) expr).getType().getNameAsString());
+ }
+ return false;
+ }
+
+ private boolean isBareScalarTypeExpr(Expression expr, MethodCallExpr contextCall) {
+ if (expr == null) {
+ return false;
+ }
+ if (expr instanceof NameExpr) {
+ Type declared = findVariableType(((NameExpr) expr).getNameAsString(), contextCall);
+ return declared != null && BareValueTypes.isBareType(declared);
+ }
+ if (expr instanceof CastExpr) {
+ return BareValueTypes.isBareType(((CastExpr) expr).getType());
+ }
+ if (expr instanceof ObjectCreationExpr) {
+ return BareValueTypes.isBareType(((ObjectCreationExpr) expr).getType());
+ }
+ if (expr instanceof MethodCallExpr) {
+ MethodCallExpr call = (MethodCallExpr) expr;
+ Optional clazz = contextCall
+ .findAncestor(ClassOrInterfaceDeclaration.class);
+ if (clazz.isPresent()) {
+ for (MethodDeclaration md : clazz.get().getMethods()) {
+ if (md.getNameAsString().equals(call.getNameAsString())) {
+ return BareValueTypes.isBareType(md.getType());
+ }
+ }
+ }
}
return false;
}
@@ -209,9 +273,52 @@ public class RedisWritePointDetector {
return null;
}
- private String classifySerialized(String method, Expression valueArg) {
- String serializer = valueArg instanceof MethodCallExpr
- ? ((MethodCallExpr) valueArg).getNameAsString() : "";
+ /** 局部 {@code String json = toJSONString(x)} 再写入:回溯到 {@code x}。 */
+ private Expression unwrapJsonStringVar(Expression valueArg, MethodCallExpr mce) {
+ if (!(valueArg instanceof NameExpr)) {
+ return null;
+ }
+ String name = ((NameExpr) valueArg).getNameAsString();
+ Type declared = findVariableType(name, mce);
+ if (declared != null) {
+ String simple = declared.isClassOrInterfaceType()
+ ? declared.asClassOrInterfaceType().getNameAsString()
+ : declared.asString();
+ if (!"String".equals(simple)) {
+ return null;
+ }
+ }
+ return unwrapSerializer(findLocalInitializer(name, mce));
+ }
+
+ private String serializerMethodOfLocal(Expression valueArg, MethodCallExpr mce) {
+ if (!(valueArg instanceof NameExpr)) {
+ return "";
+ }
+ Expression init = findLocalInitializer(((NameExpr) valueArg).getNameAsString(), mce);
+ if (init instanceof MethodCallExpr) {
+ String name = ((MethodCallExpr) init).getNameAsString();
+ if (SERIALIZE_METHODS.contains(name)) {
+ return name;
+ }
+ }
+ return "";
+ }
+
+ private Expression findLocalInitializer(String name, MethodCallExpr mce) {
+ Optional callable = mce.findAncestor(CallableDeclaration.class);
+ if (!callable.isPresent()) {
+ return null;
+ }
+ for (VariableDeclarator var : callable.get().findAll(VariableDeclarator.class)) {
+ if (var.getNameAsString().equals(name) && var.getInitializer().isPresent()) {
+ return var.getInitializer().get();
+ }
+ }
+ return null;
+ }
+
+ private String classifySerialized(String method, String serializer) {
if ("insert".equals(method)) {
return "W01";
}
@@ -237,12 +344,10 @@ public class RedisWritePointDetector {
return resolveTypeNode(((CastExpr) expr).getType(), context);
}
if (expr instanceof ObjectCreationExpr) {
- ClassOrInterfaceType t = ((ObjectCreationExpr) expr).getType();
- return resolveTypeNode(t, context);
+ return resolveTypeNode(((ObjectCreationExpr) expr).getType(), context);
}
if (expr instanceof NameExpr) {
- String name = ((NameExpr) expr).getNameAsString();
- Type declared = findVariableType(name, contextCall);
+ Type declared = findVariableType(((NameExpr) expr).getNameAsString(), contextCall);
if (declared != null) {
return resolveTypeNode(declared, context);
}
diff --git a/src/test/java/com/codechecker/cache/RecordingTodoReplaceScenarioTest.java b/src/test/java/com/codechecker/cache/RecordingTodoReplaceScenarioTest.java
index 7d65bbe..781d0cc 100644
--- a/src/test/java/com/codechecker/cache/RecordingTodoReplaceScenarioTest.java
+++ b/src/test/java/com/codechecker/cache/RecordingTodoReplaceScenarioTest.java
@@ -2,48 +2,27 @@ package com.codechecker.cache;
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.report.CheckReport;
-import com.codechecker.cache.report.KeyStructureChange;
-import com.codechecker.cache.report.ReportBuilder;
-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 org.junit.jupiter.api.Test;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.stream.Collectors;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
- * 复现业务仓实跑误报(jnpf-java-cloud / RecordingTodoDomainServiceImpl#replaceText):
- *
- * - Key --> recording:todo:replace:*:*
- * 类型: <未解析>
- * value 新增为: “{}”
- * - Key --> recording:todo:replace:title:*:*
- * 类型: <未解析>
- * value 新增为: “{}”
- *
- * 根因:{@code stringRedisTemplate.opsForValue().set(key, String变量)} 被 W04 命中,
- * String 无法展开 Schema → 空骨架 + WRITE_POINT_ADDED。
+ * 业务仓 RecordingTodoDomainServiceImpl#replaceText:StringRedisTemplate 缓存原文 String。
+ * V1.1 收敛后不应再检出写入点(修复前会误报 W04 + 类型未解析 + value 新增为 "{}")。
*/
class RecordingTodoReplaceScenarioTest {
- private static final Set PATTERNS = new HashSet<>(Arrays.asList("W01", "W02", "W03", "W04", "W05"));
+ private static final Set PATTERNS = new HashSet<>(Arrays.asList(
+ "W01", "W02", "W03", "W04", "W05"));
@Test
- void reproducesUnresolvedStringCacheFalsePositiveLikeWeCom() {
+ void ignoresPlainStringCacheWritesAfterConvergence() {
String constants = TestSupport.fixture("fixtures/recording-todo/RecordingTodoConstants.txt");
String service = TestSupport.fixture("fixtures/recording-todo/RecordingTodoDomainServiceImpl.txt");
@@ -51,89 +30,10 @@ class RecordingTodoReplaceScenarioTest {
index.addSource(constants);
index.addSource(service);
- List wps = new RedisWritePointDetector(index, PATTERNS).detect("RecordingTodoDomainServiceImpl.java", service);
- wps = wps.stream().sorted(Comparator.comparing(WritePoint::getLineNumber)).collect(Collectors.toList());
+ List wps = new RedisWritePointDetector(index, PATTERNS)
+ .detect("RecordingTodoDomainServiceImpl.java", service);
- assertEquals(2, wps.size(), "应检出 content / title 两处 set");
-
- WritePoint contentWp = wps.get(0);
- WritePoint titleWp = wps.get(1);
-
- // --- 检测层:与实跑一致 ---
- assertEquals("W04", contentWp.getPattern());
- assertEquals("W04", titleWp.getPattern());
- assertEquals("recording:todo:replace:*:*", contentWp.getResolvedKeyPattern());
- assertEquals("recording:todo:replace:title:*:*", titleWp.getResolvedKeyPattern());
- assertNull(contentWp.getResolvedValueType(), "String 不在本仓 SourceIndex,FQN 应为 null");
- assertNull(titleWp.getResolvedValueType());
- assertTrue(contentWp.getEnclosingMethod().contains("replaceText") || "replaceText".equals(contentWp.getEnclosingMethod()));
- assertTrue(contentWp.getEnclosingClass().endsWith("RecordingTodoDomainServiceImpl"));
-
- // --- Schema:空结构 → "{}" ---
- JavaSchemaExtractor extractor = new JavaSchemaExtractor(index, 8);
- SkeletonJsonRenderer renderer = new SkeletonJsonRenderer();
- TypeSchema contentSchema = extractor.extract(contentWp.getResolvedValueType(), contentWp.isRootArray());
- TypeSchema titleSchema = extractor.extract(titleWp.getResolvedValueType(), titleWp.isRootArray());
- assertTrue(contentSchema.isEmpty());
- assertTrue(titleSchema.isEmpty());
- assertEquals("{}", renderer.render(contentSchema));
- assertEquals("{}", renderer.render(titleSchema));
-
- // --- 报告层:复现企微「value 新增为 / 类型未解析」 ---
- CheckReport report = buildAddedWritePointReport(contentWp, titleWp, extractor, renderer);
- String md = new ReportBuilder("【序列化结构变更】").toMarkdown(report);
-
- assertTrue(md.contains("recording:todo:replace:*:*") || md.contains("recording:todo:replace:*:*"), "应包含 content key(企微转义后可为全角*),实际:\n" + md);
- assertTrue(md.contains("recording:todo:replace:title:*:*") || md.contains("recording:todo:replace:title:*:*"), "应包含 title key,实际:\n" + md);
- assertTrue(md.contains("<未解析>"), "类型应展示为 <未解析>,实际:\n" + md);
- assertTrue(md.contains("value 新增为"), "应走新增写入文案,实际:\n" + md);
- assertTrue(md.contains("“{}”") || md.contains("\"{}\"") || md.contains("{}"), "骨架应为空对象 {},实际:\n" + md);
- assertTrue(md.contains("RecordingTodoDomainServiceImpl#replaceText"), "位置应指向 replaceText,实际:\n" + md);
- }
-
- /**
- * 模拟 SchemaCheckAnalyzer 对「文件新增写入点」的聚合与展示路径(无 git)。
- */
- private static CheckReport buildAddedWritePointReport(WritePoint contentWp, WritePoint titleWp,
- JavaSchemaExtractor extractor,
- SkeletonJsonRenderer renderer) {
- CheckReport report = new CheckReport();
- report.setRepository("jnpf-java-cloud");
- report.setBranch("patrol/dev_patrol_2.0");
- report.setOldSha("adbb15c6");
- report.setNewSha("6ddc72dc");
- report.setModifier("xgl");
- report.setModifyTime("2026-07-29 17:57:02");
-
- for (WritePoint wp : Arrays.asList(contentWp, titleWp)) {
- SchemaChange added = new SchemaChange(ChangeType.WRITE_POINT_ADDED);
- added.setKeyPattern(wp.getResolvedKeyPattern());
- added.setKeyExpression(wp.getKeyExpression());
- added.setWriteLocation(wp.location());
- added.setValueType(displayType(wp));
- added.setMessage("新增缓存写入点,value 类型: " + displayType(wp));
- report.getChanges().add(added);
-
- TypeSchema schema = extractor.extract(wp.getResolvedValueType(), wp.isRootArray());
- KeyStructureChange kc = new KeyStructureChange();
- kc.setKeyPattern(wp.getResolvedKeyPattern());
- kc.setKeyExpression(wp.getKeyExpression());
- kc.setWriteLocation(wp.location());
- kc.setValueType(displayType(wp));
- kc.setKeyUnresolved(false);
- kc.setOldSkeletonJson("");
- kc.setNewSkeletonJson(renderer.render(schema, Collections.emptySet(), SkeletonJsonRenderer.DEFAULT_MAX_LEN));
- kc.getFieldDetails().add(added);
- report.getKeyChanges().add(kc);
- }
- return report;
- }
-
- private static String displayType(WritePoint wp) {
- String fqn = wp.getResolvedValueType();
- if (fqn == null) {
- return "<未解析>";
- }
- return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
+ assertTrue(wps.isEmpty(),
+ "纯 String 原文缓存不应再告警,实际: " + wps);
}
}
diff --git a/src/test/java/com/codechecker/cache/detector/MqWritePointDetectorTest.java b/src/test/java/com/codechecker/cache/detector/MqWritePointDetectorTest.java
index 84cc8c4..9bac143 100644
--- a/src/test/java/com/codechecker/cache/detector/MqWritePointDetectorTest.java
+++ b/src/test/java/com/codechecker/cache/detector/MqWritePointDetectorTest.java
@@ -147,6 +147,23 @@ class MqWritePointDetectorTest {
assertTrue(wp.getResolvedValueType().endsWith("PatrolNotifyVo"));
}
+ @Test
+ void ignoresLongAndStringPayload() {
+ String source = ""
+ + "package demo.mq;\n"
+ + "import org.apache.rocketmq.spring.core.RocketMQTemplate;\n"
+ + "public class BarePayloadProducer {\n"
+ + " private RocketMQTemplate rocketMQTemplate;\n"
+ + " public void send(String topic, Long count, String text) {\n"
+ + " rocketMQTemplate.syncSend(topic, count);\n"
+ + " rocketMQTemplate.asyncSend(topic, text);\n"
+ + " }\n"
+ + "}\n";
+ List wps = new MqWritePointDetector(new SourceIndex(), allRocketPatterns())
+ .detect("BarePayloadProducer.java", source);
+ assertTrue(wps.isEmpty(), "标量 MQ payload 应忽略,实际: " + wps);
+ }
+
private static Set allRocketPatterns() {
Set patterns = new HashSet<>();
patterns.add("MQ01");
diff --git a/src/test/java/com/codechecker/cache/detector/RedisWritePointDetectorTest.java b/src/test/java/com/codechecker/cache/detector/RedisWritePointDetectorTest.java
index 086eb19..74cfc6a 100644
--- a/src/test/java/com/codechecker/cache/detector/RedisWritePointDetectorTest.java
+++ b/src/test/java/com/codechecker/cache/detector/RedisWritePointDetectorTest.java
@@ -98,4 +98,121 @@ class RedisWritePointDetectorTest {
assertEquals(1, wps.size());
assertEquals("W01", wps.get(0).getPattern());
}
+
+ @Test
+ void ignoresStringVariableW04() {
+ String source = ""
+ + "package demo;\n"
+ + "public class TokenCache {\n"
+ + " private RedisTemplate redisTemplate;\n"
+ + " public void save(String key, String tokenVar) {\n"
+ + " redisTemplate.opsForValue().set(key, tokenVar, 60, TimeUnit.SECONDS);\n"
+ + " }\n"
+ + "}\n";
+ assertTrue(detect(source, new SourceIndex()).isEmpty());
+ }
+
+ @Test
+ void ignoresLongVariableAndStringGetter() {
+ String source = ""
+ + "package demo;\n"
+ + "public class CounterCache {\n"
+ + " private RedisTemplate redisTemplate;\n"
+ + " public void save(String key, Long count) {\n"
+ + " redisTemplate.opsForValue().set(key, count, 60, TimeUnit.SECONDS);\n"
+ + " redisTemplate.opsForValue().set(key, getContent(), 60, TimeUnit.SECONDS);\n"
+ + " }\n"
+ + " private String getContent() { return \"x\"; }\n"
+ + "}\n";
+ assertTrue(detect(source, new SourceIndex()).isEmpty());
+ }
+
+ @Test
+ void unwrapsLocalJsonStringToW02() {
+ String vo = ""
+ + "package demo.model;\n"
+ + "public class DemoVo { private String name; }\n";
+ String source = ""
+ + "package demo;\n"
+ + "import demo.model.DemoVo;\n"
+ + "public class DemoService {\n"
+ + " private RedisTemplate redisTemplate;\n"
+ + " public void save(String key, DemoVo vo) {\n"
+ + " String json = JSON.toJSONString(vo);\n"
+ + " redisTemplate.opsForValue().set(key, json, 60, TimeUnit.SECONDS);\n"
+ + " }\n"
+ + "}\n";
+
+ SourceIndex index = new SourceIndex();
+ index.addSource(vo);
+ index.addSource(source);
+
+ List wps = detect(source, index);
+ assertEquals(1, wps.size());
+ assertEquals("W02", wps.get(0).getPattern());
+ assertEquals("demo.model.DemoVo", wps.get(0).getResolvedValueType());
+ }
+
+ @Test
+ void unwrapsLocalJsonStringInsertToW01() {
+ String vo = ""
+ + "package demo.model;\n"
+ + "public class DemoVo { private String name; }\n";
+ String source = ""
+ + "package demo;\n"
+ + "import demo.model.DemoVo;\n"
+ + "public class DemoService {\n"
+ + " private RedisUtil redisUtil;\n"
+ + " public void save(String key, DemoVo vo) {\n"
+ + " String cache = JsonUtil.getObjectToString(vo);\n"
+ + " redisUtil.insert(key, cache, 60);\n"
+ + " }\n"
+ + "}\n";
+
+ SourceIndex index = new SourceIndex();
+ index.addSource(vo);
+ index.addSource(source);
+
+ List wps = detect(source, index);
+ assertEquals(1, wps.size());
+ assertEquals("W01", wps.get(0).getPattern());
+ assertEquals("demo.model.DemoVo", wps.get(0).getResolvedValueType());
+ }
+
+ @Test
+ void ignoresHashPutStringAndInsertStringVar() {
+ String source = ""
+ + "package demo;\n"
+ + "public class DemoService {\n"
+ + " private RedisTemplate redisTemplate;\n"
+ + " private RedisUtil redisUtil;\n"
+ + " public void save(String key, String field, String nameStr, String token) {\n"
+ + " redisTemplate.opsForHash().put(key, field, nameStr);\n"
+ + " redisUtil.insert(key, token, 60);\n"
+ + " }\n"
+ + "}\n";
+ assertTrue(detect(source, new SourceIndex()).isEmpty());
+ }
+
+ @Test
+ void ignoresW04WhenTypeNotInSourceIndex() {
+ String source = ""
+ + "package demo;\n"
+ + "import external.ExternalDto;\n"
+ + "public class DemoService {\n"
+ + " private RedisTemplate redisTemplate;\n"
+ + " public void save(String key, ExternalDto dto) {\n"
+ + " redisTemplate.opsForValue().set(key, dto, 60, TimeUnit.SECONDS);\n"
+ + " }\n"
+ + "}\n";
+ assertTrue(detect(source, new SourceIndex()).isEmpty());
+ }
+
+ private static List detect(String source, SourceIndex index) {
+ return new RedisWritePointDetector(index, allPatterns()).detect("DemoService.java", source);
+ }
+
+ private static Set allPatterns() {
+ return new HashSet<>(Arrays.asList("W01", "W02", "W03", "W04", "W05"));
+ }
}