如何在Clojure中为Java构建符号求解器

上周,我们已经看到了如何从源文件和JAR文件构建Java代码模型 。 现在,我们希望能够求解符号:实际上,我们希望将源代码中的符号引用链接到Java源文件或jar文件中的符号声明。

符号参考和符号声明

首先,我们必须考虑相同的符号可能指向不同上下文中的不同声明。 考虑这段荒谬的代码:

package me.tomassetti;
 
public class Example {
 
    private int a;
    
    void foo(long a){
        a += 1;
        for (int i=0; i<10; i++){
            new Object(){
                char a;
                
                void bar(long l){
                    long a = 0;
                    a += 2;
                }
            }.bar(a);
            
        }
    }
    
}

它包含几个符号a的声明:

  • 在第5行将其声明为int类型的字段
  • 在第7行将其声明为long类型的参数
  • 在第11行,它被声明为匿名类的字段,并且类型为char
  • 在第14行,它被声明为long类型的局部变量

我们还有两个对符号a的引用:

  • 在第8行,当我们将a递增1
  • 在第15行,当我们将a加2时

现在要解决符号问题,我们可以找出哪些符号引用引用了哪些符号声明。 这样我们就可以做一些事情,例如了解符号的类型,以及可以对符号执行哪些操作。

范围规则

解决符号的原理很简单(但是实现起来可能会很棘手): 给定符号引用后,我们将寻找最接近的对应声明。 直到找到一个参考点,我们才继续远离参考点,在AST中上升。

在我们的示例中,我们将第15行的引用与第14行的声明进行匹配。我们还将在第8行的引用与第7行的声明进行匹配。简单吗?

请考虑另一个示例:

package me.tomassetti;
 
class A {
    public void foo(){
        System.out.println("I am the external A");
    }    
}
 
public class Example {
    
    A a;
 
    class A {
        public void foo(){
            System.out.println("I am the internal A");
        }
    }
 
    void foo(A a){
        final int A = 10;
        new A().foo();
    }
 
}

现在,我们对A有不同的定义,在某些情况下为类(第3和13行),在其他情况下为变量(第20行)。 第21行的引用( new A()。foo(); )与第13行的声明匹配,而不与第20行的声明匹配,因为我们只能使用类型声明来匹配新对象实例化语句中的符号引用。 事情开始变得不那么容易了……

还有其他要考虑的事项:

  • 导入语句:导入com.foo.A可以使用com.foo包中的类A的声明来解析对A引用
  • 同一包中的类可以用简单名称来引用,而对于其他类,我们必须使用完全限定名称
  • 默认包中的类不能在默认包之外引用
  • 应该考虑继承的字段和方法(对于方法,我们必须考虑重载和重写,而不会混淆它们)
  • 在匹配方法时,我们必须考虑参数的兼容性(一点都不容易)
  • 等等等等

尽管原理很简单,但仍有大量规则和例外以及要正确解析符号的注意事项。 令人高兴的是:我们可以轻松地开始,然后在继续进行时改进解决方案。

设计我们的符号求解器

注意事项:在这篇文章中,我将解释我目前用于为有效 java构建符号求解器的方法。 我并不是说这是一个完美的解决方案,并且我相信我会随着时间的推移改进该解决方案。 但是,这种方法似乎涵盖了很多情况,我认为这不是一个糟糕的解决方案。

一个简单的参考到一个名称(不管它表示变量,参数,字段,等等)由NameExpr在所产生的AST表示JavaParser类 。 我们首先创建一个带有NameExpr节点的函数,然后返回相应的声明(如果可以找到的话)。 函数solveNameExpr基本上只调用传递三个参数的SolveSymbol函数:

  1. AST节点本身:它表示解决符号的范围
  2. 无:这表示额外的上下文信息,在这种情况下,不需要
  3. 要解决的名称:嗯,应该不言自明
(defn solveNameExpr
  "given an instance of com.github.javaparser.ast.expr.NameExpr returns the declaration it refers to,
   if it can be found, nil otherwise"
  [nameExpr]
  (let [name (.getName nameExpr)]
    (solveSymbol nameExpr nil name)))

我们首先为作用域的概念声明一个协议(某种程度上类似于Java接口):

(defprotocol Scope
  ; for example in a BlockStmt containing statements [a b c d e], when solving symbols in the context of c
  ; it will contains only statements preceeding it [a b]
  (solveSymbol [this context nameToSolve])
  ; solveClass solve on a subset of the elements of solveSymbol
  (solveClass [this context nameToSolve]))

基本思想是,在每个作用域中,我们尝试查找与要解决的符号相对应的声明,如果找不到,我们将其委托给父作用域。 我们为AST节点( com.github.javaparser.ast.Node )指定一个默认实现,该默认实现仅委托给父节点。 对于某些节点类型,我们提供了一个特定的实现。

(extend-protocol Scope
  com.github.javaparser.ast.Node
  (solveSymbol [this context nameToSolve]
    (solveSymbol (.getParentNode this) this nameToSolve))
  (solveClass [this context nameToSolve]
    (solveClass (.getParentNode this) this nameToSolve)))

对于BlockStmt,我们在检查变量声明的指令之前查看指令。 当前的检查语句将作为上下文传递。 如果您对此功能使用的功能感兴趣,只需查看一下有效的java代码:它位于GitHub :)

(extend-protocol Scope
  BlockStmt
  (solveSymbol [this context nameToSolve]
    (let [elementsToConsider (if (nil? context) (.getStmts this) (preceedingChildren (.getStmts this) context))
          decls (map (partial declare-symbol? nameToSolve) elementsToConsider)]
      (or (first decls) (solveSymbol (.getParentNode this) this nameToSolve)))))

