I'm currently trying to implement some Groovy compile time AST transformations, but I ran into trouble:
How do I specify an AST transformation for an assignment statement to a field? i.e. the AST transformation should transform the following code:
class MyClass {
#MyTransformation
String myField
public void init() {
}
}
into something like
class MyClass {
String myField
public void init() {
this.myField = "initialized!"
}
}
I tried it with this AST builder invocation:
def ast = new AstBuilder().buildFromSpec {
expression{
declaration {
variable "myField"
token "="
constant "initialized!"
}
}
}
But after inserting the resulting statement in the "init" method of the declaring class, it instead inserted a variable assignment, as in
java.lang.Object myField = "initialized!"
I looked through the examples incorporated in the Ast Builder TestCase, but they only cover field declaration in the class body, not assignments to fields. My own tries using fieldNode all resulted in compiler errors. I set the compile phase to INSTRUCTION_SELECTION; I think this should be fine.
How do I achieve this? A solution based on the AstBuilder#buildFromSpec method is preferred, but any help would be highly appreciated.
I usually recommand not to use the AST builder. It's good for prototyping, but you don't really control what it generates. In particular, here, it's not capable of handling the fact that the variable expression you create should reference the field node. AST Builder is very nice to learn about the AST, but shouldn't be used in production code IMHO.
Here is a self contained example that demonstrates how you can acheive what you want. The code inside #ASTTest would correspond to your transform code:
import groovy.transform.ASTTest
import org.codehaus.groovy.ast.expr.BinaryExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.syntax.Token
import org.codehaus.groovy.syntax.Types
class MyClass {
String myField
#ASTTest(phase=SEMANTIC_ANALYSIS,value={
def classNode = node.declaringClass
def field = classNode.getDeclaredField('myField')
def assignment = new BinaryExpression(
new VariableExpression(field),
Token.newSymbol(Types.EQUAL, 0, 0),
new ConstantExpression('initialized!')
)
node.code.addStatement(new ExpressionStatement(assignment))
})
public void init() {
}
}
def c = new MyClass()
c.init()
println c.myField
Hope this helps!
Related
I am new to groovy and I am trying with some groovy code. I have this scenario. I have the following modules
package com.utils
abstract class Base {
static String data = ''
}
package com.utils
class A extends Base {
static String data = 'dummy'
}
package com.utils
class B extends Base {
static String data = 'dummy'
}
package com.utils
class ShapeFactory {
static Map <String,Object> shapes = [
"a": A,
"b": B
]
static Object get_shapes(String shape) {
return shapes.get(shape);
}
}
And in the main file I am using
and in the main file and here is where it fails with weird error. I couldn't identify the reason, I would appreciate any help.
import com.utils.ShapeFactory
def shapeA = ShapeFactory.get_shapes('a')
shapeA.data // here it fails with the below error
hudson.remoting.ProxyException: org.codehaus.groovy.runtime.typehandling.GroovyCastException:
Cannot cast object '[]' with class 'java.util.ArrayList' to class 'java.util.Map'
due to: groovy.lang.GroovyRuntimeException:
Could not find matching constructor for: java.util.Map()
Any help will be appreciated, Thanks
Apologies, I found the issue, In the Base class I had a Map variable as well which was initialized as an array
abstract class Base {
static String data = ''
static Map mapper = [] // This has to be [:]
}
I'm going through this example but something about it is very confusing to me: https://www.testcookbook.com/book/groovy/jenkins/intro-testing-job-dsl.html
In this test, how/what is executing getJobFiles()? I don't see it being called anywhere. Is there some magic with jobFiles? Is specifying jobFiles somehow calling getJobFiles?
import javaposse.jobdsl.dsl.DslScriptLoader
import javaposse.jobdsl.plugin.JenkinsJobManagement
import org.junit.ClassRule
import org.jvnet.hudson.test.JenkinsRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class JobScriptsSpec extends Specification {
#Shared
#ClassRule
JenkinsRule jenkinsRule = new JenkinsRule()
#Unroll
def 'test script #file.name'(File file) {
given:
def jobManagement = new JenkinsJobManagement(System.out, [:], new File('.'))
when:
new DslScriptLoader(jobManagement).runScript(file.text)
then:
noExceptionThrown()
where:
file << jobFiles
}
static List<File> getJobFiles() {
List<File> files = []
new File('jobs').eachFileRecurse {
if (it.name.endsWith('.groovy')) {
files << it
}
}
files
}
}
Edit
It seems like jobFiles does call getJobFiles() but I don't understand how. Is this a groovy or spock feature? I've been trying to research this but can finding anything explaining this in detail.
This is standard Groovy functionality. You can abbreviate any getter call like
def file = new File("sarek-test-parent/sarek-test-common/src/main/java")
println file.name // getName()
println file.parent // getParent()
println file.absolutePath // getAbsolutePath()
println file.directory // isDirectory()
java
sarek-test-parent\sarek-test-common\src\main
C:\Users\alexa\Documents\java-src\Sarek\sarek-test-parent\sarek-test-common\src\main\java
true
The same works for setters:
new Person().name = "John" // setName("John")
new Person().zipCode = "12345" // setZipCode("12345")
Actually the second link provided by jaco0646 explains it, just his mixing up this simple fact with data providers clouds the explanation.
Edit
When Groovy determines that jobFiles does not refer to any existing variable, it considers the name as a Groovy property, which then allows it to take advantage of the shortcut notation for accessing properties.
I have two immutable groovy classes that have a few shared values that I'm trying to abstract to a parent class. However when I create the following, the second test case always fails. Although everything compiles correctly and no error is thrown at runtime, when I assign the parent property int he constructor, it is never set, resulting in a null value. I havent found any documentation that forbids this, but I'm wondering is this even possible? I've tried a number of configuration of Annotations and class-types (e.g. removing abstract from the parent) but nothing seems to work short of just removing the #Immutable tag altogether.
abstract class TestParent {
String parentProperty1
}
#ToString(includeNames = true)
#Immutable
class TestChild extends TestParent {
String childProperty1
String childProperty2
}
class TestCase {
#Test
void TestOne() {
TestChild testChild = new TestChild(
childProperty1: "childOne",
childProperty2: "childTwo",
parentProperty1: "parentOne"
)
assert testChild
assert testChild.parentProperty1
}
}
Based on the code for the ImmutableASTTransformation, the Map-arg constructor added by the createConstructorMapCommon method does not include a call to super(args) in the method body.
which means that immutable classes are self contained by default
Now if you want to do it you need to use composition instead of inheritance and this is an example of how you can do it :
import groovy.transform.*
#TupleConstructor
class A {
String a
}
#Immutable(knownImmutableClasses=[A])
class B {
#Delegate A base
String b
}
def b = new B(base: new A("a"), b: "b")
assert b.a
i hope this will help :)
I am new to groovy and just started exploring its metaprogramming capabilities. I got stuck with adding missing properties on a bean constructor call.
In a class to be used with FactoryBuilderSupport, I want to dynamically add those properties that are not yet defined and provided during the constructor call. Here is stripped-down version:
#Canonical
class MyClass {
def startDate
def additionalProperties = [:]
def void propertyMissing(String name, value) {
additionalProperties[name] = value
}
}
However, If I construct the class with unknown properties, the proprty is not added but I get a MissingPropertyException instead:
def thing = new MyClass(startDate: DateTime.now(), duration: 1234)
The property duration does not exist, and I expected it to be handled via propertyMissing. As far as I understand groovy, calling the tuple-constructor results in a no-argument constructor call followed by calls to the groovy-generated setters. So why do I get a MissingPropertyException?
As I am new to groovy, I am probably missing some basic AST or MOP rules. I would highly appreciate your help.
If you use #Canonical and you define the first class object with def like you are doing with startDate the annotation generates the following constructors:
#Canonical
class MyClass {
def startDate
def additionalProperties = [:]
def void propertyMissing(String name, value) {
additionalProperties[name] = value
}
}
// use reflection to see the constructors
MyClass.class.getConstructors()
Generated constructors:
public MyClass()
public MyClass(java.lang.Object)
public MyClass(java.util.LinkedHashMap)
public MyClass(java.lang.Object,java.lang.Object)
In the #Canonical documentation you can see the follow limitation:
Groovy's normal map-style naming conventions will not be available if the first property has type LinkedHashMap or if there is a single Map, AbstractMap or HashMap property
Due to public MyClass(java.util.LinkedHashMap) is generated you can't use tuple-constructor and you get MissingPropertyException.
Surprisingly if you define your first object (note that I say the first) with a type instead of using def, #Canonical annotation doesn't add the public MyClass(java.util.LinkedHashMap) and then your tuple-constructor call works, see the following code:
#Canonical
class MyClass {
java.util.Date startDate
def additionalProperties = [:]
def void propertyMissing(String name, value) {
additionalProperties[name] = value
}
}
// get the constructors
MyClass.class.getConstructors()
// now your code works
def thing = new MyClass(startDate: new java.util.Date(), duration: 1234)
Now the created constructors are:
public MyClass()
public MyClass(java.util.Date)
public MyClass(java.util.Date,java.lang.Object)
So since there isn't the public MyClass(java.util.LinkedHashMap) the limitation doesn't apply and you tuple-constructor call works.
In addition I want to say that since this solution works I can't argue why... I read the #Canonical documentation again and again and I don't see the part where this behavior is described, so I don't know why works this way, also I make some tries and I'm a bit confusing, only when the first element is def the public MyClass(java.util.LinkedHashMap) is created i.e:
#Canonical
class MyClass {
def a
int c
}
// get the constructors
MyClass.class.getConstructors()
First object defined as def...
public MyClass()
public MyClass(java.lang.Object)
public MyClass(java.util.LinkedHashMap) // first def...
public MyClass(java.lang.Object,int)
Now if I change the order:
#Canonical
class MyClass {
int c
def a
}
// get the constructors
MyClass.class.getConstructors()
Now first is not def and public MyClass(java.util.LinkedHashMap) is not generated:
public MyClass()
public MyClass(int)
public MyClass(int,java.lang.Object)
Hope this helps,
There is an application where users can provide custom groovy scripts. They can write their own functions in those scripts. I want to restrict people from using the 'synchronized' keyword as well as some other keywords in these scripts. For example it should not be possible to write a function like below.
public synchronized void test() {
}
I am creating a CompilerConfiguration and using the SecureASTCustomizer. However adding org.codehaus.groovy.syntax.Types.KEYWORD_SYNCHRONIZED to the list of black listed tokens doesn't seem to do the job. (if I add org.codehaus.groovy.syntax.Types.PLUS it's preventing the usage of '+' within scripts.. but doesn't seem to do the job for synchronized)
Any ideas on how to achieve this ...
You can do something like this:
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.syntax.SyntaxException
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.classgen.GeneratorContext
class SynchronizedRemover extends org.codehaus.groovy.control.customizers.CompilationCustomizer {
SynchronizedRemover() {
super(org.codehaus.groovy.control.CompilePhase.CONVERSION)
}
void call(final SourceUnit source, final GeneratorContext context, final ClassNode classNode) {
classNode.methods.each { mn ->
if (mn.modifiers & 0x0020) { // 0x0020 is for synchronized
source.addError(new SyntaxException("Synchronized is not allowed", mn.lineNumber, mn.columnNumber))
}
}
}
}
def config = new CompilerConfiguration()
config.addCompilationCustomizers(new SynchronizedRemover())
def shell = new GroovyShell(config)
shell.evaluate '''
class Foo { public synchronized void foo() { println 'bar' } }
'''
The idea is to create a compilation customizer that checks generated classes and for each method, add an error if the synchronized modifier is present. For synchronized block inside methods, you can probably use the SecureASTCustomizer with a custom statement checker.