漂亮的印刷和词法保存
您可以将JavaParser用于不同的目的。
最常见的两个是代码生成和代码转换。
在这两种情况下,您最终都将生成Java代码,可能将其存储在扩展名为.java的文件中。
在 CompilationUnit 上对 toString() 的简单调用将返回一个字符串,可用于编写源代码。
我们面临的问题是:它将如何格式化AST中的代码?
答案是两种方式之一:
-
使用漂亮的印刷
-
使用词法保留打印
什么是漂亮印刷?
漂亮的打印意味着当我们将代码作为源代码写出时,文本表示将以格式正确,标准的方式打印。
例如,如果要解析一个现有的java类,如下所示:
class A { int
a;}
然后,您决定使用JavaParser对它进行漂亮的打印,您将获得以下代码表示形式:
class A {
int a;
}
什么是词法保留打印?
保留词汇的印刷是指保持原始布局的印刷。
当我们编写代码时,有一些信息对于编译器而言并不是严格意义上的,但对人类而言却很重要。
即空格和注释的位置。
如果您解析了一些代码,然后以保留词法的打印方式将其打印回来,您将获得与解析后相同的代码。
如果解析某些代码,对其进行修改,然后使用lexicalpreserving打印将其打印回去,您将获得与原始代码非常接近的代码,并且更改仅限于已明确更改的节点。
即使在以编程方式构建且未从源解析的AST上,也可以始终使用保留词法的打印。
在这种情况下,没有原始布局可保留,并且代码将默认为漂亮的打印形式。
当您解析代码并将某些以编程方式创建的新节点添加到AST时,情况也是如此:您解析的代码部分将附加布局信息,而您创建的节点将没有布局信息,并且它们将被漂亮地打印。
在漂亮打印和保留词汇的打印之间进行选择
我们已经看到,使用JavaParser从AST生成代码时,可以使用两种不同的样式。
下一个要考虑的问题是何时应该使用一个,何时应该使用另一个?
从我们已经看到的情况来看,很多情况下都涉及将JavaParser用于在大型代码库上执行转换。
在这种情况下,您通常会希望使用词法保留打印。
原因是,您的转换未修改的所有代码将保持完全不变。
通常,这会使开发人员更加放心,当提交大型(甚至安全)更改时,开发人员可能会感到紧张。
这也使我们可以使用更小的diff文件,从而使手动代码审查变得容易。
最后,它使我们能够保留具有意义的复杂布局或格式。
当生成不用于手动审阅的代码或转换代码时,您可以选择漂亮的打印。
如果要强制执行特定的打印,漂亮的打印也是一个不错的选择 代码样式:每次保存代码时,都可以确保它格式正确,没有任何例外。
这些是我们认为普遍的考虑因素,但请记住,JavaParser和JavaSymbolSolver旨在用作在许多不同上下文中使用的工具。
作为开发人员,您必须分析您的上下文并为您做出适当的选择。
我们已经看到了漂亮打印和词法保留打印之间的区别。
现在,让我们看看如何使用它们以及它们如何工作。
格式化输出
代码
package com.github.houbb.javaparser.learn.chap4;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.comments.LineComment;
import com.github.javaparser.printer.PrettyPrinter;
import com.github.javaparser.printer.PrettyPrinterConfiguration;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class PrettyPrintComplete {
public static void main(String[] args) {
ClassOrInterfaceDeclaration myClass = new ClassOrInterfaceDeclaration();
myClass.setComment(new LineComment("A very cool class!"));
myClass.setName("MyClass");
myClass.addField("String", "foo");
PrettyPrinterConfiguration conf = new PrettyPrinterConfiguration();
conf.setIndentSize(1);
conf.setIndentType(PrettyPrinterConfiguration.IndentType.SPACES);
conf.setPrintComments(false);
PrettyPrinter prettyPrinter = new PrettyPrinter(conf);
System.out.println(prettyPrinter.print(myClass));
}
}
这里设置了 conf.setPrintComments(false)
注释关闭,输出结果如下:
class MyClass {
String foo;
}
格式化访问
代码
package com.github.houbb.javaparser.learn.chap4;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.comments.LineComment;
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
import com.github.javaparser.printer.PrettyPrintVisitor;
import com.github.javaparser.printer.PrettyPrinter;
import com.github.javaparser.printer.PrettyPrinterConfiguration;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class PrettyPrintVisitorComplete {
public static void main(String[] args) {
ClassOrInterfaceDeclaration myClass = new ClassOrInterfaceDeclaration();
myClass.setComment(new LineComment("A very cool class!"));
myClass.setName("MyClass");
myClass.addField("String", "foo");
myClass.addAnnotation("MySecretAnnotation");
PrettyPrinterConfiguration conf = new PrettyPrinterConfiguration();
conf.setIndentSize(2);
conf.setIndentType(PrettyPrinterConfiguration.IndentType.SPACES);
conf.setPrintComments(false);
conf.setVisitorFactory(prettyPrinterConfiguration -> new PrettyPrintVisitor(conf) {
@Override
public void visit(MarkerAnnotationExpr n, Void arg) {
// ignore
}
@Override
public void visit(SingleMemberAnnotationExpr n, Void arg) {
// ignore
}
@Override
public void visit(NormalAnnotationExpr n, Void arg) {
// ignore
}
});
PrettyPrinter prettyPrinter = new PrettyPrinter(conf);
System.out.println(prettyPrinter.print(myClass));
}
}
从这个代码我们可以看出,可以像前面的代码一样,访问其中的属性。
语法保留输出
简介
保留词汇的打印可用于保留解析代码时的原始布局。
这是它的主要目标,但是您可以将其用于已创建的节点。
在那种情况下,结果将等同于漂亮地打印那些节点。
设置词法保留打印的最简单方法是使用词法保留打印机的设置方法。
此方法将进行解析,注册初始文本并将观察者添加到AST中。
当您对AST进行操作时,观察者将在每次修改时调整文本。
在下面的示例中,您可以看到如何解析某些代码并通过使用lexicalpreserving打印将其打印回:
源码
package com.github.houbb.javaparser.learn.chap4;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class LexicalPreservationComplete {
public static void main(String[] args) {
String code = "// Hey, this is a comment\n\n\n// Another one\n\nclass A { }";
CompilationUnit cu = StaticJavaParser.parse(code);
LexicalPreservingPrinter.setup(cu);
System.out.println(LexicalPreservingPrinter.print(cu));
System.out.println("----------------");
ClassOrInterfaceDeclaration myClass = cu.getClassByName("A").get();
myClass.setName("MyNewClassName");
System.out.println(LexicalPreservingPrinter.print(cu));
System.out.println("----------------");
myClass = cu.getClassByName("MyNewClassName").get();
myClass.setName("MyNewClassName");
myClass.addModifier(Modifier.Keyword.PUBLIC);
System.out.println(LexicalPreservingPrinter.print(cu));
System.out.println("----------------");
myClass = cu.getClassByName("MyNewClassName").get();
myClass.setName("MyNewClassName");
myClass.addModifier(Modifier.Keyword.PUBLIC);
cu.setPackageDeclaration("org.javaparser.samples");
System.out.println(LexicalPreservingPrinter.print(cu));
}
}
- 输出
// Hey, this is a comment
// Another one
class A { }
----------------
// Hey, this is a comment
// Another one
class MyNewClassName { }
----------------
// Hey, this is a comment
// Another one
public class MyNewClassName { }
----------------
package org.javaparser.samples;
// Hey, this is a comment
// Another one
public class MyNewClassName { }
工作原理:NodeText和具体语法
模型我们之前曾描述过,词法保留会存储初始文本,这在某种程度上简化了所发生的事情。
词汇保留的实际作用是为每个节点。
NodeText基本上是子代的令牌或占位符的列表。
例如,如果考虑以下方法声明:
void foo(int a){}
它的 NodeText 看起来像这样:
如果我们排序不同的元素:
1. child(void)
2. token(space)
3. child(name)
4. token(lparen)
5. child(param#0)
6. token(rparen)
7. token(space)
8. child(body)
现在,假设您向此方法添加了一个参数。
会发生什么?
我们将需要更新与MethodDeclaration关联的NodeText。
词法保护设置将观察者附加到所有节点。 每次更改时,我们都会收到通知,并计算更改后该节点的外观,并将这些修改更新为NodeText。
为了了解更改后Node的外观,我们使用了ConcreteSyntaxModel。
什么是具体语法模型?
您可能想知道具体语法模型(CSM)是什么:它是节点通常应具有的外观的定义。
具体语法模型向我们解释了如何“解析” AST节点并将其转换为文本。
MethodDeclaration的ConcreteSyntaxModel如下所示:
concreteSyntaxModelByClass.put(MethodDeclaration.class, sequence(
orphanCommentsBeforeThis(),
comment(),
memberAnnotations(),
modifiers(),
conditional(ObservableProperty.DEFAULT, FLAG, sequence(token(GeneratedJavaPa\
rserConstants._DEFAULT), space())),
typeParameters(),
child(ObservableProperty.TYPE),
space(),
child(ObservableProperty.NAME),
token(GeneratedJavaParserConstants.LPAREN),
list(ObservableProperty.PARAMETERS, sequence(comma(), space()), none(), none\
()),
token(GeneratedJavaParserConstants.RPAREN),
list(ObservableProperty.THROWN_EXCEPTIONS, sequence(comma(), space()),
sequence(space(), token(GeneratedJavaParserConstants.THROWS), space(\
)), none()),
conditional(ObservableProperty.BODY, IS_PRESENT,sequence(space(), child(ObservableProperty.BODY)), semicolon())));
这非常复杂,因为它考虑了可以构成 MethodDeclaration 的所有潜在元素以及如何呈现所有这些元素。
考虑这一行,例如:
list(ObservableProperty.PARAMETERS, sequence(comma(), space()), none(), none()),
这告诉我们,理想情况下,MethodDeclaration的参数应以一个逗号和一个空格分隔。
或这些行:
conditional(ObservableProperty.BODY, IS_PRESENT, sequence(space(), child(ObservableProperty.BODY)), semicolon())
这告诉我们,如果存在该方法的主体,则应打印该文本,并在其前面加上空格,并在其后加上分号。
如果我们使用CSM来计算更改前节点的外观,我们将得到以下信息:
1. child(void)
2. child(name)
3. token(lparen)
4. child(param#0)
5. token(rparen)
6. token(space)
7. child(body)
更改后,它看起来像这样:
1. child(void)
2. child(name)
3. token(lparen)
4. child(param#0)
5. token(comma)
6. token(space)
7. child(param#1)
8. token(rparen)
9. token(space)
10. child(body)
计算CSM之间的差异
此时,将获得两个计算出的CSM之间的差异,并且基本上看起来像:
1. keep: child(void)
2. keep: child(name)
3. keep: token(lparen)
4. keep: child(param#0)
5. add: token(comma)
6. add: token(space)
7. add: child(param#1)
8. keep: token(rparen)
9. keep: token(space)
10. keep: child(body)
最后,将这种差异应用于为节点存储的NodeText:遍历NodeText,直到找到必须添加或删除新元素的位置。
在那里,我们将应用所需的更改。
在这种情况下,我们为 child 添加一个逗号,一个空格和一个占位符(第二个参数)。
应用差异并不容易,因为我们要忽略NodeText中存在的一些空白和注释。
总结
长期以来,JavaParser仅支持漂亮的打印,并且对于许多用途来说,它过去一直是一种合理的选择。
相反,某些用户希望在保留现有代码格式的同时进行修改:
保留确切的空格,将注释精确保留在原处。
此布局信息对于编译器可能毫无意义,但对开发人员而言很重要。
对于这些用法,我们创建了词法保留,这是在3.1.1版中首次引入的。
这两种选择应该可以满足您的所有需求。 如果不是,请与我们联系或在GitHub上创建新期刊。
参考资料
官方语法书