feat: V1.1 - 问题一修复

This commit is contained in:
2026-08-03 11:03:22 +08:00
parent 182e49c4e1
commit 69d581f034
11 changed files with 671 additions and 145 deletions

View File

@@ -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
* <pre>
* - Key --> recording:todo:replace::
* 类型: &lt;未解析&gt;
* value 新增为: “{}”
* - Key --> recording:todo:replace:title::
* 类型: &lt;未解析&gt;
* value 新增为: “{}”
* </pre>
* 根因:{@code stringRedisTemplate.opsForValue().set(key, String变量)} 被 W04 命中,
* String 无法展开 Schema → 空骨架 + WRITE_POINT_ADDED。
* 业务仓 RecordingTodoDomainServiceImpl#replaceTextStringRedisTemplate 缓存原文 String。
* V1.1 收敛后不应再检出写入点(修复前会误报 W04 + 类型未解析 + value 新增为 "{}")。
*/
class RecordingTodoReplaceScenarioTest {
private static final Set<String> PATTERNS = new HashSet<>(Arrays.asList("W01", "W02", "W03", "W04", "W05"));
private static final Set<String> 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<WritePoint> wps = new RedisWritePointDetector(index, PATTERNS).detect("RecordingTodoDomainServiceImpl.java", service);
wps = wps.stream().sorted(Comparator.comparing(WritePoint::getLineNumber)).collect(Collectors.toList());
List<WritePoint> 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 不在本仓 SourceIndexFQN 应为 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);
}
}

View File

@@ -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<WritePoint> wps = new MqWritePointDetector(new SourceIndex(), allRocketPatterns())
.detect("BarePayloadProducer.java", source);
assertTrue(wps.isEmpty(), "标量 MQ payload 应忽略,实际: " + wps);
}
private static Set<String> allRocketPatterns() {
Set<String> patterns = new HashSet<>();
patterns.add("MQ01");

View File

@@ -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<String, String> 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<String, Object> 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<String, String> 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<WritePoint> 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<WritePoint> 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<String, Object> 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<String, Object> 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<WritePoint> detect(String source, SourceIndex index) {
return new RedisWritePointDetector(index, allPatterns()).detect("DemoService.java", source);
}
private static Set<String> allPatterns() {
return new HashSet<>(Arrays.asList("W01", "W02", "W03", "W04", "W05"));
}
}