feat: V1.1 - 问题一修复
This commit is contained in:
@@ -167,13 +167,17 @@ public class SchemaCheckAnalyzer {
|
||||
skeletonRenderer.render(newSchema, protectedPaths(changes),
|
||||
SkeletonJsonRenderer.DEFAULT_MAX_LEN));
|
||||
} else if (fileChanged) {
|
||||
// R-C:空 Schema 的新增写入点无结构可告警,收敛丢弃
|
||||
TypeSchema newSchema = extractorNew.extract(nw.getResolvedValueType(), nw.isRootArray());
|
||||
if (newSchema == null || newSchema.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_ADDED);
|
||||
fillFromWritePoint(c, nw);
|
||||
c.setMessage(nw.isMq()
|
||||
? "新增 MQ 投递点,value 类型: " + displayType(nw)
|
||||
: "新增缓存写入点,value 类型: " + displayType(nw));
|
||||
allChanges.add(c);
|
||||
TypeSchema newSchema = extractorNew.extract(nw.getResolvedValueType(), nw.isRootArray());
|
||||
mergeKeyChange(keyChanges, nw,
|
||||
Collections.singletonList(c),
|
||||
"",
|
||||
@@ -187,13 +191,16 @@ public class SchemaCheckAnalyzer {
|
||||
for (WritePoint ow : oldWps) {
|
||||
if (!newSigs.contains(ow.signature())
|
||||
&& !isWritePointIgnored(ow) && !isWriterIgnored(ow)) {
|
||||
TypeSchema oldSchema = extractorOld.extract(ow.getResolvedValueType(), ow.isRootArray());
|
||||
if (oldSchema == null || oldSchema.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_REMOVED);
|
||||
fillFromWritePoint(c, ow);
|
||||
c.setMessage(ow.isMq()
|
||||
? "删除 MQ 投递点,原 value 类型: " + displayType(ow)
|
||||
: "删除缓存写入点,原 value 类型: " + displayType(ow));
|
||||
allChanges.add(c);
|
||||
TypeSchema oldSchema = extractorOld.extract(ow.getResolvedValueType(), ow.isRootArray());
|
||||
mergeKeyChange(keyChanges, ow,
|
||||
Collections.singletonList(c),
|
||||
skeletonRenderer.render(oldSchema, protectedPaths(
|
||||
|
||||
54
src/main/java/com/codechecker/cache/detector/BareValueTypes.java
vendored
Normal file
54
src/main/java/com/codechecker/cache/detector/BareValueTypes.java
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.codechecker.cache.detector;
|
||||
|
||||
import com.github.javaparser.ast.type.ArrayType;
|
||||
import com.github.javaparser.ast.type.Type;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 无业务 JSON 结构的标量 / 裸字节类型(Redis / MQ 共用)。这些类型非序列化,直接跳过。
|
||||
*/
|
||||
final class BareValueTypes {
|
||||
|
||||
private static final Set<String> BARE = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
||||
"String", "CharSequence", "char", "Character", "byte", "Byte", "int", "long", "short", "double", "float",
|
||||
"Integer", "Long", "Short", "Double", "Float", "Number", "BigInteger", "BigDecimal", "AtomicInteger", "AtomicLong",
|
||||
"boolean", "Boolean", "UUID")));
|
||||
|
||||
private BareValueTypes() {
|
||||
}
|
||||
|
||||
static boolean isBareSimpleName(String simple) {
|
||||
return simple != null && BARE.contains(simple);
|
||||
}
|
||||
|
||||
static boolean isBareType(Type type) {
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
if (type.isPrimitiveType()) {
|
||||
return true;
|
||||
}
|
||||
if (type instanceof ArrayType) {
|
||||
Type component = ((ArrayType) type).getComponentType();
|
||||
String simple = component.isClassOrInterfaceType() ? component.asClassOrInterfaceType().getNameAsString() : component.asString();
|
||||
return "byte".equals(simple) || "Byte".equals(simple);
|
||||
}
|
||||
if (type.isClassOrInterfaceType()) {
|
||||
return isBareSimpleName(type.asClassOrInterfaceType().getNameAsString());
|
||||
}
|
||||
String asString = type.asString();
|
||||
return "byte[]".equals(asString) || isBareSimpleName(asString);
|
||||
}
|
||||
|
||||
static boolean isBareFqn(String fqn) {
|
||||
if (fqn == null || fqn.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int dot = fqn.lastIndexOf('.');
|
||||
return isBareSimpleName(dot >= 0 ? fqn.substring(dot + 1) : fqn);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import java.util.Set;
|
||||
* <li>M2:MQ03 convertAndSend、MQ04 MessageBuilder/`Message<T>`、MQ05 JSON 字符串、
|
||||
* MQ-K03 ProducerRecord、MQ-K04 JSON 字符串</li>
|
||||
* </ul>
|
||||
* V1.1:标量 payload 忽略;直传/Message 解包后须为本仓库可展开业务类型。
|
||||
*/
|
||||
public class MqWritePointDetector {
|
||||
|
||||
@@ -53,8 +54,8 @@ public class MqWritePointDetector {
|
||||
"List", "ArrayList", "LinkedList", "Set", "HashSet", "Collection"));
|
||||
private static final Set<String> ENVELOPE_TYPES = new HashSet<>(Arrays.asList(
|
||||
"Message", "MessageExt", "ProducerRecord"));
|
||||
private static final Set<String> IGNORE_BARE_TYPES = new HashSet<>(Arrays.asList(
|
||||
"String", "byte", "Byte"));
|
||||
/** JSON 字符串模式:仅标量收敛,不强制本仓类型(与 Redis W01~W03 对齐)。 */
|
||||
private static final Set<String> JSON_STRING_PATTERNS = new HashSet<>(Arrays.asList("MQ05", "MQ-K04"));
|
||||
|
||||
private final SourceIndex index;
|
||||
private final Set<String> enabledPatterns;
|
||||
@@ -187,6 +188,9 @@ public class MqWritePointDetector {
|
||||
if (!enabledPatterns.contains(pattern)) {
|
||||
return null;
|
||||
}
|
||||
if (isBareScalarTypeExpr(resolved.typeExpr, mce)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
WritePoint wp = new WritePoint();
|
||||
wp.setFilePath(filePath);
|
||||
@@ -210,9 +214,51 @@ public class MqWritePointDetector {
|
||||
} else {
|
||||
wp.setConfidence(0.4);
|
||||
}
|
||||
// 直传对象 / Message 解包:须本仓库可展开;JSON 字符串模式仅拦标量
|
||||
if (!JSON_STRING_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 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<ClassOrInterfaceDeclaration> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 payload:Message/MessageBuilder(MQ04)、JSON 字符串(MQ05/MQ-K04)、直传对象。
|
||||
* 无法解析的 Message/纯 String 返回 null(忽略)。
|
||||
@@ -371,18 +417,7 @@ public class MqWritePointDetector {
|
||||
if (typeExpr instanceof StringLiteralExpr) {
|
||||
return true;
|
||||
}
|
||||
if (typeExpr instanceof NameExpr) {
|
||||
Type declared = findVariableType(((NameExpr) typeExpr).getNameAsString(), contextCall);
|
||||
if (declared != null) {
|
||||
String simple = declared.isClassOrInterfaceType()
|
||||
? declared.asClassOrInterfaceType().getNameAsString()
|
||||
: declared.asString();
|
||||
if (IGNORE_BARE_TYPES.contains(simple) || "byte[]".equals(declared.asString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return isBareScalarTypeExpr(typeExpr, contextCall);
|
||||
}
|
||||
|
||||
private boolean isTrivialValue(Expression expr) {
|
||||
@@ -451,8 +486,7 @@ public class MqWritePointDetector {
|
||||
}
|
||||
if (expr instanceof MethodCallExpr) {
|
||||
MethodCallExpr call = (MethodCallExpr) expr;
|
||||
Optional<ClassOrInterfaceDeclaration> clazz = contextCall
|
||||
.findAncestor(ClassOrInterfaceDeclaration.class);
|
||||
Optional<ClassOrInterfaceDeclaration> clazz = contextCall.findAncestor(ClassOrInterfaceDeclaration.class);
|
||||
if (clazz.isPresent()) {
|
||||
for (MethodDeclaration md : clazz.get().getMethods()) {
|
||||
if (md.getNameAsString().equals(call.getNameAsString())) {
|
||||
|
||||
@@ -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)。
|
||||
* <p>
|
||||
* V1.1 收敛:忽略无结构标量;局部 JSON 字符串 unwrap;W04/W05 仅保留本仓库可展开业务类型。
|
||||
*/
|
||||
public class RedisWritePointDetector {
|
||||
|
||||
@@ -48,6 +49,7 @@ public class RedisWritePointDetector {
|
||||
"randomUUID", "toString", "valueOf"));
|
||||
private static final Set<String> COLLECTION_SIMPLE = new HashSet<>(Arrays.asList(
|
||||
"List", "ArrayList", "LinkedList", "Set", "HashSet", "Collection"));
|
||||
private static final Set<String> DIRECT_OBJECT_PATTERNS = new HashSet<>(Arrays.asList("W04", "W05"));
|
||||
|
||||
private final SourceIndex index;
|
||||
private final Set<String> 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<ClassOrInterfaceDeclaration> 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<CallableDeclaration> 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);
|
||||
}
|
||||
|
||||
@@ -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:*:*
|
||||
* 类型: <未解析>
|
||||
* value 新增为: “{}”
|
||||
* - Key --> recording:todo:replace:title:*:*
|
||||
* 类型: <未解析>
|
||||
* value 新增为: “{}”
|
||||
* </pre>
|
||||
* 根因:{@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<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 不在本仓 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user