对于MethodDeclaration,我们在参数中查找

(defn solve-among-parameters [method nameToSolve]
  (let [parameters (.getParameters method)
        matchingParameters (filter (fn [p] (= nameToSolve (.getName (.getId p)))) parameters)]
    (first matchingParameters)))

(extend-protocol Scope
  com.github.javaparser.ast.body.MethodDeclaration
  (solveSymbol [this context nameToSolve]
    (or (solve-among-parameters this nameToSolve)
      (solveSymbol (.getParentNode this) nil nameToSolve)))
  (solveClass [this context nameToSolve]
    (solveClass (.getParentNode this) nil nameToSolve)))

对于ClassOrInterfaceDeclaration,我们在类或接口的字段中查找(类可能具有静态字段)。

(defn solveAmongVariableDeclarator
  [nameToSolve variableDeclarator]
  (let [id (.getId variableDeclarator)]
    (when (= nameToSolve (.getName id))
      id)))

(defn- solveAmongFieldDeclaration
  "Consider one single com.github.javaparser.ast.body.FieldDeclaration, which corresponds to possibly multiple fields"
  [fieldDeclaration nameToSolve]
  (let [variables (.getVariables fieldDeclaration)
        solvedSymbols (map (partial solveAmongVariableDeclarator nameToSolve) variables)
        solvedSymbols' (remove nil? solvedSymbols)]
    (first solvedSymbols')))

(defn- solveAmongDeclaredFields [this nameToSolve]
  (let [members (.getMembers this)
        declaredFields (filter (partial instance? com.github.javaparser.ast.body.FieldDeclaration) members)
        solvedSymbols (map (fn  (solveAmongFieldDeclaration c nameToSolve)) declaredFields)
        solvedSymbols' (remove nil? solvedSymbols)]
    (first solvedSymbols')))

(extend-protocol Scope
  com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
  (solveSymbol [this context nameToSolve]
    (let [amongDeclaredFields (solveAmongDeclaredFields this nameToSolve)]
      (if (and (nil? amongDeclaredFields) (not (.isInterface this)) (not (empty? (.getExtends this))))
        (let [superclass (first (.getExtends this))
              superclassName (.getName superclass)
              superclassDecl (solveClass this this superclassName)]
          (if (nil? superclassDecl)
            (throw (RuntimeException. (str "Superclass not solved: " superclassName)))
            (let [inheritedFields (allFields superclassDecl)
                  solvedSymbols'' (filter (fn [f] (= nameToSolve (fieldName f))) inheritedFields)]
              (first solvedSymbols''))))
        amongDeclaredFields)))
  (solveClass [this context nameToSolve]
    (solveClass (.getParentNode this) nil nameToSolve)))

对于CompilationUnit,我们在同一包中查找其他类(使用它们的简单名称或限定名称),我们考虑import语句,并查找文件中声明的类型。

(defn qNameToSimpleName [qualifiedName]
  (last (clojure.string/split qualifiedName #"\.")))

(defn importQName [importDecl]
  (str (.getName importDecl)))

(defn isImportMatchingSimpleName? [simpleName importDecl]
  (= simpleName (qNameToSimpleName (importQName importDecl))))

(defn solveImportedClass 
  "Try to solve the classname by looking among the imported classes"
  [cu nameToSolve]
  (let [imports (.getImports cu)
        relevantImports (filter (partial isImportMatchingSimpleName? nameToSolve) imports)
        importNames (map (fn [i] (.getName (.getName i))) imports)
        correspondingClasses (map typeSolver importNames)]
    (first correspondingClasses)))

(extend-protocol Scope
  com.github.javaparser.ast.CompilationUnit
  (solveClass [this context nameToSolve]
    (let [typesInCu (topLevelTypes this)
          ; match types in cu using their simple name
          compatibleTypes (filter (fn [t] (= nameToSolve (getName t))) typesInCu)
          ; match types in cu using their qualified name
          compatibleTypes' (filter (fn [t] (= nameToSolve (getQName t))) typesInCu)]
      (or (first compatibleTypes)
          (first compatibleTypes')
          (solveImportedClass this nameToSolve)
          (solveClassInPackage (getClassPackage this) nameToSolve)
          ; we solve in nil context: it means look for absolute names
          (solveClass nil nil nameToSolve)))))

如果我们无法在编译单元的范围内解决符号,则没有父对象可以委托,因此我们使用nil范围表示缺少范围。 在这种情况下,只能解析绝对名称,例如类的规范名称。 我们使用typeSolver函数来实现。 typeSolver需要知道要使用哪个类路径,并且基本上可以在源文件目录和JAR中查找类。 同样在这种情况下,请随意研究有效的Java代码。

(extend-protocol Scope
  nil
  (solveClass [this context nameToSolve] (typeSolver nameToSolve)))

结论

我认为Clojure非常适合逐步构建解决方案:随着我们的前进,我们可以实现协议的不同方法。 建立简单的测试并一次将其改进。

我们已经迭代地构建了这个简单的解决方案,直到现在,它对于我们的目标仍然可以正常工作。 我们将继续编写更多的测试用例,我们可能不得不重构一两个东西,但我认为我们正在朝着正确的总体方向发展。

将来将这个符号解析器提取到一个单独的库中很有用,该库将与JavaParser一起使用以执行静态分析和重构。

翻译自: https://www.javacodegeeks.com/2015/08/how-to-build-a-symbol-solver-for-java-in-clojure.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值