静态代码分析工具 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种情况:

  1. 有源代码 reference.getDeclaration() = reference.getTypeDeclaration()
  2. 没有源代码, 只有binary(jar). reference.getDeclaration() = null, reference.getTypeDeclaration() 反射得来, isShadow = true.
  3. 没有源代码, 也没有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());
            }
        });
    }

标签: none

添加新评论