静态代码分析工具 Spoon 使用
官网: https://spoon.gforge.inria.fr/launcher.html
它是基于 Eclipse JDT 打造的静态代码分析工具. 它把原代码拆解成包,模块,类,方法,语句,表达式,各种语法单元, 各个语法单元又形成了父子包含等关系. 可以对原代码编译, 检查, 分析, 过滤, 替换, 转换 等.
AST 语法树的元素
语法树包含的各类语法基本单元: https://spoon.gforge.inria.fr/structural_elements.html
详细的代码块形成的各个基本单元: https://spoon.gforge.inria.fr/code_elements.html
setup project
可以分析一个基本的 Java 项目, 一个 Maven 项目, 或者一个 Jar包(通过反编译).
# 分析一个 Maven project 的 source code
MavenLauncher launcher = new MavenLauncher("/Users/tianxiaohui/codes/myProj", SOURCE_TYPE.APP_SOURCE,"/Users/tianxiaohui/apache-maven-3.9.1/");
launcher.getEnvironment().setComplianceLevel(17);
launcher.getEnvironment().setNoClasspath(true); //有些类没提供, 比如 servlet jar 里面的类
launcher.buildModel();
CtModel model = launcher.getModel();
3种情况:
- 有源代码 reference.getDeclaration() = reference.getTypeDeclaration()
- 没有源代码, 只有binary(jar). reference.getDeclaration() = null, reference.getTypeDeclaration() 反射得来, isShadow = true.
- 没有源代码, 也没有binary. reference.getDeclaration() = reference.getTypeDeclaration() = false.
上面的 getTypeDeclaration 适用于 getFieldDeclaration, getExecutableDeclaration.
常见的代码分析
返回原代码中所有的包和类
for(CtPackage p : model.getAllPackages()) {
System.out.println("package: " + p.getQualifiedName());
}
// list all classes of the model
for(CtType<?> s : model.getAllTypes()) {
System.out.println("class: " + s.getQualifiedName());
}
找到一个方法的定义
当你知道一个方法名的时候, 你要查看这个类具体的定义, 可以通过下面的查找方法.
public static void findMethodDefinition(CtModel ctModel, String clazzName, String methodName) {
CtClass<?> foundClass = ctModel.getElements(new TypeFilter<CtClass<?>>(CtClass.class) {
@Override
public boolean matches(CtClass<?> clazz) {
return clazz.getQualifiedName().equals(clazzName);
}
}).stream().findFirst().orElse(null);
if (foundClass != null) {
//System.out.println("Found class definition: " + foundClass);
foundClass.getMethodsByName(methodName).forEach(m -> {
System.out.println("Found method definition: " + m.getSignature());
System.out.println(m.toString());
});
} else {
System.out.println("Class definition not found for: " + clazzName);
}
}
public static void findInvocationPoints(CtModel ctModel, String className, String methodName) {
System.out.println(" Method " + className + "." + methodName + " is called by:");
findInvocation(ctModel, className, methodName, 0, false);
}
查找一个类实例是在哪里构造的
有时候我们要查找某个类是在哪里被初始化的, 可以通过下面的代码获得.
public static void findNewClassConstruct(CtModel ctModel, String clazzName) {
long start = System.currentTimeMillis();
ctModel.getRootPackage().getElements(new TypeFilter<>(CtConstructorCall.class)).forEach(e -> {
if (e.getExecutable().getDeclaringType().getQualifiedName().equals(clazzName)) {
System.out.println(clazzName + "is created at: " + e.getPosition());
}
});
System.out.println("time0: " + (System.currentTimeMillis() - start));
}
public static void findNewClassConstruct1(CtModel ctModel, String clazzName) {
long start = System.currentTimeMillis();
ctModel.getRootPackage().getElements(new TypeFilter<>(CtConstructorCall.class)).forEach(e -> {
System.out.println(e.getType());
String type = e.getType().toString();
if (type.equals(clazzName)) {
System.out.println(clazzName + "is created at: " + e.getPosition());
}
});
System.out.println("time1: " + (System.currentTimeMillis() - start));
}
查找一个方法的调用点
一个方法被调用的时候, 它声明的类型可能是它本身的类型, 或者它实现的接口类型, 或者直接/非直接父类的类型. 为了查看完整的可能性, 要能要去每个父类, 实现的接口都去查看一遍. 下面的方法只是查看当前类型的直接调用.
/**
* here we only find the invocation with the exactly class name and method name, not the declared method in
* Interface and parent class.
* Sometimes you want to find all the invocation points of a method, include the declared method in Interface and parent class.
* ex:
* IHello hello = new Hello();
* hello.sayHello();
* In this case, the invocation point of sayHello() is in IHello, not in Hello.
* @param ctModel
* @param className
* @param methodName
* @param depth
* @param recursive
*/
private static void findInvocation(CtModel ctModel, String className, String methodName, int depth, boolean recursive) {
List<CtInvocation<?>> invocations = ctModel.getElements(new TypeFilter<CtInvocation<?>>(CtInvocation.class) {
@Override
public boolean matches(CtInvocation<?> element) {
return element.getExecutable().getSignature().toString().equals(methodName) && containsRefType(element.getReferencedTypes(), className);
}
});
for (CtInvocation<?> invocation : invocations) {
CtExecutable<?> caller = invocation.getParent(CtExecutable.class);
if (caller != null) {
for (int i = 0; i < depth; i++) {
System.out.print("\t");
}
System.out.print(" - " + caller.getParent(CtClass.class).getPackage() + "." + caller.getParent(CtClass.class).getSimpleName() + "." + caller.getSignature());
if (caller.getThrownTypes().size() > 0) {
System.out.print(" throws ");
System.out.print(caller.getThrownTypes().stream().map(t -> t.toString()).collect(Collectors.joining( ", ")));
}
System.out.println(" at line " + invocation.getPosition().getLine());
if (recursive) {
findInvocation(ctModel, caller.getParent(CtClass.class).getQualifiedName(), caller.getSignature(), 1 + depth, recursive);
}
}
}
}
找到所有抛出异常的代码
下面的代码找出所有抛出异常的代码点. 当然你可以根据异常的类型去过滤.
public static void findThrowStatements(CtModel ctModel) {
List<CtThrow> throwStatements = ctModel.getElements(new TypeFilter<>(CtThrow.class));
// Process each throw statement
for (CtThrow throwStatement : throwStatements) {
CtExecutable<?> executable = throwStatement.getParent(CtExecutable.class);
if (executable != null) {
System.out.println(executable.getParent(CtClass.class).getPackage() + " - " + executable.getParent(CtClass.class).getSimpleName() + "." + executable.getSimpleName()
+ " at line " + throwStatement.getPosition().getLine());
}
}
}
找到包含特定注解的类或方法
有时候你想找到特定注解的类, 比如有些注解定义了系统的所有API, 有些注解标注了系统将要废弃的API.
findWithAnnotation(ctModel, javax.ws.rs.ApplicationPath.class).forEach(clazz -> {
System.out.println(clazz.getAnnotation(javax.ws.rs.ApplicationPath.class).value());
});
public static List<CtClass> findWithAnnotation(CtModel ctModel, Class<? extends Annotation> annotationType) {
return ctModel.getRootPackage().getElements(new AnnotationFilter<>(annotationType));
列出某个函数调用的其它函数列表
给出特定函数, 我们可以列出当前函数使用了其他哪些函数
public static void findNextCalls(CtModel ctModel, String pkg, String clazz, String methodName) {
//find this method
ctModel.getElements(new TypeFilter<CtMethod<?>>(CtMethod.class) {
@Override
public boolean matches(CtMethod<?> method) {
if (!(method.getParent() instanceof CtClass)) {
return false;
}
CtClass cz = (CtClass) method.getParent();
String curClazz = cz.getSimpleName();
String curPkg = null != cz.getPackage() ? cz.getPackage().getQualifiedName() : "-";
if (method.getSimpleName().equals(methodName)) {
System.out.println(curPkg + " - " + curClazz + " - " + method.getSimpleName());
}
return pkg.equals(curPkg) && clazz.equals(curClazz) && method.getSimpleName().equals(methodName);
}
}).forEach(m -> {
System.out.println("Method found: " + m.getSimpleName());
//find next calls
List<CtInvocation<?>> invocations = m.getElements(new TypeFilter<>(CtInvocation.class));
for (CtInvocation<?> invocation : invocations) {
// Check if the invocation is a client call
CtExecutableReference exec = invocation.getExecutable();
System.out.println(m.getSimpleName() + " -> " + invocation.getExecutable().getDeclaringType() + "."
+ invocation.getExecutable().getSignature());
}
});
}