diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 0a8142c2..2a2f9bee 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -23,6 +23,7 @@ Picocli follows [semantic versioning](http://semver.org/). * [#1126] Enhancement: Make picocli trace levels case-insensitive. * [#1128] Enhancement: `ParameterException` caused by `TypeConversionException` now have their cause exception set. * [#1137] Bugfix: The `picocli-codegen` annotation processor causes the build to fail with a `ClassCastException` when an option has `completionCandidates` defined. +* [#1134] Bugfix: The `picocli-codegen` annotation processor should allow `@Spec`-annotated field in classes implementing `IVersionProvider`. * [#1127] DOC: Custom ShortErrorMessageHandler manual example should use bold red for error message. * [#1130] DOC: Clarify how to run picocli-based applications. * [#1131] DOC: Add anchor links before section titles in user manual. diff --git a/picocli-annotation-processing-tests/src/test/java/picocli/annotation/processing/tests/Issue1134Test.java b/picocli-annotation-processing-tests/src/test/java/picocli/annotation/processing/tests/Issue1134Test.java new file mode 100644 index 00000000..a67696fc --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/java/picocli/annotation/processing/tests/Issue1134Test.java @@ -0,0 +1,40 @@ +package picocli.annotation.processing.tests; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import org.junit.Test; + +import javax.annotation.processing.Processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; +import static picocli.annotation.processing.tests.Resources.slurp; +import static picocli.annotation.processing.tests.YamlAssert.compareCommandYamlDump; + +public class Issue1134Test { + @Test + public void testIssue1134() { + Processor processor = new AnnotatedCommandSourceGeneratorProcessor(); + Compilation compilation = + javac() + .withProcessors(processor) + .compile(JavaFileObjects.forResource( + "picocli/issue1134/Issue1134.java")); + + assertThat(compilation).succeeded(); + } + + @Test + public void testIssue1134Details() { + + Compilation compilation = compareCommandYamlDump(slurp("/picocli/issue1134/Issue1134.yaml"), + JavaFileObjects.forResource("picocli/issue1134/Issue1134.java")); + + assertOnlySourceVersionWarning(compilation); + } + + private void assertOnlySourceVersionWarning(Compilation compilation) { + assertThat(compilation).hadWarningCount(0); // #826 version warnings are now suppressed + // assertThat(compilation).hadWarningContaining("Supported source version 'RELEASE_6' from annotation processor 'picocli.annotation.processing.tests"); + } +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/issue1134/Issue1134.java b/picocli-annotation-processing-tests/src/test/resources/picocli/issue1134/Issue1134.java new file mode 100644 index 00000000..e6fb2ff5 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/issue1134/Issue1134.java @@ -0,0 +1,30 @@ +package picocli.issue1134; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.IVersionProvider; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + + +@Command(name = "top", + versionProvider = MyVersionProvider.class, + resourceBundle = "mybundle5") +public class Issue1134 { + + @CommandLine.Option(names = "--level") + private String level; + + @Spec + CommandSpec spec; +} + +class MyVersionProvider implements IVersionProvider { + @Spec + CommandSpec spec; + + public String[] getVersion() { + return new String[] {spec.qualifiedName() + " 1.0"}; + } +} \ No newline at end of file diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/issue1134/Issue1134.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/issue1134/Issue1134.yaml new file mode 100644 index 00000000..3491fbe4 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/issue1134/Issue1134.yaml @@ -0,0 +1,70 @@ +--- +CommandSpec: + name: 'top' + aliases: [] + userObject: picocli.issue1134.Issue1134 + helpCommand: false + defaultValueProvider: null + versionProvider: VersionProviderMetaData(picocli.issue1134.MyVersionProvider) + version: [] + ArgGroups: [] + Options: + - names: [--level] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: CompileTimeTypeInfo(java.lang.String, aux=[java.lang.String], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: AnnotatedElementHolder(FIELD level in picocli.issue1134.Issue1134) + setter: AnnotatedElementHolder(FIELD level in picocli.issue1134.Issue1134) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: false + Subcommands: [] diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AbstractCommandSpecProcessor.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AbstractCommandSpecProcessor.java index ad49cb64..2fe6e1b7 100644 --- a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AbstractCommandSpecProcessor.java +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AbstractCommandSpecProcessor.java @@ -33,7 +33,9 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVisitor; import javax.lang.model.util.SimpleElementVisitor6; +import javax.lang.model.util.TypeKindVisitor6; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import java.io.PrintWriter; @@ -840,7 +842,23 @@ public abstract class AbstractCommandSpecProcessor extends AbstractProcessor { logger.fine("Adding " + entry + " to commandSpec " + commandSpec1); commandSpec1.addSpecElement(entry.getValue()); } else { - proc.error(entry.getKey(), "@Spec must be enclosed in a @Command, but was %s: %s", entry.getKey().getEnclosingElement(), entry.getKey().getEnclosingElement().getSimpleName()); + Element enclosingElement = entry.getKey().getEnclosingElement(); + if (enclosingElement.getKind() == ElementKind.CLASS || enclosingElement.getKind() == ENUM) { + TypeMirror typeMirror = enclosingElement.asType(); + TypeElement typeElement = (TypeElement) ((DeclaredType) typeMirror).asElement(); + List interfaces = typeElement.getInterfaces(); + boolean valid = false; + for (TypeMirror interf : interfaces) { + if (interf.toString().equals("picocli.CommandLine.IVersionProvider")) { + valid = true; + } + } + if (!valid) { + proc.error(entry.getKey(), "@Spec must be enclosed in a @Command, or in a class that implements IVersionProvider but was %s: %s", entry.getKey().getEnclosingElement(), entry.getKey().getEnclosingElement().getSimpleName()); + } + } else { + proc.error(entry.getKey(), "@Spec must be enclosed in a @Command, but was %s: %s", entry.getKey().getEnclosingElement(), entry.getKey().getEnclosingElement().getSimpleName()); + } } } for (Map.Entry entry : parentCommandElements.entrySet()) {