OpenRewrite中引用Recipe应用类及解决测试IllegalStateException问题
问题描述
开发OpenRewrite Recipe时遇到两个问题:
- 如何引用Recipe所应用的类?
- 运行Recipe测试时抛出
IllegalStateException:
java.lang.IllegalStateException: LST contains missing or invalid type information
Identifier->FieldAccess->MethodInvocation->NamedVariable->VariableDeclarations->Block->ClassDeclaration->CompilationUnit
/(Identifier type is missing or malformed)>/Test
推测原因是未像添加IString那样将Test类加入类路径,想知道是否有办法将Test添加到类路径,或通过其他方式避免Test.class抛出异常。
已尝试通过#{ }占位符替换为传入的类名来规避直接引用Test,相关代码如下:
测试代码
public class StringToIStringTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { spec.recipe(new StringToIString("Test")).parser(JavaParser.fromJavaVersion().logCompilationWarningsAndErrors(true).classpath("com.something.core.services")); } @Test void prerequisiteTest() { rewriteRun( //language=java java( """ class Test { } """, """ import com.something.core.services.IString; class Test { private static final IString.Builder STRBLD = IString.builder(Test.class); } """ ) ); } }
Recipe代码
public class StringToIString extends Recipe { @NonNull String fullyQualifiedClassName; @JsonCreator public StringToIString(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedClassName) { this.fullyQualifiedClassName = fullyQualifiedClassName; } @Override public TreeVisitor<?, ExecutionContext> getVisitor() { return new StringVisitor(); } public class StringVisitor extends JavaIsoVisitor<ExecutionContext> { private final JavaTemplate stringBuilderTemplate = JavaTemplate.builder("private static final IString.Builder STRBLD = IString.builder(#{}.class);").imports("com.something.core.services.IString").javaParser(JavaParser.fromJavaVersion().classpath(fullyQualifiedClassName).classpath("com.something.core.services")).build(); @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) { //check for right class if (classDecl.getType() == null || !classDecl.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) { return classDecl; } //check if STRBLD is present and add STRBLD boolean stringBuilderExists = classDecl.getBody().getStatements().stream().filter(statement -> statement instanceof J.VariableDeclarations).map(J.VariableDeclarations.class::cast).anyMatch(variableDeclarations -> variableDeclarations.toString().equals("private static final IString.Builder STRBLD = IString.builder(" +fullyQualifiedClassName + ".class)")); if (!stringBuilderExists) { classDecl = classDecl.withBody(stringBuilderTemplate.apply(new Cursor(getCursor(), classDecl.getBody()), classDecl.getBody().getCoordinates().lastStatement(), fullyQualifiedClassName)); maybeAddImport("com.something.core.services.IString"); } return classDecl; } } }
解决方案
问题1:引用Recipe所应用的类
无需手动传入类名,直接通过visitClassDeclaration方法中的classDecl对象获取类型信息即可:
// 获取当前处理类的全限定名 String currentClassName = classDecl.getType().getFullyQualifiedName();
这样可以去掉构造函数中传入类名的逻辑,直接动态获取当前处理的类。
问题2:解决LST类型信息缺失异常
测试中的Test类是内联代码,不会被自动加入解析器的类路径,导致类型解析失败。可以通过以下两种方式解决:
方式1:将测试类所在路径加入解析器类路径
在测试类的defaults方法中,把测试类的类路径添加到JavaParser中:
@Override public void defaults(RecipeSpec spec) { ClassLoader classLoader = getClass().getClassLoader(); spec.recipe(new StringToIString("Test")) .parser(JavaParser.fromJavaVersion() .logCompilationWarningsAndErrors(true) .classpath("com.something.core.services") .classpath(classLoader.getResource("").getPath())); // 添加测试类所在路径 }
方式2:使用类型引用而非字符串拼接.class
修改JavaTemplate和应用逻辑,直接注入类字面量的语法树节点,避免解析器无法识别类型:
// 修改模板定义,去掉手动拼接的.class private final JavaTemplate stringBuilderTemplate = JavaTemplate.builder("private static final IString.Builder STRBLD = IString.builder(#{});") .imports("com.something.core.services.IString") .javaParser(JavaParser.fromJavaVersion().classpath("com.something.core.services")) .build(); // 应用模板时传入类字面量节点 @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) { if (classDecl.getType() == null) { return classDecl; } String currentClassName = classDecl.getType().getFullyQualifiedName(); if (!currentClassName.equals(fullyQualifiedClassName)) { return classDecl; } // 检查STRBLD是否存在(优化判断逻辑) boolean stringBuilderExists = classDecl.getBody().getStatements().stream() .filter(J.VariableDeclarations.class::isInstance) .map(J.VariableDeclarations.class::cast) .anyMatch(vd -> vd.getVariables().stream() .anyMatch(var -> "STRBLD".equals(var.getName().getSimpleName()) && var.getType() instanceof J.ParameterizedType paramType && "IString.Builder".equals(paramType.getType().getFullyQualifiedName()))); if (!stringBuilderExists) { // 构造类字面量节点 J.Ident classIdent = new J.Ident(null, classDecl.getType().getShortName()); J.FieldAccess classLiteral = new J.FieldAccess(null, classIdent, "class"); classDecl = classDecl.withBody(stringBuilderTemplate.apply( new Cursor(getCursor(), classDecl.getBody()), classDecl.getBody().getCoordinates().lastStatement(), classLiteral )); maybeAddImport("com.something.core.services.IString"); } return classDecl; }
额外优化:变量存在性判断逻辑
不要用toString()判断变量是否存在,容易因格式细节出错,改用类型和变量名匹配的方式,如上述代码所示。
内容的提问来源于stack exchange,提问作者user26677970

