This commit is contained in:
@@ -31,22 +31,24 @@ public class FileScanner {
|
||||
public Map<String, String> scan() {
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
try (Stream<Path> stream = Files.walk(repoRoot)) {
|
||||
stream.filter(Files::isRegularFile)
|
||||
List<Path> javaFiles = stream
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".java"))
|
||||
.forEach(p -> {
|
||||
.filter(p -> {
|
||||
String rel = repoRoot.relativize(p).toString().replace('\\', '/');
|
||||
if (!rel.contains("/src/main/java/")) {
|
||||
return;
|
||||
}
|
||||
if (!moduleAllowed(rel)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
result.put(rel, new String(Files.readAllBytes(p), StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
return rel.contains("/src/main/java/") && moduleAllowed(rel);
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
javaFiles.parallelStream().forEach(p -> {
|
||||
String rel = repoRoot.relativize(p).toString().replace('\\', '/');
|
||||
try {
|
||||
synchronized (result) {
|
||||
result.put(rel, new String(Files.readAllBytes(p), StandardCharsets.UTF_8));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ public class SchemaCheckAnalyzer {
|
||||
List<WritePoint> newWps = detectorNew.detect(path, newContent);
|
||||
List<WritePoint> oldWps = oldContent == null
|
||||
? new ArrayList<>() : detectorOld.detect(path, oldContent);
|
||||
newWps.forEach(this::applyManualMappings);
|
||||
oldWps.forEach(this::applyManualMappings);
|
||||
|
||||
Map<String, WritePoint> oldBySig = new LinkedHashMap<>();
|
||||
for (WritePoint wp : oldWps) {
|
||||
@@ -399,12 +401,30 @@ public class SchemaCheckAnalyzer {
|
||||
|
||||
private SourceIndex buildIndex(Iterable<String> contents) {
|
||||
SourceIndex index = new SourceIndex();
|
||||
List<String> list = new ArrayList<>();
|
||||
for (String content : contents) {
|
||||
index.addSource(content);
|
||||
list.add(content);
|
||||
}
|
||||
index.addSources(list);
|
||||
return index;
|
||||
}
|
||||
|
||||
private void applyManualMappings(WritePoint wp) {
|
||||
String location = wp.getEnclosingClass() + "#" + wp.getEnclosingMethod();
|
||||
for (CheckerConfig.ManualMapping mapping : config.getManualMappings()) {
|
||||
if (mapping.getWriterMethod() == null || !mapping.getWriterMethod().equals(location)) {
|
||||
continue;
|
||||
}
|
||||
if (mapping.getKeyPattern() != null && !mapping.getKeyPattern().isEmpty()) {
|
||||
wp.setResolvedKeyPattern(mapping.getKeyPattern());
|
||||
}
|
||||
if (mapping.getValueType() != null && !mapping.getValueType().isEmpty()) {
|
||||
wp.setResolvedValueType(mapping.getValueType());
|
||||
wp.setConfidence(Math.max(wp.getConfidence(), 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void collectTypeNames(String content, Set<String> out) {
|
||||
if (content == null || content.isEmpty()) {
|
||||
return;
|
||||
|
||||
@@ -11,10 +11,17 @@ 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;
|
||||
import com.github.javaparser.ast.expr.CastExpr;
|
||||
import com.github.javaparser.ast.expr.Expression;
|
||||
import com.github.javaparser.ast.expr.IntegerLiteralExpr;
|
||||
import com.github.javaparser.ast.expr.LongLiteralExpr;
|
||||
import com.github.javaparser.ast.expr.MethodCallExpr;
|
||||
import com.github.javaparser.ast.expr.NameExpr;
|
||||
import com.github.javaparser.ast.expr.NullLiteralExpr;
|
||||
import com.github.javaparser.ast.expr.ObjectCreationExpr;
|
||||
import com.github.javaparser.ast.expr.StringLiteralExpr;
|
||||
import com.github.javaparser.ast.type.ClassOrInterfaceType;
|
||||
import com.github.javaparser.ast.type.Type;
|
||||
|
||||
@@ -26,13 +33,19 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 从单个 Java 源文件中检测 Redis value 写入点(W01~W03:JSON 字符串写入)。
|
||||
* 从单个 Java 源文件中检测 Redis value 写入点(W01~W05)。
|
||||
*/
|
||||
public class RedisWritePointDetector {
|
||||
|
||||
private static final Set<String> SERIALIZE_METHODS = new HashSet<>(Arrays.asList(
|
||||
"toJSONString", "toJsonString", "getObjectToString", "toJsonStr", "writeValueAsString"));
|
||||
private static final Set<String> WRITE_METHODS = new HashSet<>(Arrays.asList("set", "insert"));
|
||||
private static final Set<String> VALUE_WRITE_METHODS = new HashSet<>(Arrays.asList("set", "insert"));
|
||||
private static final Set<String> SKIP_METHODS = new HashSet<>(Arrays.asList(
|
||||
"setIfAbsent", "setIfPresent", "increment", "decrement", "delete", "remove",
|
||||
"expire", "get", "hasKey", "exists", "setnx", "getAndSet", "getAndDelete",
|
||||
"multiGet", "multiSet", "keys", "scan"));
|
||||
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"));
|
||||
|
||||
@@ -59,58 +72,133 @@ public class RedisWritePointDetector {
|
||||
}
|
||||
|
||||
for (MethodCallExpr mce : cu.findAll(MethodCallExpr.class)) {
|
||||
String method = mce.getNameAsString();
|
||||
if (!WRITE_METHODS.contains(method)) {
|
||||
continue;
|
||||
WritePoint wp = tryDetectValueWrite(mce, filePath);
|
||||
if (wp == null) {
|
||||
wp = tryDetectHashWrite(mce, filePath);
|
||||
}
|
||||
String scope = mce.getScope().map(Expression::toString).orElse("");
|
||||
if (!isRedisScope(scope)) {
|
||||
continue;
|
||||
if (wp != null) {
|
||||
result.add(wp);
|
||||
}
|
||||
if (mce.getArguments().size() < 2) {
|
||||
continue;
|
||||
}
|
||||
Expression valueArg = mce.getArgument(1);
|
||||
Expression serialized = unwrapSerializer(valueArg);
|
||||
if (serialized == null) {
|
||||
continue; // 非 JSON 字符串写入(W04/W05 归后续阶段)
|
||||
}
|
||||
String pattern = classify(method, valueArg);
|
||||
if (!enabledPatterns.contains(pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WritePoint wp = new WritePoint();
|
||||
wp.setFilePath(filePath);
|
||||
wp.setLineNumber(mce.getBegin().map(p -> p.line).orElse(0));
|
||||
wp.setPattern(pattern);
|
||||
wp.setKeyExpression(mce.getArgument(0).toString());
|
||||
wp.setValueExpression(valueArg.toString());
|
||||
|
||||
fillEnclosing(mce, wp);
|
||||
|
||||
SourceIndex.IndexedType context = index.get(wp.getEnclosingClass());
|
||||
ClassOrInterfaceDeclaration enclosingDecl = mce
|
||||
.findAncestor(ClassOrInterfaceDeclaration.class).orElse(null);
|
||||
wp.setResolvedKeyPattern(keyResolver.resolve(mce.getArgument(0), enclosingDecl, context));
|
||||
InferredType inferred = inferType(serialized, mce, context);
|
||||
if (inferred != null) {
|
||||
wp.setResolvedValueType(inferred.fqn);
|
||||
wp.setRootArray(inferred.isArray);
|
||||
wp.setConfidence(inferred.fqn == null ? 0.4 : 1.0);
|
||||
} else {
|
||||
wp.setConfidence(0.4);
|
||||
}
|
||||
result.add(wp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isRedisScope(String scope) {
|
||||
private WritePoint tryDetectValueWrite(MethodCallExpr mce, String filePath) {
|
||||
String method = mce.getNameAsString();
|
||||
if (!VALUE_WRITE_METHODS.contains(method) || SKIP_METHODS.contains(method)) {
|
||||
return null;
|
||||
}
|
||||
if (!isValueOpsScope(mce)) {
|
||||
return null;
|
||||
}
|
||||
if (mce.getArguments().size() < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Expression valueArg = mce.getArgument(1);
|
||||
Expression serialized = unwrapSerializer(valueArg);
|
||||
String pattern;
|
||||
Expression typeExpr;
|
||||
if (serialized != null) {
|
||||
pattern = classifySerialized(method, valueArg);
|
||||
typeExpr = serialized;
|
||||
} else if (enabledPatterns.contains("W04") && !isTrivialValue(valueArg)) {
|
||||
pattern = "W04";
|
||||
typeExpr = valueArg;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (!enabledPatterns.contains(pattern)) {
|
||||
return null;
|
||||
}
|
||||
return buildWritePoint(mce, filePath, pattern, mce.getArgument(0), valueArg, typeExpr);
|
||||
}
|
||||
|
||||
private WritePoint tryDetectHashWrite(MethodCallExpr mce, String filePath) {
|
||||
if (!"put".equals(mce.getNameAsString()) || !enabledPatterns.contains("W05")) {
|
||||
return null;
|
||||
}
|
||||
if (!isHashOpsScope(mce) || mce.getArguments().size() < 3) {
|
||||
return null;
|
||||
}
|
||||
Expression valueArg = mce.getArgument(2);
|
||||
if (isTrivialValue(valueArg)) {
|
||||
return null;
|
||||
}
|
||||
Expression serialized = unwrapSerializer(valueArg);
|
||||
Expression typeExpr = serialized != null ? serialized : valueArg;
|
||||
return buildWritePoint(mce, filePath, "W05", mce.getArgument(0), valueArg, typeExpr);
|
||||
}
|
||||
|
||||
private WritePoint buildWritePoint(MethodCallExpr mce, String filePath, String pattern,
|
||||
Expression keyArg, Expression valueArg, Expression typeExpr) {
|
||||
WritePoint wp = new WritePoint();
|
||||
wp.setFilePath(filePath);
|
||||
wp.setLineNumber(mce.getBegin().map(p -> p.line).orElse(0));
|
||||
wp.setPattern(pattern);
|
||||
wp.setKeyExpression(keyArg.toString());
|
||||
wp.setValueExpression(valueArg.toString());
|
||||
fillEnclosing(mce, wp);
|
||||
|
||||
SourceIndex.IndexedType context = index.get(wp.getEnclosingClass());
|
||||
ClassOrInterfaceDeclaration enclosingDecl = mce
|
||||
.findAncestor(ClassOrInterfaceDeclaration.class).orElse(null);
|
||||
wp.setResolvedKeyPattern(keyResolver.resolve(keyArg, enclosingDecl, context));
|
||||
InferredType inferred = inferType(typeExpr, mce, context);
|
||||
if (inferred != null) {
|
||||
wp.setResolvedValueType(inferred.fqn);
|
||||
wp.setRootArray(inferred.isArray);
|
||||
wp.setConfidence(inferred.fqn == null ? 0.4 : 1.0);
|
||||
} else {
|
||||
wp.setConfidence(0.4);
|
||||
}
|
||||
return wp;
|
||||
}
|
||||
|
||||
private boolean isValueOpsScope(MethodCallExpr mce) {
|
||||
String scope = mce.getScope().map(Expression::toString).orElse("");
|
||||
String lower = scope.toLowerCase();
|
||||
return lower.contains("redis") || lower.contains("opsforvalue") || lower.contains("boundvalueops");
|
||||
}
|
||||
|
||||
private boolean isHashOpsScope(MethodCallExpr mce) {
|
||||
String scope = mce.getScope().map(Expression::toString).orElse("");
|
||||
String lower = scope.toLowerCase();
|
||||
return lower.contains("opsforhash") || lower.contains("boundhashops");
|
||||
}
|
||||
|
||||
private boolean isTrivialValue(Expression expr) {
|
||||
if (expr instanceof StringLiteralExpr
|
||||
|| expr instanceof IntegerLiteralExpr
|
||||
|| expr instanceof LongLiteralExpr
|
||||
|| expr instanceof BooleanLiteralExpr
|
||||
|| expr instanceof NullLiteralExpr) {
|
||||
return true;
|
||||
}
|
||||
if (expr instanceof BinaryExpr) {
|
||||
BinaryExpr be = (BinaryExpr) expr;
|
||||
if (be.getOperator() == BinaryExpr.Operator.PLUS) {
|
||||
return isTrivialValue(be.getLeft()) && isTrivialValue(be.getRight());
|
||||
}
|
||||
}
|
||||
if (expr instanceof MethodCallExpr) {
|
||||
MethodCallExpr call = (MethodCallExpr) expr;
|
||||
String name = call.getNameAsString();
|
||||
if (TRIVIAL_VALUE_CALLS.contains(name)) {
|
||||
return true;
|
||||
}
|
||||
if ("valueOf".equals(name) && !call.getArguments().isEmpty()) {
|
||||
return isTrivialValue(call.getArgument(0));
|
||||
}
|
||||
}
|
||||
if (expr instanceof ObjectCreationExpr) {
|
||||
ObjectCreationExpr oce = (ObjectCreationExpr) expr;
|
||||
String typeName = oce.getType().getNameAsString();
|
||||
return "UUID".equals(typeName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Expression unwrapSerializer(Expression valueArg) {
|
||||
if (valueArg instanceof MethodCallExpr) {
|
||||
MethodCallExpr call = (MethodCallExpr) valueArg;
|
||||
@@ -121,7 +209,7 @@ public class RedisWritePointDetector {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String classify(String method, Expression valueArg) {
|
||||
private String classifySerialized(String method, Expression valueArg) {
|
||||
String serializer = valueArg instanceof MethodCallExpr
|
||||
? ((MethodCallExpr) valueArg).getNameAsString() : "";
|
||||
if ("insert".equals(method)) {
|
||||
@@ -145,6 +233,9 @@ public class RedisWritePointDetector {
|
||||
}
|
||||
|
||||
private InferredType inferType(Expression expr, MethodCallExpr contextCall, SourceIndex.IndexedType context) {
|
||||
if (expr instanceof CastExpr) {
|
||||
return resolveTypeNode(((CastExpr) expr).getType(), context);
|
||||
}
|
||||
if (expr instanceof ObjectCreationExpr) {
|
||||
ClassOrInterfaceType t = ((ObjectCreationExpr) expr).getType();
|
||||
return resolveTypeNode(t, context);
|
||||
@@ -157,6 +248,18 @@ public class RedisWritePointDetector {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
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 resolveTypeNode(md.getType(), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -164,20 +267,17 @@ public class RedisWritePointDetector {
|
||||
Optional<CallableDeclaration> callable = contextCall.findAncestor(CallableDeclaration.class);
|
||||
if (callable.isPresent()) {
|
||||
CallableDeclaration<?> decl = callable.get();
|
||||
// 局部变量
|
||||
for (VariableDeclarator var : decl.findAll(VariableDeclarator.class)) {
|
||||
if (var.getNameAsString().equals(name)) {
|
||||
return var.getType();
|
||||
}
|
||||
}
|
||||
// 方法参数
|
||||
for (Parameter p : decl.getParameters()) {
|
||||
if (p.getNameAsString().equals(name)) {
|
||||
return p.getType();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 类字段
|
||||
Optional<ClassOrInterfaceDeclaration> clazz = contextCall.findAncestor(ClassOrInterfaceDeclaration.class);
|
||||
if (clazz.isPresent()) {
|
||||
for (FieldDeclaration field : clazz.get().getFields()) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.util.Optional;
|
||||
*/
|
||||
public class RedisKeyResolver {
|
||||
|
||||
private static final int MAX_DEPTH = 6;
|
||||
private static final int MAX_DEPTH = 8;
|
||||
|
||||
private final SourceIndex index;
|
||||
|
||||
@@ -52,7 +52,7 @@ public class RedisKeyResolver {
|
||||
}
|
||||
if (expr instanceof NameExpr) {
|
||||
String name = ((NameExpr) expr).getNameAsString();
|
||||
String constVal = lookupConstant(enclosingClass, name);
|
||||
String constVal = lookupConstant(enclosingClass, name, context, depth);
|
||||
if (constVal != null) {
|
||||
return constVal;
|
||||
}
|
||||
@@ -62,12 +62,23 @@ public class RedisKeyResolver {
|
||||
if (expr instanceof FieldAccessExpr) {
|
||||
FieldAccessExpr fae = (FieldAccessExpr) expr;
|
||||
String fieldName = fae.getNameAsString();
|
||||
if (fae.getScope() instanceof NameExpr) {
|
||||
String scope = ((NameExpr) fae.getScope()).getNameAsString();
|
||||
String local = lookupConstant(enclosingClass, fieldName, context, depth);
|
||||
if (local != null && scope.equals(enclosingClass == null ? "" : enclosingClass.getNameAsString())) {
|
||||
return local;
|
||||
}
|
||||
String external = lookupExternalConstant(scope, fieldName, context, depth);
|
||||
if (external != null) {
|
||||
return external;
|
||||
}
|
||||
}
|
||||
String scope = fae.getScope().toString();
|
||||
String external = lookupExternalConstant(scope, fieldName, context);
|
||||
String external = lookupExternalConstant(scope, fieldName, context, depth);
|
||||
if (external != null) {
|
||||
return external;
|
||||
}
|
||||
String local = lookupConstant(enclosingClass, fieldName);
|
||||
String local = lookupConstant(enclosingClass, fieldName, context, depth);
|
||||
return local != null ? local : "*";
|
||||
}
|
||||
if (expr instanceof MethodCallExpr) {
|
||||
@@ -77,23 +88,26 @@ public class RedisKeyResolver {
|
||||
String fmt = resolveExpr(call.getArgument(0), enclosingClass, context, depth + 1);
|
||||
return fmt.replaceAll("%[-0-9.]*[sdxDX]", "*");
|
||||
}
|
||||
// buildCacheKey(...) 等本类方法:解析其 return 表达式
|
||||
String methodVal = lookupMethodReturn(enclosingClass, name, context, depth);
|
||||
return methodVal != null ? methodVal : "*";
|
||||
}
|
||||
return "*";
|
||||
}
|
||||
|
||||
private String lookupConstant(ClassOrInterfaceDeclaration clazz, String name) {
|
||||
private String lookupConstant(ClassOrInterfaceDeclaration clazz, String name,
|
||||
SourceIndex.IndexedType context, int depth) {
|
||||
if (clazz == null) {
|
||||
return null;
|
||||
}
|
||||
for (FieldDeclaration field : clazz.getFields()) {
|
||||
if (!field.isStatic()) {
|
||||
continue;
|
||||
}
|
||||
for (VariableDeclarator var : field.getVariables()) {
|
||||
if (var.getNameAsString().equals(name)) {
|
||||
Optional<Expression> init = var.getInitializer();
|
||||
if (init.isPresent() && init.get() instanceof StringLiteralExpr) {
|
||||
return ((StringLiteralExpr) init.get()).asString();
|
||||
if (init.isPresent()) {
|
||||
return resolveExpr(init.get(), clazz, context, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +115,8 @@ public class RedisKeyResolver {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String lookupExternalConstant(String scopeName, String fieldName, SourceIndex.IndexedType context) {
|
||||
private String lookupExternalConstant(String scopeName, String fieldName,
|
||||
SourceIndex.IndexedType context, int depth) {
|
||||
String fqn = index.resolveFqn(scopeName, context);
|
||||
if (fqn == null) {
|
||||
return null;
|
||||
@@ -110,7 +125,7 @@ public class RedisKeyResolver {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
return lookupConstant(type.getDeclaration(), fieldName);
|
||||
return lookupConstant(type.getDeclaration(), fieldName, type, depth);
|
||||
}
|
||||
|
||||
private String lookupMethodReturn(ClassOrInterfaceDeclaration clazz, String methodName,
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.Set;
|
||||
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
|
||||
* <ul>
|
||||
* <li>企微:按 key 展示位置/类型/序列化骨架变更,不含字段明细,不分 P0/P1/P2</li>
|
||||
* <li>未解析 key 展示源码表达式 + 灰色「key 未解析」提示</li>
|
||||
* <li>未解析 key 展示源码表达式 + 灰色「key 无法解析」提示</li>
|
||||
* <li>多 key 优先拼成一条;超过企微上限则按 key 拆成多条</li>
|
||||
* <li>CI:先打字段明细,再完整输出企微 Markdown(拆分后的每条)</li>
|
||||
* </ul>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package com.codechecker.cache.schema;
|
||||
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.FieldDeclaration;
|
||||
import com.github.javaparser.ast.expr.AnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
|
||||
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
|
||||
import com.github.javaparser.ast.expr.Expression;
|
||||
import com.github.javaparser.ast.expr.MemberValuePair;
|
||||
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.StringLiteralExpr;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 处理 Fastjson / Jackson 序列化相关注解:字段忽略与字段名映射。
|
||||
*/
|
||||
@@ -20,17 +27,12 @@ public final class AnnotationSupport {
|
||||
*/
|
||||
public static boolean isSerialized(FieldDeclaration field) {
|
||||
for (AnnotationExpr annotation : field.getAnnotations()) {
|
||||
String name = annotation.getNameAsString();
|
||||
if (name.equals("JsonIgnore")) {
|
||||
String name = simpleName(annotation);
|
||||
if (isIgnoreAnnotation(name)) {
|
||||
return false;
|
||||
}
|
||||
if (name.equals("JSONField") && annotation instanceof NormalAnnotationExpr) {
|
||||
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) {
|
||||
if (pair.getNameAsString().equals("serialize")
|
||||
&& pair.getValue().toString().equals("false")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (name.equals("JSONField") && isJsonFieldSerializeDisabled(annotation)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -41,38 +43,93 @@ public final class AnnotationSupport {
|
||||
*/
|
||||
public static String jsonName(FieldDeclaration field, String defaultName) {
|
||||
for (AnnotationExpr annotation : field.getAnnotations()) {
|
||||
String name = annotation.getNameAsString();
|
||||
String name = simpleName(annotation);
|
||||
if (name.equals("JsonProperty")) {
|
||||
String v = singleStringValue(annotation);
|
||||
String v = stringMember(annotation, "value");
|
||||
if (v != null && !v.isEmpty()) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
if (name.equals("JSONField") && annotation instanceof NormalAnnotationExpr) {
|
||||
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) {
|
||||
if (pair.getNameAsString().equals("name")
|
||||
&& pair.getValue() instanceof StringLiteralExpr) {
|
||||
String v = ((StringLiteralExpr) pair.getValue()).asString();
|
||||
if (!v.isEmpty()) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
if (name.equals("JSONField")) {
|
||||
String v = stringMember(annotation, "name");
|
||||
if (v != null && !v.isEmpty()) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultName;
|
||||
}
|
||||
|
||||
private static String singleStringValue(AnnotationExpr annotation) {
|
||||
/**
|
||||
* 类级别 @JsonIgnoreProperties 声明的忽略字段名。
|
||||
*/
|
||||
public static Set<String> ignoredProperties(ClassOrInterfaceDeclaration type) {
|
||||
Set<String> ignored = new HashSet<>();
|
||||
for (AnnotationExpr annotation : type.getAnnotations()) {
|
||||
if (!"JsonIgnoreProperties".equals(simpleName(annotation))) {
|
||||
continue;
|
||||
}
|
||||
collectIgnoredNames(annotation, ignored);
|
||||
}
|
||||
return ignored;
|
||||
}
|
||||
|
||||
private static boolean isIgnoreAnnotation(String name) {
|
||||
return name.equals("JsonIgnore")
|
||||
|| name.equals("Transient")
|
||||
|| name.equals("JsonIgnoreType");
|
||||
}
|
||||
|
||||
private static boolean isJsonFieldSerializeDisabled(AnnotationExpr annotation) {
|
||||
if (annotation instanceof NormalAnnotationExpr) {
|
||||
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) {
|
||||
if (pair.getNameAsString().equals("serialize")
|
||||
&& isFalseLiteral(pair.getValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void collectIgnoredNames(AnnotationExpr annotation, Set<String> out) {
|
||||
if (annotation instanceof SingleMemberAnnotationExpr) {
|
||||
if (((SingleMemberAnnotationExpr) annotation).getMemberValue() instanceof StringLiteralExpr) {
|
||||
return ((StringLiteralExpr) ((SingleMemberAnnotationExpr) annotation)
|
||||
.getMemberValue()).asString();
|
||||
addStringArray(((SingleMemberAnnotationExpr) annotation).getMemberValue(), out);
|
||||
return;
|
||||
}
|
||||
if (annotation instanceof NormalAnnotationExpr) {
|
||||
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) {
|
||||
if ("value".equals(pair.getNameAsString())) {
|
||||
addStringArray(pair.getValue(), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addStringArray(Expression expr, Set<String> out) {
|
||||
if (expr instanceof StringLiteralExpr) {
|
||||
out.add(((StringLiteralExpr) expr).asString());
|
||||
return;
|
||||
}
|
||||
if (expr instanceof ArrayInitializerExpr) {
|
||||
for (Expression value : ((ArrayInitializerExpr) expr).getValues()) {
|
||||
if (value instanceof StringLiteralExpr) {
|
||||
out.add(((StringLiteralExpr) value).asString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String stringMember(AnnotationExpr annotation, String member) {
|
||||
if (annotation instanceof SingleMemberAnnotationExpr) {
|
||||
Expression value = ((SingleMemberAnnotationExpr) annotation).getMemberValue();
|
||||
if (value instanceof StringLiteralExpr) {
|
||||
return ((StringLiteralExpr) value).asString();
|
||||
}
|
||||
}
|
||||
if (annotation instanceof NormalAnnotationExpr) {
|
||||
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) {
|
||||
if (pair.getNameAsString().equals("value")
|
||||
if (pair.getNameAsString().equals(member)
|
||||
&& pair.getValue() instanceof StringLiteralExpr) {
|
||||
return ((StringLiteralExpr) pair.getValue()).asString();
|
||||
}
|
||||
@@ -80,4 +137,14 @@ public final class AnnotationSupport {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isFalseLiteral(Expression expr) {
|
||||
return expr instanceof BooleanLiteralExpr && !((BooleanLiteralExpr) expr).getValue();
|
||||
}
|
||||
|
||||
private static String simpleName(AnnotationExpr annotation) {
|
||||
String name = annotation.getNameAsString();
|
||||
int dot = name.lastIndexOf('.');
|
||||
return dot >= 0 ? name.substring(dot + 1) : name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ public class JavaSchemaExtractor {
|
||||
Set<String> nextAncestors = new LinkedHashSet<>(ancestors);
|
||||
nextAncestors.add(type.getFqn());
|
||||
|
||||
Set<String> classIgnored = AnnotationSupport.ignoredProperties(type.getDeclaration());
|
||||
for (FieldDeclaration field : collectFields(type, new HashSet<>())) {
|
||||
if (field.isStatic() || field.isTransient()) {
|
||||
continue;
|
||||
@@ -88,6 +89,9 @@ public class JavaSchemaExtractor {
|
||||
}
|
||||
for (VariableDeclarator var : field.getVariables()) {
|
||||
String jsonName = AnnotationSupport.jsonName(field, var.getNameAsString());
|
||||
if (classIgnored.contains(jsonName) || classIgnored.contains(var.getNameAsString())) {
|
||||
continue;
|
||||
}
|
||||
String path = prefix.isEmpty() ? jsonName : prefix + "." + jsonName;
|
||||
expandType(var.getType(), path, type, schema, nextAncestors, depth);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.TypeDeclaration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 一次提交快照下的源码类型索引。仅索引本仓库源码(不含依赖 jar),供类型解析与字段展开使用。
|
||||
@@ -21,9 +22,9 @@ import java.util.Map;
|
||||
public class SourceIndex {
|
||||
|
||||
/** FQN(以 . 分隔,含内部类) -> 类型信息 */
|
||||
private final Map<String, IndexedType> byFqn = new LinkedHashMap<>();
|
||||
private final Map<String, IndexedType> byFqn = new ConcurrentHashMap<>();
|
||||
/** 简单类名 -> FQN 列表(兜底解析) */
|
||||
private final Map<String, List<String>> bySimpleName = new LinkedHashMap<>();
|
||||
private final Map<String, List<String>> bySimpleName = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
ParserConfiguration config = new ParserConfiguration()
|
||||
@@ -31,6 +32,20 @@ public class SourceIndex {
|
||||
StaticJavaParser.setConfiguration(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解析源文件并建立索引。
|
||||
* <p>文件读取可在 {@link com.codechecker.cache.analyze.FileScanner} 中并行;
|
||||
* AST 解析使用 StaticJavaParser 串行写入,避免其全局配置的线程安全问题。</p>
|
||||
*/
|
||||
public void addSources(Collection<String> contents) {
|
||||
if (contents == null || contents.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (String content : contents) {
|
||||
addSource(content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并加入一个 Java 源文件内容。解析失败时静默跳过(返回 false)。
|
||||
*/
|
||||
@@ -65,7 +80,6 @@ public class SourceIndex {
|
||||
byFqn.put(fqn, indexed);
|
||||
bySimpleName.computeIfAbsent(simpleName, k -> new ArrayList<>()).add(fqn);
|
||||
}
|
||||
// 递归内部类型
|
||||
for (Object member : type.getMembers()) {
|
||||
if (member instanceof TypeDeclaration) {
|
||||
registerType((TypeDeclaration<?>) member, packageName, imports, fqn);
|
||||
|
||||
@@ -42,6 +42,8 @@ detection:
|
||||
- W01 # redisUtil.insert(key, JSON.toJSONString(x), ttl)
|
||||
- W02 # redisTemplate.opsForValue().set(key, JSON.toJSONString(x), ...)
|
||||
- W03 # stringRedisTemplate.opsForValue().set(key, JsonUtil.getObjectToString(x), ...)
|
||||
- W04 # redisTemplate.opsForValue().set(key, obj, ...)
|
||||
- W05 # redisTemplate.opsForHash().put(key, field, obj)
|
||||
# 类型推断最低置信度,低于此值降级为 P2 提示
|
||||
min_confidence: 0.6
|
||||
# 字段展开最大深度(防止循环引用)
|
||||
|
||||
101
src/test/java/com/codechecker/cache/detector/RedisWritePointDetectorTest.java
vendored
Normal file
101
src/test/java/com/codechecker/cache/detector/RedisWritePointDetectorTest.java
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
package com.codechecker.cache.detector;
|
||||
|
||||
import com.codechecker.cache.TestSupport;
|
||||
import com.codechecker.cache.schema.SourceIndex;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RedisWritePointDetectorTest {
|
||||
|
||||
@Test
|
||||
void detectsW04DirectObjectWrite() {
|
||||
String source = ""
|
||||
+ "package demo;\n"
|
||||
+ "import demo.model.DemoVo;\n"
|
||||
+ "public class DemoService {\n"
|
||||
+ " private RedisTemplate<String, DemoVo> redisTemplate;\n"
|
||||
+ " public void save(String key, DemoVo vo) {\n"
|
||||
+ " redisTemplate.opsForValue().set(key, vo, 60, TimeUnit.SECONDS);\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
String vo = ""
|
||||
+ "package demo.model;\n"
|
||||
+ "public class DemoVo { private String name; }\n";
|
||||
|
||||
SourceIndex index = new SourceIndex();
|
||||
index.addSource(vo);
|
||||
index.addSource(source);
|
||||
|
||||
Set<String> patterns = new HashSet<>(Arrays.asList("W04"));
|
||||
List<WritePoint> wps = new RedisWritePointDetector(index, patterns)
|
||||
.detect("DemoService.java", source);
|
||||
|
||||
assertEquals(1, wps.size());
|
||||
WritePoint wp = wps.get(0);
|
||||
assertEquals("W04", wp.getPattern());
|
||||
assertEquals("demo.model.DemoVo", wp.getResolvedValueType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsW05HashPut() {
|
||||
String source = ""
|
||||
+ "package demo;\n"
|
||||
+ "import demo.model.DemoVo;\n"
|
||||
+ "public class DemoService {\n"
|
||||
+ " private RedisTemplate<String, Object> redisTemplate;\n"
|
||||
+ " public void save(String key, String field, DemoVo vo) {\n"
|
||||
+ " redisTemplate.opsForHash().put(key, field, vo);\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
String vo = ""
|
||||
+ "package demo.model;\n"
|
||||
+ "public class DemoVo { private String name; }\n";
|
||||
|
||||
SourceIndex index = new SourceIndex();
|
||||
index.addSource(vo);
|
||||
index.addSource(source);
|
||||
|
||||
Set<String> patterns = new HashSet<>(Arrays.asList("W05"));
|
||||
List<WritePoint> wps = new RedisWritePointDetector(index, patterns)
|
||||
.detect("DemoService.java", source);
|
||||
|
||||
assertEquals(1, wps.size());
|
||||
assertEquals("W05", wps.get(0).getPattern());
|
||||
assertEquals("demo.model.DemoVo", wps.get(0).getResolvedValueType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresLockAndLiteralWrites() {
|
||||
String source = TestSupport.fixture("fixtures/lock/LockService.txt");
|
||||
Set<String> patterns = new HashSet<>(Arrays.asList("W01", "W02", "W03", "W04", "W05"));
|
||||
List<WritePoint> wps = new RedisWritePointDetector(new SourceIndex(), patterns)
|
||||
.detect("LockService.java", source);
|
||||
assertTrue(wps.isEmpty(), "锁/计数器/token 写入应被忽略");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stillDetectsW01ToW03() {
|
||||
String helper = TestSupport.fixture("fixtures/tenant/HelperOld.txt");
|
||||
String tenantVo = TestSupport.fixture("fixtures/tenant/TenantVO.txt");
|
||||
String tenantLink = TestSupport.fixture("fixtures/tenant/TenantLinkModel.txt");
|
||||
|
||||
SourceIndex index = new SourceIndex();
|
||||
index.addSource(tenantVo);
|
||||
index.addSource(tenantLink);
|
||||
index.addSource(helper);
|
||||
|
||||
Set<String> patterns = new HashSet<>(Arrays.asList("W01", "W02", "W03"));
|
||||
List<WritePoint> wps = new RedisWritePointDetector(index, patterns)
|
||||
.detect("Helper.java", helper);
|
||||
|
||||
assertEquals(1, wps.size());
|
||||
assertEquals("W01", wps.get(0).getPattern());
|
||||
}
|
||||
}
|
||||
68
src/test/java/com/codechecker/cache/key/RedisKeyResolverTest.java
vendored
Normal file
68
src/test/java/com/codechecker/cache/key/RedisKeyResolverTest.java
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.codechecker.cache.key;
|
||||
|
||||
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.expr.Expression;
|
||||
import com.github.javaparser.ast.expr.MethodCallExpr;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class RedisKeyResolverTest {
|
||||
|
||||
@Test
|
||||
void resolvesStringFormatWithConstant() {
|
||||
String source = ""
|
||||
+ "package demo;\n"
|
||||
+ "public class AttendanceService {\n"
|
||||
+ " private static final String ATTENDANCE_BASE_SETTING_CACHE_KEY = "
|
||||
+ "\"fbt:attendance:base_setting:cache:%s\";\n"
|
||||
+ " public void save(String tenantId) {\n"
|
||||
+ " String key = String.format(ATTENDANCE_BASE_SETTING_CACHE_KEY, tenantId);\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
|
||||
SourceIndex index = new SourceIndex();
|
||||
index.addSource(source);
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
ClassOrInterfaceDeclaration clazz = cu.getType(0).asClassOrInterfaceDeclaration();
|
||||
MethodCallExpr formatCall = cu.findAll(MethodCallExpr.class).stream()
|
||||
.filter(m -> "format".equals(m.getNameAsString()))
|
||||
.findFirst()
|
||||
.orElseThrow(IllegalStateException::new);
|
||||
|
||||
String pattern = new RedisKeyResolver(index).resolve(
|
||||
formatCall, clazz, index.get("demo.AttendanceService"));
|
||||
assertEquals("fbt:attendance:base_setting:cache:*", pattern);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesConstantConcatAndBuildMethod() {
|
||||
String source = ""
|
||||
+ "package jnpf.util;\n"
|
||||
+ "public class TenantDbContentCacheHelper {\n"
|
||||
+ " private static final String CACHE_KEY_PREFIX = \"tenant:db:content:\";\n"
|
||||
+ " public String buildCacheKey(String encode) {\n"
|
||||
+ " return CACHE_KEY_PREFIX + encode;\n"
|
||||
+ " }\n"
|
||||
+ " public void cache(String encode) {\n"
|
||||
+ " String key = buildCacheKey(encode);\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
|
||||
SourceIndex index = new SourceIndex();
|
||||
index.addSource(source);
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
ClassOrInterfaceDeclaration clazz = cu.getType(0).asClassOrInterfaceDeclaration();
|
||||
MethodCallExpr buildCall = cu.findAll(MethodCallExpr.class).stream()
|
||||
.filter(m -> "buildCacheKey".equals(m.getNameAsString()))
|
||||
.findFirst()
|
||||
.orElseThrow(IllegalStateException::new);
|
||||
|
||||
String pattern = new RedisKeyResolver(index).resolve(
|
||||
buildCall, clazz, index.get("jnpf.util.TenantDbContentCacheHelper"));
|
||||
assertEquals("tenant:db:content:*", pattern);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ class ReportBuilderTest {
|
||||
String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report);
|
||||
|
||||
assertTrue(md.contains("- Key --> `saas:period-config:migration:current`"));
|
||||
assertFalse(md.contains("(key 未解析)"));
|
||||
assertFalse(md.contains("(key 无法解析)"));
|
||||
assertTrue(md.contains("> **位置**: `SaasPeriodConfigMigrationRedisSupport#putCurrent:41`"));
|
||||
assertTrue(md.contains("> **类型**: `MigrationCurrentVo`"));
|
||||
|
||||
@@ -158,10 +158,10 @@ class ReportBuilderTest {
|
||||
report.getKeyChanges().add(key);
|
||||
|
||||
String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report);
|
||||
assertTrue(md.contains("- Key --> `req.getKey()` <font color=\"comment\">(key 未解析)</font>"));
|
||||
assertTrue(md.contains("- Key --> `req.getKey()` <font color=\"comment\">(key 无法解析)</font>"));
|
||||
assertTrue(md.contains("> **位置**: `ClockInXxxService#export:128`"));
|
||||
assertTrue(md.contains("> **类型**: `List<ClockInExportVo>`"));
|
||||
assertFalse(md.contains("unknown-key"));
|
||||
assertFalse(md.contains("`unknown-key`"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
84
src/test/java/com/codechecker/cache/schema/JavaSchemaExtractorTest.java
vendored
Normal file
84
src/test/java/com/codechecker/cache/schema/JavaSchemaExtractorTest.java
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
package com.codechecker.cache.schema;
|
||||
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.FieldDeclaration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class JavaSchemaExtractorTest {
|
||||
|
||||
@Test
|
||||
void honorsJsonIgnoreAndPropertyRename() {
|
||||
String source = ""
|
||||
+ "package demo;\n"
|
||||
+ "import com.fasterxml.jackson.annotation.JsonIgnore;\n"
|
||||
+ "import com.fasterxml.jackson.annotation.JsonProperty;\n"
|
||||
+ "public class AnnotatedVo {\n"
|
||||
+ " @JsonProperty(\"display_name\")\n"
|
||||
+ " private String name;\n"
|
||||
+ " @JsonIgnore\n"
|
||||
+ " private String secret;\n"
|
||||
+ " private String visible;\n"
|
||||
+ "}\n";
|
||||
|
||||
SourceIndex index = new SourceIndex();
|
||||
index.addSource(source);
|
||||
TypeSchema schema = new JavaSchemaExtractor(index, 8).extract("demo.AnnotatedVo");
|
||||
|
||||
assertTrue(schema.getFields().containsKey("display_name"));
|
||||
assertTrue(schema.getFields().containsKey("visible"));
|
||||
assertFalse(schema.getFields().containsKey("secret"));
|
||||
assertFalse(schema.getFields().containsKey("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void honorsFastjsonFieldAndClassIgnoreProperties() {
|
||||
String source = ""
|
||||
+ "package demo;\n"
|
||||
+ "import com.alibaba.fastjson.annotation.JSONField;\n"
|
||||
+ "import com.fasterxml.jackson.annotation.JsonIgnoreProperties;\n"
|
||||
+ "@JsonIgnoreProperties({\"password\"})\n"
|
||||
+ "public class FastVo {\n"
|
||||
+ " @JSONField(name = \"user_id\")\n"
|
||||
+ " private String userId;\n"
|
||||
+ " @JSONField(serialize = false)\n"
|
||||
+ " private String token;\n"
|
||||
+ " private String password;\n"
|
||||
+ "}\n";
|
||||
|
||||
SourceIndex index = new SourceIndex();
|
||||
index.addSource(source);
|
||||
TypeSchema schema = new JavaSchemaExtractor(index, 8).extract("demo.FastVo");
|
||||
|
||||
assertTrue(schema.getFields().containsKey("user_id"));
|
||||
assertFalse(schema.getFields().containsKey("token"));
|
||||
assertFalse(schema.getFields().containsKey("password"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonNameHelpersWorkOnFieldAnnotations() {
|
||||
CompilationUnit cu = StaticJavaParser.parse(""
|
||||
+ "class X {\n"
|
||||
+ " @com.fasterxml.jackson.annotation.JsonProperty(\"alias\")\n"
|
||||
+ " private String field;\n"
|
||||
+ "}");
|
||||
FieldDeclaration field = cu.getType(0).asClassOrInterfaceDeclaration().getFields().get(0);
|
||||
assertEquals("alias", AnnotationSupport.jsonName(field, "field"));
|
||||
assertTrue(AnnotationSupport.isSerialized(field));
|
||||
}
|
||||
|
||||
@Test
|
||||
void classIgnorePropertiesCollected() {
|
||||
CompilationUnit cu = StaticJavaParser.parse(""
|
||||
+ "@com.fasterxml.jackson.annotation.JsonIgnoreProperties({\"a\", \"b\"})\n"
|
||||
+ "class X {}");
|
||||
ClassOrInterfaceDeclaration type = cu.getType(0).asClassOrInterfaceDeclaration();
|
||||
assertTrue(AnnotationSupport.ignoredProperties(type).contains("a"));
|
||||
assertTrue(AnnotationSupport.ignoredProperties(type).contains("b"));
|
||||
}
|
||||
}
|
||||
16
src/test/resources/fixtures/lock/LockService.txt
Normal file
16
src/test/resources/fixtures/lock/LockService.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
package jnpf.lock;
|
||||
|
||||
public class LockService {
|
||||
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
public void acquire(String bizId) {
|
||||
redisTemplate.opsForValue().setIfAbsent("order:lock:" + bizId, "1", 30, TimeUnit.SECONDS);
|
||||
redisTemplate.opsForValue().increment("loginCount:" + bizId);
|
||||
redisTemplate.delete("temp:" + bizId);
|
||||
redisUtil.insert("Authorization:" + bizId, "token-abc", 60);
|
||||
redisTemplate.opsForValue().set("plain:flag", "1", 60, TimeUnit.SECONDS);
|
||||
redisTemplate.opsForValue().set("uuid:key", UUID.randomUUID().toString(), 60, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
12
src/test/resources/fixtures/template/TemplateWrite.txt
Normal file
12
src/test/resources/fixtures/template/TemplateWrite.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
package demo;
|
||||
|
||||
import demo.model.DemoVo;
|
||||
|
||||
public class TemplateService {
|
||||
|
||||
private RedisTemplate<String, DemoVo> redisTemplate;
|
||||
|
||||
public void cache(String key, DemoVo vo) {
|
||||
redisTemplate.opsForValue().set(key, vo, 3600, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user