【问题标题】:How do I get to the bottom of an AST Expression如何到达 AST 表达式的底部
【发布时间】:2016-04-24 16:55:13
【问题描述】:

我是 AST 新手(我第一次编写插件)。表达式在现实生活中可能非常复杂。例如,我想知道如何解决分配的左侧和右侧。

class Visitor extends ASTVisitor
{
    @Override
    public boolean visit(Assignment node)
    {
        //here, how do I get the final name to each each side of the assignment resolves?
    }
}

我还有一个疑问,如何获取用于调用方法的实例?

public boolean visit(MethodInvocation node)
{
    //how do I get to know the object used to invoke this method?
    //like, for example, MyClass is a class, and it has a field called myField
    //the type of myField has a method called myMethod.
    //how do I find myField? or for that matter some myLocalVariable used in the same way.
}

假设以下作业

SomeType.someStaticMethod(params).someInstanceMethod(moreParams).someField =
     [another expression with arbitrary complexity]

如何从Assigment 节点到达someField

另外,MethodInvocation 的什么属性为我提供了用于调用该方法的实例?

编辑 1: 鉴于我收到的答案,我的问题显然不清楚。我不想解决 this 特定的表达式。在给定任何分配的情况下,我希望能够找出分配给它的名称,以及分配给第一个的名称(如果不是右值)。

因此,例如,方法调用的参数可以是字段访问或先前声明的局部变量。

SomeType.someStaticMethod(instance.field).someInstanceMethod(type.staticField, localVariable, localField).Field.destinationField

所以,这是一个有希望的客观问题:给定任何左侧和右侧都具有任意复杂性的赋值语句,如何获得被分配到的最终字段/变量,以及最终(如果有)字段/分配给它的变量。

编辑 2: 更具体地说,我想要实现的是不变性,通过注释 @Const:

/**
* When Applied to a method, ensures the method doesn't change in any
* way the state of the object used to invoke it, i.e., all the fields
* of the object must remain the same, and no field may be returned,
* unless the field itself is marked as {@code @Const} or the field is
* a primitive non-array type. A method  annotated with {@code @Const} 
* can only invoke other {@code @Const} methods of its class, can only 
* use the class's fields to invoke {@code @Const} methods of the fields 
* classes and can only pass fields as parameters to methods that 
* annotate that formal parameter as {@code @Const}.
*
* When applied to a formal parameter, ensures the method will not
* modify the value referenced by the formal parameter. A formal   
* parameter annotated as {@code @Const} will not be aliased inside the
* body of the method. The method is not allowed to invoke another 
* method and pass the annotated parameter, save if the other method 
* also annotates the formal parameter as {@code @Const}. The method is 
* not allowed to use the parameter to invoke any of its type's methods,
* unless the method being invoked is also annotated as {@code @Const}
* 
* When applied to a field, ensures the field cannot be aliased and that
* no code can alter the state of that field, either from inside the   
* class that owns the field or from outside it. Any constructor in any
* derived class is allowed to set the value of the field and invoke any
* methods using it. As for methods, only those annotated as
* {@code @Const} may be invoked using the field. The field may only be
* passed as a parameter to a method if the method annotates the 
* corresponding formal parameter as {@code @Const}
* 
* When applied to a local variable, ensures neither the block where the
* variable is declared or any nested block will alter the value of that 
* local variable. The local variable may be defined only once, at any
* point where it is in scope and cannot be aliased. Only methods
* annotated as {@code @Const} may be invoked using this variable, and 
* the variable  may only be passed as a parameter to another method if 
* said method annotates its corresponding formal parameter as
* {@code @Const}
*
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD,
ElementType.LOCAL_VARIABLE})
@Inherited
public @interface Const
{

}

要实现这一点,我要做的第一件事就是将作业左侧标记为@Const(很简单)。我还必须检测和表达式的右侧何时是标记为@Const的字段,在这种情况下,它只能在相同类型的@Const变量的定义处赋值。

问题是我很难在表达式右侧找到最终字段,以避免为字段产生别名并使 @Const 注释无用。

【问题讨论】:

  • 你在这里使用什么库? (PMD 或类似的东西?)
  • eclipse jdt 用于插件开发。
  • 好的,那么你想到达形成的 AST 的叶子吗?
  • 我想访问作为赋值的左右解析表达式的字段(或局部变量或参数)的Name。所以我可以解决它并获取它的注释。
  • @FinnTheHuman 看看你最近发布的一些其他问题,我觉得你可能有兴趣看看 Annotation Processors 和/或 LombokProject。两者都旨在在解析 Java 源文件时对它们的 AST 执行自定义处理。注释处理器获得对 AST 树的只读访问权限(但仍然可以向其添加警告和错误),而 LombokProject 获得对 AST 树的读写访问权限。也许这可以帮助你。

标签: java eclipse abstract-syntax-tree eclipse-jdt


【解决方案1】:

首先引用我发布的答案:

您必须使用bindings。要有可用的绑定,这意味着resolveBinding() 不返回nullpossibly additional steps 我已经发布是必要的。

以下访问者应该可以帮助您做您想做的事:

class AssignmentVisitor extends ASTVisitor {

    public boolean visit(Assignment node) {
        ensureConstAnnotationNotViolated(node);
        return super.visit(node);
    }

    private void ensureConstAnnotationNotViolated(Assignment node) {
        Expression leftHandSide = node.getLeftHandSide();
        if (leftHandSide.getNodeType() == ASTNode.FIELD_ACCESS) {
            FieldAccess fieldAccess = (FieldAccess) leftHandSide;
            // access field IVariableBinding
            fieldAccess.resolveFieldBinding();
            // access IAnnotationBindings e.g. your @const
            fieldAccess.resolveFieldBinding().getAnnotations();
            // access field ITypeBinding
            fieldAccess.getExpression().resolveTypeBinding();
        } else {
            // TODO: check possible other cases
        }

    }
}

【讨论】:

    【解决方案2】:

    访问者是一个非常棒的工具,但对特定问题的正确解决方案并不总是让单个访问者耐心地等待其访问方法被调用...您提出的问题就是这种情况的一个示例。

    让我们重新表述一下你要做什么:

    1. 您要识别以识别每个分配(即leftSide = rightSide

    2. 对于每个赋值,你要确定左边的性质(即要么是局部变量,要么是字段访问),如果确实是字段访问,则要构建对应于该字段的“路径”(即源对象,后跟一系列方法调用或字段访问,并以字段访问结束)。

    3. 对于每个分配,您要确定与右侧对应的类似“路径”。

    我认为您已经解决了第 1 点:您只需创建一个扩展 org.eclipse.jdt.core.dom.ASTVisitor 的类;在那里,您覆盖 #visit(Assignment) 方法。最后,在合适的地方实例化你的访问者类,并让它访问一个 AST 树,从满足你需要的那个节点开始(很可能是CompilationUnitTypeDeclarationMethodDeclaration 的实例) .

    然后呢? #visit(Assignment) 方法确实接收到 Assignment 节点。直接在该对象上,您可以获得左侧和右侧表达式(assignment.getLeftHandSide()assignment.getRightHandSide())。正如您所提到的,两者都是Expressions,结果可能非常复杂,那么我们如何从这些子树中提取干净的线性“路径”?访问者当然是这样做的最佳方式,但这里有一个问题,应该使用 distinct 访问者来完成,而不是让您的第一个访问者(捕获 Assignments 的访问者)继续下降侧面表达。从技术上讲,使用单个访问者完成所有操作是可行的,但这将涉及该访问者内部的重要状态管理。无论如何,我非常相信这种管理的复杂性会如此之高,以至于这种实施实际上会比不同访问者方法的效率低。

    所以我们可以这样成像:

    class MyAssignmentListVisitor extends ASTVisitor {
        @Override
        public boolean visit(Assignment assignment) {
            FieldAccessLineralizationVisitor leftHandSideVisitor = new FieldAccessLineralizationVisitor();
            assignment.getLeftHandSide().accept(leftHandSideVisitor);
            LinearFieldAccess leftHandSidePath = leftHandSideVisitor.asLinearFieldAccess();
    
            FieldAccessLineralizationVisitor rightHandSideVisitor = new FieldAccessLineralizationVisitor();
            assignment.getRightHandSide().accept(rightHandSideVisitor);
            LinearFieldAccess rightHandSidePath = rightHandSideVisitor.asLinearFieldAccess();
    
            processAssigment(leftHandSidePath, rightHandSidePath);
    
            return true;
        }
    }
    
    class FieldAccessLineralizationVisitor extends ASTVisitor {
    
        List<?> significantFieldAccessParts = [...];
    
        // ... various visit method expecting concrete subtypes of Expression ...
    
        @Override
        public boolean visit(Assignment assignment) {
            // Found an assignment inside an assignment; ignore its
            // left hand side, as it does not affect the "path" for 
            // the assignment currently being investigated
    
            assignment.getRightHandSide().accept(this);
    
            return false;
        }
    }
    

    注意在这段代码中MyAssignmentListVisitor.visit(Assignment) 返回true,表示应该递归地检查赋值的子级。起初这听起来可能没有必要,Java 语言确实支持几种构造,其中一个赋值可能包含其他赋值;例如,考虑以下极端情况:

    (varA = someObject).someField = varB = (varC = new SomeClass(varD = "string").someField);
    

    出于同样的原因,在表达式的线性化过程中只访问赋值的右侧,因为赋值的“结果值”是它的右侧。在这种情况下,左侧只是一个副作用,可以放心地忽略。

    鉴于我不知道您的具体情况所需的信息的性质,我不会进一步对路径的实际建模方式进行原型设计。为左侧表达式和右侧表达式分别创建不同的访问者类也可能更合适,例如,为了更好地处理右侧实际上可能涉及多个变量/字段/方法调用组合的事实通过二元运算符。这将是你的决定。

    关于 AST 树的访问者遍历仍有一些主要问题需要讨论,即,通过依赖默认的节点遍历顺序,您失去了获取每个节点之间关系信息的机会。例如,给定表达式this.someMethod(this.fieldA).fieldB,您将看到类似于以下序列的内容:

    FieldAccess      => corresponding to the whole expression
    MethodInvovation => corresponding to this.someMethod(this.fieldA)
    ThisExpression
    SimpleName ("someMethod")
    FieldAccess      => corresponding to this.fieldA
    ThisExpression
    SimpleName ("fieldA")
    SimpleName ("fieldB")
    

    您根本无法从这一系列事件中实际推导出线性化表达式。相反,您将希望显式拦截每个节点,并仅在适当时以适当的顺序显式递归节点的子节点。例如,我们可以这样做:

        @Override
        public boolean visit(FieldAccess fieldAccess) {
            // FieldAccess :: <expression>.<name>
    
            // First descend on the "subject" of the field access
            fieldAccess.getExpression().accept(this);
    
            // Then append the name of the accessed field itself
            this.path.append(fieldAccess.getName().getIdentifier());
    
            return false;
        }
    
        @Override
        public boolean visit(MethodInvocation methodInvocation) {
            // MethodInvocation :: <expression>.<methodName><<typeArguments>>(arguments)
    
            // First descend on the "subject" of the method invocation
            methodInvocation.getExpression().accept(this);
    
            // Then append the name of the accessed field itself
            this.path.append(methodAccess.getName().getIdentifier() + "()");
    
            return false;
        }
    
        @Override
        public boolean visit(ThisExpression thisExpression) {
            // ThisExpression :: [<qualifier>.] this
    
            // I will ignore the qualifier part for now, it will be up
            // to you to determine if it is pertinent
            this.path.append("this");
    
            return false;
        }
    

    根据前面的示例,这些方法将在path 中收集以下序列:thissomeMethod()fieldB。我相信,这与您正在寻找的内容非常接近。如果您想收集所有字段访问/方法调用序列(例如,您希望您的访问者返回 this,someMethod(),fieldBthis,fieldA),那么您可以重写 visit(MethodInvocation) 方法,大致类似于:

        @Override
        public boolean visit(MethodInvocation methodInvocation) {
            // MethodInvocation :: <expression>.<methodName><<typeArguments>>(arguments)
    
            // First descend on the "subject" of the method invocation
            methodInvocation.getExpression().accept(this);
    
            // Then append the name of the accessed field itself
            this.path.append(methodAccess.getName().getIdentifier() + "()");
    
            // Now deal with method arguments, each within its own, distinct access chain
            for (Expression arg : methodInvocation.getArguments()) {
                LinearPath orginalPath = this.path;
                this.path = new LinearPath();
    
                arg.accept(this);
    
                this.collectedPaths.append(this.path);
                this.path = originalPath;
            }
    
            return false;
        }
    

    最后,如果您有兴趣了解路径中每一步的值类型,则必须查看与每个节点关联的绑定对象,例如:methodInvocation.resolveMethodBinding().getDeclaringClass()。但请注意,必须在构建 AST 树时明确请求绑定解析。

    上面的代码不能正确处理更多的语言结构;不过,我相信您应该能够自己解决这些剩余的问题。如果您需要查看参考实现,请查看org.eclipse.jdt.internal.core.dom.rewrite.ASTRewriteFlattener 类,它基本上是从现有的 AST 树重构 Java 源代码;虽然这个特定的访问者比大多数其他ASTVisitors 大得多,但它更容易理解。

    针对 OP 的编辑 #2 进行更新

    这是您最近一次编辑后的更新起点。还有很多情况需要处理,但更符合你的具体问题。另请注意,虽然我使用了许多 instanceof 检查(因为目前这对我来说更容易,因为我正在一个简单的文本编辑器中编写代码,并且没有 ASTNode 常量的代码完成),你可以选择而是在node.getNodeType() 上使用 switch 语句,这通常会更有效。

    class ConstCheckVisitor extends ASTVisitor {
    
        @Override
        public boolean visit(MethodInvocation methodInvocation) {    
            if (isConst(methodInvocation.getExpression())) {
                if (isConst(methodInvocation.resolveMethodBinding().getMethodDeclaration()))
                    reportInvokingNonConstMethodOnConstSubject(methodInvocation);
            }
    
            return true;
        }
    
        @Override
        public boolean visit(Assignment assignment) {
            if (isConst(assignment.getLeftHandSide())) {
                if ( /* assignment to @Const value is not acceptable in the current situation */ )
                    reportAssignmentToConst(assignment.getLeftHandSide());
    
                // FIXME: I assume here that aliasing a @Const value to
                //        another @Const value is acceptable. Is that right?
    
            } else if (isImplicitelyConst(assigment.getLeftHandSide())) {
                reportAssignmentToImplicitConst(assignment.getLeftHandSide());        
    
            } else if (isConst(assignment.getRightHandSide())) {
                reportAliasing(assignment.getRightHandSide());
            }
    
            return true;
        }
    
        private boolean isConst(Expression expression) {
            if (expression instanceof FieldAccess)
                return (isConst(((FieldAccess) expression).resolveFieldBinding()));
    
            if (expression instanceof SuperFieldAccess)
                return isConst(((SuperFieldAccess) expression).resolveFieldBinding());
    
            if (expression instanceof Name)
                return isConst(((Name) expression).resolveBinding());
    
            if (expression instanceof ArrayAccess)
                return isConst(((ArrayAccess) expression).getArray());
    
            if (expression instanceof Assignment)
                return isConst(((Assignment) expression).getRightHandSide());
    
            return false;
        }
    
        private boolean isImplicitConst(Expression expression) {
            // Check if field is actually accessed through a @Const chain
            if (expression instanceof FieldAccess)
                return isConst((FieldAccess expression).getExpression()) ||
                       isimplicitConst((FieldAccess expression).getExpression());
    
            // FIXME: Not sure about the effect of MethodInvocation, assuming
            //        that its subject is const or implicitly const
    
            return false;
        }
    
        private boolean isConst(IBinding binding) {
            if ((binding instanceof IVariableBinding) || (binding instanceof IMethodBinding))
                return containsConstAnnotation(binding.getAnnotations());
    
            return false;
        }
    }
    

    希望对您有所帮助。

    【讨论】:

    • 所有这些类都取决于我拥有一个 RewriteEventStore 实例。我不知道如何得到一个,我怀疑这不是要走的路。我会用更多细节更新我的问题。
    • 不应该有这样的要求...你究竟是如何将代码注入 JDT 的?
    • @FinnTheHuman 感谢您的更新,这确实有很大帮助。向 JDT 添加额外的编译警告或错误基本上是通过实现和注册 CompilationParticipant 来完成的。您可以在此处获得这样做的示例起点:stackoverflow.com/questions/24195142/…。在reconcile()中,可以使用reconcileContext.getAST8()获取当前文件的AST;然后,您通过访问者浏览它。使用reconcileContext.putProblems() 添加将出现在编辑器中的问题标记。
    • 我应该警告您,您尝试实施的合同非常复杂,无法正确有效地实施。我主要担心的是您的代码必须系统地导航源文件中的所有字段绑定,以确定该字段是否具有 @Const 注释。这可能会显着减慢编译过程。
    • 您的目标似乎与检查null 相关注释的目标非常相似;然而,据我所知,JDT 中该功能的所有正确实现(随着时间的推移有一些提议)依赖于直接在 JDT 的 Java 模型和绑定模型中添加新标志,以避免显着的开销系统地重新分析其他课程。这些额外的标志还使实施 Quick Fix 和与此类复杂合约相关的其他贡献变得更加容易。
    【解决方案3】:

    您可以进一步访问node.getLeftHandSide()。 我认为可以在 Sharpen(Java2C# 翻译)代码中找到一个很好的例子:

    https://github.com/mono/sharpen/blob/master/src/main/sharpen/core/CSharpBuilder.java#L2848

    这里有一个简单的示例项目: https://github.com/revaultch/jdt-sample

    Junit 测试在这里: https://github.com/revaultch/jdt-sample/blob/master/src/test/java/ch/revault/jdt/test/VisitorTest.java

    【讨论】:

    • 如果我这样做,我不会得到完整的表达吗?我想要子树的最后一片叶子。
    • 如果您进一步访问 LHS,您最终会找到 visit(FieldAccess)visit(QualifiedName)
    • 我该怎么做? Expression 没有访客。好的,但即使我设法访问字段访问,我怎么知道它是表达式的叶节点?
    • 你能不能写一个方法来找到赋值两边的两个子树上的叶子节点?
    • 我的例子是一个糟糕的例子,让我澄清一下我的真正意思:如果中间有一个 FieldAccess 怎么办,这意味着它不是表达式的叶节点。所以我的意思是:我如何找出表达式解析的最终名称?如何找到分配给它的名称和分配给它的右侧名称?如果它碰巧是一个右值,那么左侧可能没有最终名称。那么如何编写适用于所有这些情况的访问者呢?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-15
    • 1970-01-01
    • 2013-09-12
    • 2012-03-01
    • 1970-01-01
    • 2011-12-01
    相关资源
    最近更新 更多