Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Duplicate annotation for class" with @PicocliScript when the script contains classes #388

Closed
bpow opened this issue Jun 11, 2018 · 6 comments
Milestone

Comments

@bpow
Copy link

bpow commented Jun 11, 2018

This works as expected:

@Grab('info.picocli:picocli:3.0.2')
import static picocli.CommandLine.*
@Command(name="classyTest", mixinStandardHelpOptions = true, separator = ' ', version = '0.1')
@picocli.groovy.PicocliScript
import groovy.transform.Field

@Option(names = ['-g', '--greeting'], description = 'Type of greeting')
@Field String greeting = 'Hello'

println "${greeting} world!"

/* class Message {
     String greeting
     String target
} */

However, if the Message class is un-commented, I get the following error:

Caught: java.lang.annotation.AnnotationFormatError: Duplicate annotation for class: interface picocli.CommandLine$Command: @picocli.CommandLine$Command(showDefaultValues=false, optionListHeading=, hidden=false, footer=[], commandListHeading=Commands:%n, description=[], helpCommand=false, requiredOptionMarker= , separator= , version=[0.1], customSynopsis=[], descriptionHeading=, parameterListHeading=, synopsisHeading=Usage: , sortOptions=true, footerHeading=, name=classyTest, header=[], headerHeading=, abbreviateSynopsis=false, mixinStandardHelpOptions=true, versionProvider=class picocli.CommandLine$NoVersionProvider, subcommands=[])
java.lang.annotation.AnnotationFormatError: Duplicate annotation for class: interface picocli.CommandLine$Command: @picocli.CommandLine$Command(showDefaultValues=false, optionListHeading=, hidden=false, footer=[], commandListHeading=Commands:%n, description=[], helpCommand=false, requiredOptionMarker= , separator= , version=[0.1], customSynopsis=[], descriptionHeading=, parameterListHeading=, synopsisHeading=Usage: , sortOptions=true, footerHeading=, name=classyTest, header=[], headerHeading=, abbreviateSynopsis=false, mixinStandardHelpOptions=true, versionProvider=class picocli.CommandLine$NoVersionProvider, subcommands=[])
        at picocli.CommandLine$Model$CommandReflection.updateCommandAttributes(CommandLine.java:4129)
        at picocli.CommandLine$Model$CommandReflection.extractCommandSpec(CommandLine.java:4114)
        at picocli.CommandLine$Model$CommandSpec.forAnnotatedObject(CommandLine.java:2706)
        at picocli.CommandLine.<init>(CommandLine.java:151)
        at picocli.CommandLine.<init>(CommandLine.java:133)
        at picocli.groovy.PicocliBaseScript.createCommandLine(PicocliBaseScript.java:177)
        at picocli.groovy.PicocliBaseScript.getOrCreateCommandLine(PicocliBaseScript.java:159)
        at picocli.groovy.PicocliBaseScript.run(PicocliBaseScript.java:104)

I guess maybe the AST transformation is somehow being applied to the "inner" class as well?

The script will also run if the Message part is un-commented, but with the @Command line removed. But then I don't think there is a way to specify things like separator and mixinStandardHelpOptions. Is there someway to make sure the @Command annotation is applied just to the Script class itself, and not other classes within?

@remkop
Copy link
Owner

remkop commented Jun 11, 2018

Thank you for the bug report!
I was able to reproduce the issue.

With the inner class, the decompiled code for the script looks like this:

import org.codehaus.groovy.runtime.GStringImpl;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.callsite.CallSite;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

@Command(
    name = "classyTest",
    mixinStandardHelpOptions = true,
    separator = " ",
    version = {"0.1"}
)
@Command(
    name = "classyTest",
    mixinStandardHelpOptions = true,
    separator = " ",
    version = {"0.1"}
)
public class ScriptWithInnerClassTest extends PicocliBaseScript {
    @Option(
        names = {"-g", "--greeting"},
        description = {"Type of greeting"}
    )
    String greeting;

    public ScriptWithInnerClassTest() {
        CallSite[] var1 = $getCallSiteArray();
        String var2 = "Hello";
        this.greeting = var2;
    }

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

    protected Object runScriptBody() {
        CallSite[] var1 = $getCallSiteArray();
        Object var10000 = null;
        return var1[1].callCurrent(this, new GStringImpl(new Object[]{this.greeting}, new String[]{"", " world!"}));
    }
}

While the decompiled inner class looks like this:

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.codehaus.groovy.runtime.callsite.CallSite;

public class Message implements GroovyObject {
    private String greeting;
    private String target;

    public Message() {
        CallSite[] var1 = $getCallSiteArray();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    public String getGreeting() {
        return this.greeting;
    }

    public void setGreeting(String var1) {
        this.greeting = var1;
    }

    public String getTarget() {
        return this.target;
    }

    public void setTarget(String var1) {
        this.target = var1;
    }
}

Still investigating...

@remkop
Copy link
Owner

remkop commented Jun 11, 2018

It turns out that the AST transformation is invoked twice if the script contains an inner class. (I didn't experiment further if it would be invoked more often for additional inner classes.)

The fix is to only add the annotation if it does not already exist.

Index: src/main/java/picocli/groovy/PicocliScriptASTTransformation.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/main/java/picocli/groovy/PicocliScriptASTTransformation.java	(revision )
+++ src/main/java/picocli/groovy/PicocliScriptASTTransformation.java	(revision )
@@ -137,7 +137,9 @@
         }
 
         List<AnnotationNode> annotations = parent.getAnnotations(COMMAND_TYPE);
-        cNode.addAnnotations(annotations);
+        if (cNode.getAnnotations(COMMAND_TYPE).isEmpty()) { // #388 prevent "Duplicate annotation for class" AnnotationFormatError
+            cNode.addAnnotations(annotations);
+        }
         cNode.setSuperClass(baseScriptType);

This test breaks without the fix but passes when the fix is applied:

Index: src/test/groovy/picocli/groovy/PicocliBaseScriptTest.groovy
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/test/groovy/picocli/groovy/PicocliBaseScriptTest.groovy	(revision )
+++ src/test/groovy/picocli/groovy/PicocliBaseScriptTest.groovy	(revision )
@@ -292,4 +292,31 @@
         assert params.positional.contains("123")
     }
 
+
+    @Test
+    void testScriptWithInnerClass() {
+        String script = '''
+import static picocli.CommandLine.*
+@Command(name="classyTest")
+@picocli.groovy.PicocliScript
+import groovy.transform.Field
+
+@Option(names = ['-g', '--greeting'], description = 'Type of greeting')
+@Field String greeting = 'Hello\'
+
+println "${greeting} world!"
+
+class Message {
+    String greeting
+    String target
+}
+'''
+        GroovyShell shell = new GroovyShell(new Binding())
+        shell.context.setVariable('args', ["-g", "Hi"] as String[])
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream()
+        System.setOut(new PrintStream(baos))
+        shell.evaluate script
+        assertEquals("Hi world!", baos.toString().trim())
+    }
 }

@remkop
Copy link
Owner

remkop commented Jun 11, 2018

I will release a new version with this fix within the next day or two.

@remkop remkop added this to the 3.1 milestone Jun 11, 2018
@remkop remkop closed this as completed in 15da254 Jun 11, 2018
@remkop
Copy link
Owner

remkop commented Jun 11, 2018

I just released picocli 3.1, which includes the fix for this issue.

Thanks again for the bug report! Keep them coming!

@bpow
Copy link
Author

bpow commented Jun 11, 2018

Thanks, that was turbo-fast!

@remkop
Copy link
Owner

remkop commented Jun 11, 2018

Spread the word!
:-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants