9

This isn't a question, but rather a cautionary tale: I tried to save some space and declared my variables in Jenkins Declarative pipeline like so:

int a, b, c

Then, I initialized them as:

a = b = c = 0

In my code, I use these integers as counters in a for-loop. My script kept failing over and over, some of the exceptions thrown:

java.lang.NullPointerException: Cannot invoke method next() on null object

and I knew for sure that my list is valid since it was hard-coded. So, I started wondering what's going on with these counters and when I called getClass() on them, Jenkins happily told me that they weren't integers, but rather

org.codehaus.groovy.runtime.NullObject

After changing code to

int a = 0
int b = 0
int c = 0

everything worked like a charm. Just wanted to share this. Maybe it'll help someone to save some frustration.

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131
Sparkle
  • 681
  • 5
  • 10

1 Answers1

12

Jenkins pipelines execute Groovy code in the continuation-passing style using groovy-cps interpreter. This is not vanilla Groovy you can execute directly in the IDE or in Groovy Shell.

Groovy CPS transforms your code to support the continuation-passing style and the correct Groovy expression like:

a = b = c = 0

gets transformed to something that looks more like:

eval(
  var("a"), 
  assign(
    eval(
      var("b"), 
      assign(
        eval(
          var("c"), 
          assign(0)
        )
      )
    )
  )
)

The problem with this expression in the CPS interpreter is that the assignment does not return any value, and thus the null value gets assigned to the variable b, and the same thing happens to the variable a.

If you want to dig deeper in the CPS invocations block, you can clone groovy-cps project and write a simple test case in the com.cloudbees.groovy.cps.CpsTransformerTest class.

@Test
void testMultiVariablesInlineCPS() {
    def cps = parseCps('''
int a, b, c
a = b = c = 0
''')
    println cps
}

Then you can put a breakpoint at the println cps and run the debugger. When you open the inspection window, you will see the picture similar to this one:

enter image description here

As a side note, keep in mind that the Groovy compiler also transforms your single line assignments when compiled the code to the bytecode. If you compile a simple Groovy script like:

int a, b, c
a = b = c = 0

println "$a $b $c"

and then you open its class file in the IDE to decompile the bytecode to the Java equivalent, you will see something like this:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.GStringImpl;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.callsite.CallSite;

public class test extends Script {
    public test() {
        CallSite[] var1 = $getCallSiteArray();
    }

    public test(Binding context) {
        CallSite[] var2 = $getCallSiteArray();
        super(context);
    }

    public static void main(String... args) {
        CallSite[] var1 = $getCallSiteArray();
        var1[0].call(InvokerHelper.class, test.class, args);
    }

    public Object run() {
        CallSite[] var1 = $getCallSiteArray();
        int a = 0;
        int b = 0;
        int c = 0;
        byte var5 = 0;
        return var1[1].callCurrent(this, new GStringImpl(new Object[]{Integer.valueOf(var5), Integer.valueOf(var5), Integer.valueOf(var5)}, new String[]{"", " ", " ", ""}));
    }
}
Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131