Tuesday, September 18, 2012

To paren or not to paren

Groovy's policy on parenthesis can leave you scratching your head when using Gradle, if you're not careful.  In Groovy, parentheses are optional when the method being invoked has at least one parameter.  Also, a closure can be specified outside any parentheses, provided it's the final argument.

In more concrete terms, given this Gradle config file:

 def myFunc (val, Closure c) { println val; c.call() }  
   
 task hello << {  
      myFunc( "1", { println "All parameters in parentheses" } )  
      myFunc "2", { println "No parameters parentheses" }  
      myFunc( "3" ) { println "Only the closure outside parentheses" }  
 }  

All three of these lines are equivalent.  In each case, the first parameter is passed to myFunc and printed, and the closure is passed and called.

But what if we decide to leave the parentheses out entirely?

 task hello << {  
      myFunc( "1", { println "All parameters in parentheses" } )  
      myFunc "2", { println "No parameters parentheses" }  
      myFunc( "3" ) { println "Only the closure outside parentheses" }  
      myFunc "4" { println "No parenthesis at all" }  
 }  

Gradle throws an error:

 > Could not find method 4() for arguments [build_1cuekq1blf8f476ss7e8ms7254$_run_closure1_closure7@1f52125]  

This is because Groovy actually parses this line as if it looked like this:

 myFunc ("4" ({ println "No parenthesis at all" }) )  

In other words, Groovy thinks you're trying to chain calls: passing the closure to the string "4", and then passing the result of that operation to myFunc().  And it rightly objects.  You might be wondering why anyone would do this, and probably no one would.  But when using Gradle, you can accidentally fall into this trap.

Take, for example, configuring a JAR file using the Java plugin.  Let's say we want to copy all our runtime dependency JARs into a directory in our artifact's internal "/runtime" directory.  (Nevermind why we want to do this, it's just an example!)

 apply plugin: 'java'  
   
 dependencies {  
      runtime (  
           // specify runtime dependencies here  
      )  
 }  
   
 jar {  
      from ( configurations.runtime ) { into "/runtime" }  
 }  

This works as expected.  You end up with a normally constructed JAR, with a special runtime directory that contains all runtime dependencies.

But what if you neglect the parentheses?

 jar {  
      from configurations.runtime { into "/runtime" }  
 }  

Now, rather than copying only the runtime JARs into the runtime directory, it copies all the JAR's files into that directory.  In effect, it's the same as specifying this:

 jar {  
      into "/runtime"  
 }  

This is because the ConfigurationContainer (which wraps the "runtime" Configuration), accepts and executes closures.  So Gradle doesn't give you any error as it did with the "4" example above, and you're left very confused as to why your JAR's directory structures are wrong.

The bottom line is: when in doubt, keep the parentheses there.