feat: 二阶段
This commit is contained in:
@@ -3,6 +3,7 @@ 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.MqReadHintDetector;
|
||||
import com.codechecker.cache.detector.MqWritePointDetector;
|
||||
import com.codechecker.cache.detector.RedisWritePointDetector;
|
||||
import com.codechecker.cache.detector.WritePoint;
|
||||
@@ -105,6 +106,9 @@ public class SchemaCheckAnalyzer {
|
||||
SchemaDiffer differ = new SchemaDiffer();
|
||||
SkeletonJsonRenderer skeletonRenderer = new SkeletonJsonRenderer();
|
||||
|
||||
List<CacheReadHint> mqHintsNew = collectMqReadHints(newContents, newIndex);
|
||||
List<CacheReadHint> mqHintsOld = collectMqReadHints(oldContents, oldIndex);
|
||||
|
||||
List<SchemaChange> allChanges = new ArrayList<>();
|
||||
Map<String, KeyStructureChange> keyChanges = new LinkedHashMap<>();
|
||||
|
||||
@@ -129,6 +133,8 @@ public class SchemaCheckAnalyzer {
|
||||
if (oldContent != null) {
|
||||
applyReadHints(oldWps, path, oldContent, oldIndex);
|
||||
}
|
||||
applyMqReadHints(newWps, mqHintsNew);
|
||||
applyMqReadHints(oldWps, mqHintsOld);
|
||||
newWps.forEach(this::applyManualMappings);
|
||||
oldWps.forEach(this::applyManualMappings);
|
||||
|
||||
@@ -474,12 +480,38 @@ public class SchemaCheckAnalyzer {
|
||||
}
|
||||
for (WritePoint wp : writePoints) {
|
||||
if (wp.isMq()) {
|
||||
continue; // Redis W06 不补强 MQ 投递点(MQ-R 为 Phase M2)
|
||||
continue; // MQ 投递点由 MQ-R 补强
|
||||
}
|
||||
enrichWritePointFromHints(wp, hints);
|
||||
}
|
||||
}
|
||||
|
||||
private List<CacheReadHint> collectMqReadHints(Map<String, String> contents, SourceIndex index) {
|
||||
if (!config.getDetection().isMqReadHintsEnabled() || contents == null || contents.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
MqReadHintDetector detector = new MqReadHintDetector(index);
|
||||
List<CacheReadHint> all = new ArrayList<>();
|
||||
for (Map.Entry<String, String> e : contents.entrySet()) {
|
||||
all.addAll(detector.detect(e.getKey(), e.getValue()));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/** MQ-R:按 destination 匹配,补强低置信度 / 缺类型的 MQ 投递点。 */
|
||||
private void applyMqReadHints(List<WritePoint> writePoints, List<CacheReadHint> hints) {
|
||||
if (!config.getDetection().isMqReadHintsEnabled()
|
||||
|| writePoints == null || writePoints.isEmpty()
|
||||
|| hints == null || hints.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (WritePoint wp : writePoints) {
|
||||
if (wp.isMq()) {
|
||||
enrichWritePointFromHints(wp, hints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enrichWritePointFromHints(WritePoint wp, List<CacheReadHint> hints) {
|
||||
boolean needType = wp.getResolvedValueType() == null || wp.getResolvedValueType().isEmpty()
|
||||
|| wp.getConfidence() < config.getDetection().getMinConfidence();
|
||||
|
||||
@@ -118,14 +118,14 @@ public class CheckerConfig {
|
||||
|
||||
public static class Detection {
|
||||
private List<String> patterns = new ArrayList<>();
|
||||
/** MQ 投递检测模式:MQ01/MQ02/MQ-K01/MQ-K02… */
|
||||
/** MQ 投递检测模式:MQ01~MQ05、MQ-K01~MQ-K04 */
|
||||
private List<String> mqPatterns = new ArrayList<>();
|
||||
private double minConfidence = 0.6;
|
||||
private int maxFieldDepth = 8;
|
||||
/** W06:是否启用读侧反序列化类型辅助补强 */
|
||||
private boolean readHintsEnabled = true;
|
||||
/** MQ-R:读侧 Listener / parse 补强(Phase M2;M1 默认 false) */
|
||||
private boolean mqReadHintsEnabled = false;
|
||||
/** MQ-R:读侧 Listener / parse 补强 */
|
||||
private boolean mqReadHintsEnabled = true;
|
||||
|
||||
public List<String> getPatterns() {
|
||||
return patterns;
|
||||
|
||||
@@ -103,7 +103,7 @@ public final class ConfigLoader {
|
||||
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));
|
||||
d.setMqReadHintsEnabled(bool(detection, "mq_read_hints_enabled", false));
|
||||
d.setMqReadHintsEnabled(bool(detection, "mq_read_hints_enabled", true));
|
||||
|
||||
Map<String, Object> severityOverrides = asMap(map.get("severity_overrides"));
|
||||
Map<String, String> so = new LinkedHashMap<>();
|
||||
|
||||
365
src/main/java/com/codechecker/cache/detector/MqReadHintDetector.java
vendored
Normal file
365
src/main/java/com/codechecker/cache/detector/MqReadHintDetector.java
vendored
Normal file
@@ -0,0 +1,365 @@
|
||||
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.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.MethodDeclaration;
|
||||
import com.github.javaparser.ast.body.Parameter;
|
||||
import com.github.javaparser.ast.expr.AnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
|
||||
import com.github.javaparser.ast.expr.ClassExpr;
|
||||
import com.github.javaparser.ast.expr.Expression;
|
||||
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.MemberValuePair;
|
||||
import com.github.javaparser.ast.expr.MethodCallExpr;
|
||||
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
|
||||
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.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MQ-R:消费侧类型提示,补强同 destination 生产点的 value 类型(不单独告警)。
|
||||
* <ul>
|
||||
* <li>MQ-R01:{@code RocketMQListener<T>} + {@code @RocketMQMessageListener}</li>
|
||||
* <li>MQ-R02:{@code @KafkaListener} 非 String 参数类型</li>
|
||||
* <li>MQ-R03:Listener 内 {@code parseObject/parseArray(..., Xxx.class)}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class MqReadHintDetector {
|
||||
|
||||
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> SKIP_PARAM_TYPES = new HashSet<>(Arrays.asList(
|
||||
"String", "byte", "Byte", "ConsumerRecord", "MessageExt", "Message"));
|
||||
|
||||
private final SourceIndex index;
|
||||
private final RedisKeyResolver keyResolver;
|
||||
|
||||
public MqReadHintDetector(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 (ClassOrInterfaceDeclaration clazz : cu.findAll(ClassOrInterfaceDeclaration.class)) {
|
||||
result.addAll(detectRocketListener(clazz, filePath));
|
||||
for (MethodDeclaration md : clazz.getMethods()) {
|
||||
result.addAll(detectKafkaListener(md, clazz, filePath));
|
||||
result.addAll(detectParseInListener(md, clazz, filePath));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** MQ-R01 */
|
||||
private List<CacheReadHint> detectRocketListener(ClassOrInterfaceDeclaration clazz, String filePath) {
|
||||
List<CacheReadHint> result = new ArrayList<>();
|
||||
Optional<ClassOrInterfaceType> listenerType = findRocketListenerType(clazz);
|
||||
if (!listenerType.isPresent()) {
|
||||
return result;
|
||||
}
|
||||
AnnotationExpr ann = findAnnotation(clazz.getAnnotations(), "RocketMQMessageListener");
|
||||
if (ann == null) {
|
||||
return result;
|
||||
}
|
||||
String dest = resolveRocketDestination(ann, clazz);
|
||||
InferredType payload = resolveTypeArg(listenerType.get(), index.get(
|
||||
clazz.getFullyQualifiedName().orElse(clazz.getNameAsString())));
|
||||
if (payload.fqn == null) {
|
||||
return result;
|
||||
}
|
||||
CacheReadHint hint = baseHint(filePath, clazz, "<listener>", dest, payload);
|
||||
hint.setConfidence(0.85);
|
||||
result.add(hint);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** MQ-R02 */
|
||||
private List<CacheReadHint> detectKafkaListener(MethodDeclaration md,
|
||||
ClassOrInterfaceDeclaration clazz,
|
||||
String filePath) {
|
||||
List<CacheReadHint> result = new ArrayList<>();
|
||||
AnnotationExpr ann = findAnnotation(md.getAnnotations(), "KafkaListener");
|
||||
if (ann == null) {
|
||||
return result;
|
||||
}
|
||||
List<String> topics = resolveKafkaTopics(ann, clazz);
|
||||
if (topics.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
SourceIndex.IndexedType context = index.get(
|
||||
clazz.getFullyQualifiedName().orElse(clazz.getNameAsString()));
|
||||
InferredType payload = null;
|
||||
for (Parameter p : md.getParameters()) {
|
||||
InferredType t = resolveParamPayload(p.getType(), context);
|
||||
if (t != null && t.fqn != null) {
|
||||
payload = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (payload == null) {
|
||||
return result;
|
||||
}
|
||||
for (String topic : topics) {
|
||||
CacheReadHint hint = baseHint(filePath, clazz, md.getNameAsString(), topic, payload);
|
||||
hint.setConfidence(0.85);
|
||||
result.add(hint);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** MQ-R03:在已标注 KafkaListener 的方法,或 RocketMQListener 类内 parse */
|
||||
private List<CacheReadHint> detectParseInListener(MethodDeclaration md,
|
||||
ClassOrInterfaceDeclaration clazz,
|
||||
String filePath) {
|
||||
List<CacheReadHint> result = new ArrayList<>();
|
||||
boolean kafka = findAnnotation(md.getAnnotations(), "KafkaListener") != null;
|
||||
boolean rocket = findRocketListenerType(clazz).isPresent();
|
||||
if (!kafka && !rocket) {
|
||||
return result;
|
||||
}
|
||||
List<String> destinations = new ArrayList<>();
|
||||
if (kafka) {
|
||||
destinations.addAll(resolveKafkaTopics(
|
||||
findAnnotation(md.getAnnotations(), "KafkaListener"), clazz));
|
||||
}
|
||||
if (rocket) {
|
||||
AnnotationExpr ann = findAnnotation(clazz.getAnnotations(), "RocketMQMessageListener");
|
||||
if (ann != null) {
|
||||
String dest = resolveRocketDestination(ann, clazz);
|
||||
if (dest != null && !dest.isEmpty()) {
|
||||
destinations.add(dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (destinations.isEmpty()) {
|
||||
destinations.add(null);
|
||||
}
|
||||
|
||||
SourceIndex.IndexedType context = index.get(
|
||||
clazz.getFullyQualifiedName().orElse(clazz.getNameAsString()));
|
||||
for (MethodCallExpr mce : md.findAll(MethodCallExpr.class)) {
|
||||
String name = mce.getNameAsString();
|
||||
boolean array = ARRAY_PARSE.contains(name);
|
||||
boolean object = OBJECT_PARSE.contains(name);
|
||||
if (!array && !object || mce.getArguments().size() < 2) {
|
||||
continue;
|
||||
}
|
||||
Expression classArg = mce.getArgument(1);
|
||||
if (!(classArg instanceof ClassExpr)) {
|
||||
continue;
|
||||
}
|
||||
Type type = ((ClassExpr) classArg).getType();
|
||||
if (!(type instanceof ClassOrInterfaceType)) {
|
||||
continue;
|
||||
}
|
||||
String fqn = index.resolveFqn(((ClassOrInterfaceType) type).getNameWithScope(), context);
|
||||
if (fqn == null) {
|
||||
fqn = index.resolveFqn(((ClassOrInterfaceType) type).getNameAsString(), context);
|
||||
}
|
||||
if (fqn == null) {
|
||||
continue;
|
||||
}
|
||||
InferredType payload = new InferredType(fqn, array);
|
||||
for (String dest : destinations) {
|
||||
CacheReadHint hint = baseHint(filePath, clazz, md.getNameAsString(), dest, payload);
|
||||
hint.setLineNumber(mce.getBegin().map(p -> p.line).orElse(0));
|
||||
hint.setConfidence(dest == null ? 0.7 : 0.8);
|
||||
result.add(hint);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private CacheReadHint baseHint(String filePath, ClassOrInterfaceDeclaration clazz,
|
||||
String method, String dest, InferredType payload) {
|
||||
CacheReadHint hint = new CacheReadHint();
|
||||
hint.setFilePath(filePath);
|
||||
hint.setLineNumber(clazz.getBegin().map(p -> p.line).orElse(0));
|
||||
hint.setEnclosingClass(clazz.getFullyQualifiedName().orElse(clazz.getNameAsString()));
|
||||
hint.setEnclosingMethod(method);
|
||||
hint.setResolvedKeyPattern(dest);
|
||||
hint.setResolvedValueType(payload.fqn);
|
||||
hint.setRootArray(payload.isArray);
|
||||
return hint;
|
||||
}
|
||||
|
||||
private Optional<ClassOrInterfaceType> findRocketListenerType(ClassOrInterfaceDeclaration clazz) {
|
||||
for (ClassOrInterfaceType t : clazz.getImplementedTypes()) {
|
||||
String n = t.getNameAsString();
|
||||
if ("RocketMQListener".equals(n) || "RocketMQReplyListener".equals(n)) {
|
||||
return Optional.of(t);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private InferredType resolveParamPayload(Type type, SourceIndex.IndexedType context) {
|
||||
if (!(type instanceof ClassOrInterfaceType)) {
|
||||
return null;
|
||||
}
|
||||
ClassOrInterfaceType cit = (ClassOrInterfaceType) type;
|
||||
String simple = cit.getNameAsString();
|
||||
if (SKIP_PARAM_TYPES.contains(simple) && !"ConsumerRecord".equals(simple)) {
|
||||
return null;
|
||||
}
|
||||
if ("ConsumerRecord".equals(simple) || "List".equals(simple) || "ArrayList".equals(simple)) {
|
||||
Optional<Type> last = cit.getTypeArguments()
|
||||
.filter(a -> !a.isEmpty())
|
||||
.map(a -> a.get(a.size() - 1));
|
||||
if (!last.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
InferredType inner = resolveTypeArgFromType(last.get(), context);
|
||||
if (inner != null && ("List".equals(simple) || "ArrayList".equals(simple))) {
|
||||
return new InferredType(inner.fqn, true);
|
||||
}
|
||||
return inner;
|
||||
}
|
||||
return resolveTypeArgFromType(type, context);
|
||||
}
|
||||
|
||||
private InferredType resolveTypeArg(ClassOrInterfaceType listenerType, SourceIndex.IndexedType context) {
|
||||
Optional<Type> arg = listenerType.getTypeArguments().filter(a -> !a.isEmpty()).map(a -> a.get(0));
|
||||
if (!arg.isPresent()) {
|
||||
return new InferredType(null, false);
|
||||
}
|
||||
return resolveTypeArgFromType(arg.get(), context);
|
||||
}
|
||||
|
||||
private InferredType resolveTypeArgFromType(Type type, SourceIndex.IndexedType context) {
|
||||
if (!(type instanceof ClassOrInterfaceType)) {
|
||||
return new InferredType(null, false);
|
||||
}
|
||||
ClassOrInterfaceType cit = (ClassOrInterfaceType) type;
|
||||
if ("List".equals(cit.getNameAsString()) || "ArrayList".equals(cit.getNameAsString())) {
|
||||
Optional<Type> el = cit.getTypeArguments().filter(a -> !a.isEmpty()).map(a -> a.get(0));
|
||||
if (el.isPresent() && el.get() instanceof ClassOrInterfaceType) {
|
||||
return new InferredType(resolveFqn((ClassOrInterfaceType) el.get(), context), true);
|
||||
}
|
||||
return new InferredType(null, true);
|
||||
}
|
||||
String simple = cit.getNameAsString();
|
||||
if (SKIP_PARAM_TYPES.contains(simple)) {
|
||||
return new InferredType(null, false);
|
||||
}
|
||||
return new InferredType(resolveFqn(cit, context), false);
|
||||
}
|
||||
|
||||
private String resolveFqn(ClassOrInterfaceType cit, SourceIndex.IndexedType context) {
|
||||
String fqn = index.resolveFqn(cit.getNameWithScope(), context);
|
||||
if (fqn == null) {
|
||||
fqn = index.resolveFqn(cit.getNameAsString(), context);
|
||||
}
|
||||
return fqn;
|
||||
}
|
||||
|
||||
private String resolveRocketDestination(AnnotationExpr ann, ClassOrInterfaceDeclaration clazz) {
|
||||
SourceIndex.IndexedType context = index.get(
|
||||
clazz.getFullyQualifiedName().orElse(clazz.getNameAsString()));
|
||||
Expression topicExpr = annotationValue(ann, "topic");
|
||||
if (topicExpr == null) {
|
||||
return null;
|
||||
}
|
||||
String topicPattern = keyResolver.resolve(topicExpr, clazz, context);
|
||||
Expression tagExpr = annotationValue(ann, "selectorExpression");
|
||||
if (tagExpr != null) {
|
||||
String tagPattern = keyResolver.resolve(tagExpr, clazz, context);
|
||||
if (tagPattern != null && !tagPattern.isEmpty()
|
||||
&& !"*".equals(tagPattern) && !tagPattern.contains("||")) {
|
||||
return topicPattern + ":" + tagPattern;
|
||||
}
|
||||
}
|
||||
return topicPattern;
|
||||
}
|
||||
|
||||
private List<String> resolveKafkaTopics(AnnotationExpr ann, ClassOrInterfaceDeclaration clazz) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (ann == null) {
|
||||
return result;
|
||||
}
|
||||
SourceIndex.IndexedType context = index.get(
|
||||
clazz.getFullyQualifiedName().orElse(clazz.getNameAsString()));
|
||||
Expression topicsExpr = annotationValue(ann, "topics");
|
||||
if (topicsExpr == null && ann instanceof SingleMemberAnnotationExpr) {
|
||||
topicsExpr = ((SingleMemberAnnotationExpr) ann).getMemberValue();
|
||||
}
|
||||
if (topicsExpr == null) {
|
||||
return result;
|
||||
}
|
||||
List<Expression> items = new ArrayList<>();
|
||||
if (topicsExpr instanceof ArrayInitializerExpr) {
|
||||
items.addAll(((ArrayInitializerExpr) topicsExpr).getValues());
|
||||
} else {
|
||||
items.add(topicsExpr);
|
||||
}
|
||||
for (Expression item : items) {
|
||||
String resolved = keyResolver.resolve(item, clazz, context);
|
||||
if (resolved != null && !resolved.isEmpty()) {
|
||||
result.add(resolved);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Expression annotationValue(AnnotationExpr ann, String name) {
|
||||
if (ann instanceof NormalAnnotationExpr) {
|
||||
for (MemberValuePair pair : ((NormalAnnotationExpr) ann).getPairs()) {
|
||||
if (name.equals(pair.getNameAsString())) {
|
||||
return pair.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AnnotationExpr findAnnotation(List<AnnotationExpr> annotations, String simpleName) {
|
||||
for (AnnotationExpr ann : annotations) {
|
||||
String n;
|
||||
if (ann instanceof MarkerAnnotationExpr) {
|
||||
n = ((MarkerAnnotationExpr) ann).getNameAsString();
|
||||
} else if (ann instanceof SingleMemberAnnotationExpr) {
|
||||
n = ((SingleMemberAnnotationExpr) ann).getNameAsString();
|
||||
} else if (ann instanceof NormalAnnotationExpr) {
|
||||
n = ((NormalAnnotationExpr) ann).getNameAsString();
|
||||
} else {
|
||||
n = ann.getNameAsString();
|
||||
}
|
||||
if (n.equals(simpleName) || n.endsWith("." + simpleName)) {
|
||||
return ann;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class InferredType {
|
||||
final String fqn;
|
||||
final boolean isArray;
|
||||
|
||||
InferredType(String fqn, boolean isArray) {
|
||||
this.fqn = fqn;
|
||||
this.isArray = isArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,12 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 检测 RocketMQ / Kafka 生产侧投递点(Phase M1:MQ01/MQ02、MQ-K01/MQ-K02)。
|
||||
* 检测 RocketMQ / Kafka 生产侧投递点(Phase M1 + M2)。
|
||||
* <ul>
|
||||
* <li>M1:MQ01/MQ02、MQ-K01/MQ-K02</li>
|
||||
* <li>M2:MQ03 convertAndSend、MQ04 MessageBuilder/`Message<T>`、MQ05 JSON 字符串、
|
||||
* MQ-K03 ProducerRecord、MQ-K04 JSON 字符串</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class MqWritePointDetector {
|
||||
|
||||
@@ -41,12 +46,15 @@ public class MqWritePointDetector {
|
||||
private static final Set<String> ROCKET_SYNC = new HashSet<>(Arrays.asList("syncSend"));
|
||||
private static final Set<String> ROCKET_ASYNC = new HashSet<>(Arrays.asList(
|
||||
"asyncSend", "syncSendOrderly", "sendOneWay", "asyncSendOrderly"));
|
||||
private static final Set<String> ROCKET_CONVERT = new HashSet<>(Arrays.asList("convertAndSend"));
|
||||
private static final Set<String> TRIVIAL_VALUE_CALLS = new HashSet<>(Arrays.asList(
|
||||
"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> IGNORE_PAYLOAD_TYPES = new HashSet<>(Arrays.asList(
|
||||
"String", "byte", "Byte", "MessageExt", "Message", "ProducerRecord"));
|
||||
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"));
|
||||
|
||||
private final SourceIndex index;
|
||||
private final Set<String> enabledPatterns;
|
||||
@@ -83,19 +91,19 @@ public class MqWritePointDetector {
|
||||
|
||||
private WritePoint tryDetectRocketMq(MethodCallExpr mce, String filePath) {
|
||||
String method = mce.getNameAsString();
|
||||
String pattern = null;
|
||||
String basePattern = null;
|
||||
if (ROCKET_SYNC.contains(method) && enabledPatterns.contains("MQ01")) {
|
||||
pattern = "MQ01";
|
||||
basePattern = "MQ01";
|
||||
} else if (ROCKET_ASYNC.contains(method) && enabledPatterns.contains("MQ02")) {
|
||||
pattern = "MQ02";
|
||||
basePattern = "MQ02";
|
||||
} else if (ROCKET_CONVERT.contains(method) && enabledPatterns.contains("MQ03")) {
|
||||
basePattern = "MQ03";
|
||||
}
|
||||
if (pattern == null || !isRocketMqScope(mce) || mce.getArguments().size() < 2) {
|
||||
if (basePattern == null || !isRocketMqScope(mce) || mce.getArguments().size() < 2) {
|
||||
return null;
|
||||
}
|
||||
Expression destArg = mce.getArgument(0);
|
||||
Expression payloadArg = mce.getArgument(1);
|
||||
return buildMqWritePoint(mce, filePath, pattern, WritePoint.CHANNEL_ROCKETMQ,
|
||||
destArg, payloadArg);
|
||||
return buildMqWritePoint(mce, filePath, basePattern, WritePoint.CHANNEL_ROCKETMQ,
|
||||
mce.getArgument(0), mce.getArgument(1), "MQ04", "MQ05");
|
||||
}
|
||||
|
||||
private WritePoint tryDetectKafka(MethodCallExpr mce, String filePath) {
|
||||
@@ -103,32 +111,80 @@ public class MqWritePointDetector {
|
||||
return null;
|
||||
}
|
||||
int argc = mce.getArguments().size();
|
||||
String pattern;
|
||||
if (argc == 1 && enabledPatterns.contains("MQ-K03")) {
|
||||
return buildFromProducerRecord(mce, filePath);
|
||||
}
|
||||
String basePattern;
|
||||
Expression topicArg;
|
||||
Expression payloadArg;
|
||||
if (argc == 2 && enabledPatterns.contains("MQ-K01")) {
|
||||
pattern = "MQ-K01";
|
||||
basePattern = "MQ-K01";
|
||||
topicArg = mce.getArgument(0);
|
||||
payloadArg = mce.getArgument(1);
|
||||
} else if (argc >= 3 && enabledPatterns.contains("MQ-K02")) {
|
||||
pattern = "MQ-K02";
|
||||
basePattern = "MQ-K02";
|
||||
topicArg = mce.getArgument(0);
|
||||
payloadArg = mce.getArgument(argc - 1);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return buildMqWritePoint(mce, filePath, pattern, WritePoint.CHANNEL_KAFKA,
|
||||
topicArg, payloadArg);
|
||||
return buildMqWritePoint(mce, filePath, basePattern, WritePoint.CHANNEL_KAFKA,
|
||||
topicArg, payloadArg, null, "MQ-K04");
|
||||
}
|
||||
|
||||
private WritePoint buildMqWritePoint(MethodCallExpr mce, String filePath, String pattern,
|
||||
String channel, Expression destArg, Expression payloadArg) {
|
||||
Expression serialized = unwrapSerializer(payloadArg);
|
||||
Expression typeExpr = serialized != null ? serialized : payloadArg;
|
||||
if (serialized == null && isTrivialValue(payloadArg)) {
|
||||
private WritePoint buildFromProducerRecord(MethodCallExpr mce, String filePath) {
|
||||
Expression recordArg = mce.getArgument(0);
|
||||
ObjectCreationExpr creation = findProducerRecordCreation(recordArg, mce);
|
||||
if (creation == null || creation.getArguments().size() < 2) {
|
||||
return null;
|
||||
}
|
||||
if (isBareStringOrBytesPayload(typeExpr, mce)) {
|
||||
Expression topicArg = creation.getArgument(0);
|
||||
Expression payloadArg = creation.getArgument(creation.getArguments().size() - 1);
|
||||
WritePoint wp = buildMqWritePoint(mce, filePath, "MQ-K03", WritePoint.CHANNEL_KAFKA,
|
||||
topicArg, payloadArg, null, "MQ-K04");
|
||||
if (wp != null) {
|
||||
wp.setValueExpression(recordArg.toString());
|
||||
}
|
||||
return wp;
|
||||
}
|
||||
|
||||
private ObjectCreationExpr findProducerRecordCreation(Expression expr, MethodCallExpr contextCall) {
|
||||
if (expr instanceof ObjectCreationExpr) {
|
||||
ObjectCreationExpr oce = (ObjectCreationExpr) expr;
|
||||
if ("ProducerRecord".equals(oce.getType().getNameAsString())) {
|
||||
return oce;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (expr instanceof NameExpr) {
|
||||
String name = ((NameExpr) expr).getNameAsString();
|
||||
Optional<CallableDeclaration> callable = contextCall.findAncestor(CallableDeclaration.class);
|
||||
if (!callable.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
for (VariableDeclarator var : callable.get().findAll(VariableDeclarator.class)) {
|
||||
if (!var.getNameAsString().equals(name) || !var.getInitializer().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
Expression init = var.getInitializer().get();
|
||||
if (init instanceof ObjectCreationExpr
|
||||
&& "ProducerRecord".equals(((ObjectCreationExpr) init).getType().getNameAsString())) {
|
||||
return (ObjectCreationExpr) init;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private WritePoint buildMqWritePoint(MethodCallExpr mce, String filePath, String basePattern,
|
||||
String channel, Expression destArg, Expression payloadArg,
|
||||
String messagePattern, String jsonPattern) {
|
||||
PayloadResolution resolved = resolvePayload(payloadArg, mce, messagePattern, jsonPattern);
|
||||
if (resolved == null) {
|
||||
return null;
|
||||
}
|
||||
String pattern = resolved.overridePattern != null ? resolved.overridePattern : basePattern;
|
||||
if (!enabledPatterns.contains(pattern)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -146,7 +202,7 @@ public class MqWritePointDetector {
|
||||
.findAncestor(ClassOrInterfaceDeclaration.class).orElse(null);
|
||||
wp.setResolvedKeyPattern(keyResolver.resolve(destArg, enclosingDecl, context));
|
||||
|
||||
InferredType inferred = inferType(typeExpr, mce, context);
|
||||
InferredType inferred = inferType(resolved.typeExpr, mce, context);
|
||||
if (inferred != null) {
|
||||
wp.setResolvedValueType(inferred.fqn);
|
||||
wp.setRootArray(inferred.isArray);
|
||||
@@ -157,6 +213,150 @@ public class MqWritePointDetector {
|
||||
return wp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 payload:Message/MessageBuilder(MQ04)、JSON 字符串(MQ05/MQ-K04)、直传对象。
|
||||
* 无法解析的 Message/纯 String 返回 null(忽略)。
|
||||
*/
|
||||
private PayloadResolution resolvePayload(Expression payloadArg, MethodCallExpr mce,
|
||||
String messagePattern, String jsonPattern) {
|
||||
if (payloadArg == null) {
|
||||
return null;
|
||||
}
|
||||
// MQ04:Message<T> 泛型 / MessageBuilder.withPayload
|
||||
if (messagePattern != null && enabledPatterns.contains(messagePattern)) {
|
||||
Expression fromMessage = unwrapMessagePayload(payloadArg, mce);
|
||||
if (fromMessage != null) {
|
||||
return new PayloadResolution(fromMessage, messagePattern);
|
||||
}
|
||||
if (isEnvelopeBare(payloadArg, mce)) {
|
||||
return null;
|
||||
}
|
||||
} else if (isEnvelopeBare(payloadArg, mce)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 内联 JSON 序列化
|
||||
Expression serialized = unwrapSerializer(payloadArg);
|
||||
if (serialized != null) {
|
||||
if (jsonPattern != null && enabledPatterns.contains(jsonPattern)) {
|
||||
return new PayloadResolution(serialized, jsonPattern);
|
||||
}
|
||||
return new PayloadResolution(serialized, null);
|
||||
}
|
||||
|
||||
// 局部 String = toJSONString(...)
|
||||
if (jsonPattern != null && enabledPatterns.contains(jsonPattern)) {
|
||||
Expression fromLocalJson = unwrapJsonStringVar(payloadArg, mce);
|
||||
if (fromLocalJson != null) {
|
||||
return new PayloadResolution(fromLocalJson, jsonPattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (isTrivialValue(payloadArg) || isBareStringOrBytesPayload(payloadArg, mce)) {
|
||||
return null;
|
||||
}
|
||||
return new PayloadResolution(payloadArg, null);
|
||||
}
|
||||
|
||||
private Expression unwrapMessagePayload(Expression payloadArg, MethodCallExpr mce) {
|
||||
Expression fromBuilder = unwrapMessageBuilderPayload(payloadArg, mce);
|
||||
if (fromBuilder != null) {
|
||||
return fromBuilder;
|
||||
}
|
||||
// Message<DutyImNotice> → DutyImNotice(无 withPayload 也可)
|
||||
if (payloadArg instanceof NameExpr) {
|
||||
Type declared = findVariableType(((NameExpr) payloadArg).getNameAsString(), mce);
|
||||
if (declared instanceof ClassOrInterfaceType) {
|
||||
ClassOrInterfaceType cit = (ClassOrInterfaceType) declared;
|
||||
if (ENVELOPE_TYPES.contains(cit.getNameAsString())) {
|
||||
Optional<Type> payloadType = cit.getTypeArguments()
|
||||
.filter(a -> !a.isEmpty())
|
||||
.map(a -> a.get(a.size() - 1));
|
||||
if (payloadType.isPresent() && payloadType.get() instanceof ClassOrInterfaceType) {
|
||||
// 用伪 ObjectCreation 表达类型不便;改为在 infer 前用 NameExpr 不够
|
||||
// 返回一个标记:借助 CastExpr 包装类型信息
|
||||
return new CastExpr(payloadType.get(), payloadArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression unwrapMessageBuilderPayload(Expression payloadArg, MethodCallExpr mce) {
|
||||
Expression chain = payloadArg;
|
||||
if (payloadArg instanceof NameExpr) {
|
||||
String name = ((NameExpr) payloadArg).getNameAsString();
|
||||
Optional<CallableDeclaration> callable = mce.findAncestor(CallableDeclaration.class);
|
||||
if (callable.isPresent()) {
|
||||
for (VariableDeclarator var : callable.get().findAll(VariableDeclarator.class)) {
|
||||
if (var.getNameAsString().equals(name) && var.getInitializer().isPresent()) {
|
||||
chain = var.getInitializer().get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return findWithPayloadArg(chain);
|
||||
}
|
||||
|
||||
private Expression findWithPayloadArg(Expression expr) {
|
||||
Expression current = expr;
|
||||
int guard = 0;
|
||||
while (current instanceof MethodCallExpr && guard++ < 16) {
|
||||
MethodCallExpr call = (MethodCallExpr) current;
|
||||
if ("withPayload".equals(call.getNameAsString()) && !call.getArguments().isEmpty()) {
|
||||
return call.getArgument(0);
|
||||
}
|
||||
if (!call.getScope().isPresent()) {
|
||||
break;
|
||||
}
|
||||
current = call.getScope().get();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression unwrapJsonStringVar(Expression payloadArg, MethodCallExpr mce) {
|
||||
if (!(payloadArg instanceof NameExpr)) {
|
||||
return null;
|
||||
}
|
||||
String name = ((NameExpr) payloadArg).getNameAsString();
|
||||
Type declared = findVariableType(name, mce);
|
||||
if (declared != null) {
|
||||
String simple = declared.isClassOrInterfaceType()
|
||||
? declared.asClassOrInterfaceType().getNameAsString()
|
||||
: declared.asString();
|
||||
if (!"String".equals(simple)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
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()) {
|
||||
continue;
|
||||
}
|
||||
return unwrapSerializer(var.getInitializer().get());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isEnvelopeBare(Expression typeExpr, MethodCallExpr contextCall) {
|
||||
if (!(typeExpr instanceof NameExpr)) {
|
||||
return false;
|
||||
}
|
||||
Type declared = findVariableType(((NameExpr) typeExpr).getNameAsString(), contextCall);
|
||||
if (declared == null) {
|
||||
return false;
|
||||
}
|
||||
String simple = declared.isClassOrInterfaceType()
|
||||
? declared.asClassOrInterfaceType().getNameAsString()
|
||||
: declared.asString();
|
||||
return ENVELOPE_TYPES.contains(simple);
|
||||
}
|
||||
|
||||
private boolean isRocketMqScope(MethodCallExpr mce) {
|
||||
String scope = mce.getScope().map(Expression::toString).orElse("").toLowerCase();
|
||||
return scope.contains("rocketmqtemplate") || scope.contains("rocketmq");
|
||||
@@ -177,7 +377,7 @@ public class MqWritePointDetector {
|
||||
String simple = declared.isClassOrInterfaceType()
|
||||
? declared.asClassOrInterfaceType().getNameAsString()
|
||||
: declared.asString();
|
||||
if (IGNORE_PAYLOAD_TYPES.contains(simple) || "byte[]".equals(declared.asString())) {
|
||||
if (IGNORE_BARE_TYPES.contains(simple) || "byte[]".equals(declared.asString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -306,6 +506,15 @@ public class MqWritePointDetector {
|
||||
}
|
||||
return new InferredType(null, true);
|
||||
}
|
||||
if (ENVELOPE_TYPES.contains(simple)) {
|
||||
Optional<Type> payload = cit.getTypeArguments()
|
||||
.filter(a -> !a.isEmpty())
|
||||
.map(a -> a.get(a.size() - 1));
|
||||
if (payload.isPresent()) {
|
||||
return resolveTypeNode(payload.get(), context);
|
||||
}
|
||||
return new InferredType(null, false);
|
||||
}
|
||||
return new InferredType(resolveFqn(cit, context), false);
|
||||
}
|
||||
|
||||
@@ -317,6 +526,16 @@ public class MqWritePointDetector {
|
||||
return fqn;
|
||||
}
|
||||
|
||||
private static final class PayloadResolution {
|
||||
final Expression typeExpr;
|
||||
final String overridePattern;
|
||||
|
||||
PayloadResolution(Expression typeExpr, String overridePattern) {
|
||||
this.typeExpr = typeExpr;
|
||||
this.overridePattern = overridePattern;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InferredType {
|
||||
final String fqn;
|
||||
final boolean isArray;
|
||||
|
||||
Reference in New Issue
Block a user