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; /** Redis key 表达式静态模式推断单测(常量、format、拼接等)。 */ 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); } @Test void resolvesLocalVariableAssignedFromStringFormat() { String source = "" + "package demo;\n" + "public class CombinationRepo {\n" + " private static final String COMBINATION_QUERY_KEY = " + "\"data_analysis:combination_query_key:%s:%s\";\n" + " public void write(String tenantId, String hashKey) {\n" + " String key = String.format(COMBINATION_QUERY_KEY, tenantId, hashKey);\n" + " redisSet(key);\n" + " }\n" + " void redisSet(String k) {}\n" + "}\n"; SourceIndex index = new SourceIndex(); index.addSource(source); CompilationUnit cu = StaticJavaParser.parse(source); ClassOrInterfaceDeclaration clazz = cu.getType(0).asClassOrInterfaceDeclaration(); // redisSet(key) 的实参 key —— 局部变量使用点 MethodCallExpr redisSet = cu.findAll(MethodCallExpr.class).stream() .filter(m -> "redisSet".equals(m.getNameAsString())) .findFirst() .orElseThrow(IllegalStateException::new); Expression keyArg = redisSet.getArgument(0); String pattern = new RedisKeyResolver(index).resolve( keyArg, clazz, index.get("demo.CombinationRepo")); assertEquals("data_analysis:combination_query_key:*:*", pattern); } @Test void localReassignmentUsesNearestBindingBeforeUse() { String source = "" + "package demo;\n" + "public class ReassignRepo {\n" + " private static final String A = \"prefix:a:%s\";\n" + " private static final String B = \"prefix:b:%s\";\n" + " public void write(String id) {\n" + " String key = String.format(A, id);\n" + " key = String.format(B, id);\n" + " redisSet(key);\n" + " }\n" + " void redisSet(String k) {}\n" + "}\n"; SourceIndex index = new SourceIndex(); index.addSource(source); CompilationUnit cu = StaticJavaParser.parse(source); ClassOrInterfaceDeclaration clazz = cu.getType(0).asClassOrInterfaceDeclaration(); MethodCallExpr redisSet = cu.findAll(MethodCallExpr.class).stream() .filter(m -> "redisSet".equals(m.getNameAsString())) .findFirst() .orElseThrow(IllegalStateException::new); String pattern = new RedisKeyResolver(index).resolve( redisSet.getArgument(0), clazz, index.get("demo.ReassignRepo")); assertEquals("prefix:b:*", pattern); } @Test void staticConstantStillPreferredOverLocalNameCollision() { // 局部变量名与静态常量同名时:仍优先静态常量(保持旧语义) String source = "" + "package demo;\n" + "public class Collision {\n" + " private static final String KEY = \"const:prefix:%s\";\n" + " public void write(String id) {\n" + " String KEY = \"local:\" + id;\n" + " redisSet(KEY);\n" + " }\n" + " void redisSet(String k) {}\n" + "}\n"; SourceIndex index = new SourceIndex(); index.addSource(source); CompilationUnit cu = StaticJavaParser.parse(source); ClassOrInterfaceDeclaration clazz = cu.getType(0).asClassOrInterfaceDeclaration(); MethodCallExpr redisSet = cu.findAll(MethodCallExpr.class).stream() .filter(m -> "redisSet".equals(m.getNameAsString())) .findFirst() .orElseThrow(IllegalStateException::new); String pattern = new RedisKeyResolver(index).resolve( redisSet.getArgument(0), clazz, index.get("demo.Collision")); assertEquals("const:prefix:%s", pattern); } }