Search code examples
groovyabstract-syntax-tree

Groovy AstBuilder use argumentList for method params


I want to add the following if-statement at the beginning of a method:

@Grant( [ Permission.admin ] )
def someMethod( someArg ){
  if( GrantUtil.checkAuthorization( someArg, Permission.admin ){ // 2nd param comes from annotation
    // nothing
  }else{
    return null
  }

  // the rest
}

For that I'm using a custom ASTTransformation:

class GrantASTTransformation implements ASTTransformation {
  
  @Override
  void visit( ASTNode[] nodes, SourceUnit source ) {
    if( !nodes ) return
    
    MethodNode methodNode = (MethodNode) nodes.find{ it in MethodNode }
    
    if( methodNode ) augmentMethod methodNode
  }

  void augmentMethod( MethodNode methodNode ) {       
    List<AnnotationNode> annos = methodNode.getAnnotations new ClassNode( Grant )
    Expression rolesValue = annos.first().getMember 'value'
    
    Expression param = new VariableExpression( methodNode.parameters[ 0 ] )

    Expression args = new ArgumentListExpression( [ param, rolesValue ] )
    StaticMethodCallExpression callExp = new StaticMethodCallExpression( new ClassNode( GrantUtil ), 'checkAuthorization', args )
    BooleanExpression checkExpression = new BooleanExpression( callExp )

    IfStatement ifStmt = new IfStatement( checkExpression, new EmptyStatement(), new ReturnStatement( ConstantExpression.NULL ) )
  
    ((BlockStatement)methodNode.code).statements.add 0, ifStmt
  }
}

and it works like charm.

Now I want to rewrite this method using a more transparent AstBuilder and so far I came out with:

void augmentMethod( MethodNode methodNode ) {       
  List<AnnotationNode> annos = methodNode.getAnnotations new ClassNode( Grant )
  Expression rolesValue = annos.first().getMember 'value'
  
  IfStatement ifStmt = (IfStatement)new AstBuilder().buildFromSpec{
      ifStatement{
        booleanExpression{
          staticMethodCall( GrantUtil, 'checkAuthorization' ){
            argumentList{ 
              // this doesn't work
              param
              rolesValue
            }
          }
        }
        //if block
        empty()
        //else block
        returnStatement{ constant null }
      }
  }.first()
    
  ((BlockStatement)methodNode.code).statements.add 0, ifStmt
}

but it fails filling up the argumentList, throwing

groovy.lang.MissingMethodException: No signature of method: static GrantUtil.checkAuthorization() is applicable for argument types: () values: []
Possible solutions: checkAuthorization(io.vertx.ext.web.RoutingContext, java.util.List)

I looked through the test but could not find a way to introduce the "external" params to the argumentList.

What is the proper way of doing this? Is this supported by the AstBuilder?

UPDATE:

I tried the suggestion of @cfrick and it worked to 50%:

staticMethodCall(GrantUtil, 'checkAuthorization') {
  argumentList {
    variable methodNode.parameters[0].name // works like charm!
    constant( [ Permission.user ] as Permission[] )
  }
}

Now upon execution I'm getting the exception:

org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
General error during instruction selection: Cannot generate bytecode for constant: [Lio.lootwalk.domain.Permission;@1e7f19b4 of type: [Lio.lootwalk.domain.Permission;

So, passing an array of Enumeration object as a constant in argumentList fails...


Solution

  • You can improve your original solution using the static methods in Groovy's GeneralUtils class:

    class GrantASTTransformation implements ASTTransformation {
        @Override
        void visit(ASTNode[] nodes, SourceUnit source ) {
            nodes?.find{ it in MethodNode }?.with{ augmentMethod it }
        }
    
        void augmentMethod( MethodNode mn ) {
            def annos = mn.getAnnotations new ClassNode( Grant )
            def rolesValue = annos.first().getMember 'value'
            def param = varX( mn.parameters[ 0 ] )
    
            def args = args( param, rolesValue )
            def callCheck = callX( new ClassNode( GrantUtil ), 'checkAuthorization', args )
            def notAuthorized = notX( boolX( callCheck ) )
            def returnEarly = returnS( nullX() )
    
            def guard = ifS( notAuthorized, returnEarly )
            mn.code.statements.add 0, guard
        }
    }
    

    With regards to AstBuilder, it is now less favoured than its replacement, macros. One way to use macros to write the augmentMethod is:

    void augmentMethod( MethodNode mn ) {
        def grantNode = ClassHelper.make(Grant)
        def grantUtilNode = ClassHelper.make(GrantUtil)
        def annos = mn.getAnnotations grantNode
        def rolesValue = annos.first().getMember 'value'
        def param = varX( mn.parameters[ 0 ] )
        def guard = macro {
            if (!$v{ classX(grantUtilNode) }.checkAuthorization( $v{ param }, $v{ rolesValue } )) {
                return
            }
        }
        mn.code.statements.add 0, guard
    }
    

    You just have to be careful with referencing classes within that code which is why GrantUtil doesn't appear explicitly.

    To stick with AstBuilder you can use:

    void augmentMethod( MethodNode mn ) {
        def annos = mn.getAnnotations new ClassNode( Grant )
        def rolesValue = annos.first().getMember('value').expressions[0]
        def param = varX( mn.parameters[ 0 ] )
        def guard = new AstBuilder().buildFromSpec {
            ifStatement{
                booleanExpression{
                    staticMethodCall( GrantUtil, 'checkAuthorization' ) {
                        argumentList {
                            variable param.name
                            property {
                                classExpression rolesValue.objectExpression.type.typeClass
                                constant rolesValue.propertyAsString
                            }
                        }
                    }
                }
                empty()
                returnStatement{ constant null }
            }
        }.first()
        mn.code.statements.add 0, guard
    }
    

    There is probably a simplification possible but the above works.