diff --git a/plugin/src/main/java/io/jenkins/plugins/casc/impl/configurators/DescriptorConfigurator.java b/plugin/src/main/java/io/jenkins/plugins/casc/impl/configurators/DescriptorConfigurator.java index f20c795dda..7b17a8740b 100644 --- a/plugin/src/main/java/io/jenkins/plugins/casc/impl/configurators/DescriptorConfigurator.java +++ b/plugin/src/main/java/io/jenkins/plugins/casc/impl/configurators/DescriptorConfigurator.java @@ -8,6 +8,8 @@ import io.jenkins.plugins.casc.ConfigurationContext; import io.jenkins.plugins.casc.RootElementConfigurator; import io.jenkins.plugins.casc.model.Mapping; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -64,17 +66,63 @@ private List resolvePossibleNames(Descriptor descriptor) { return Optional.ofNullable(descriptor.getClass().getAnnotation(Symbol.class)) .map(s -> Arrays.asList(s.value())) .orElseGet(() -> { - /* TODO: extract Descriptor parameter type such that DescriptorImpl extends Descriptor returns XX. - * Then, if `baseClass == fooXX` we get natural name `foo`. - */ - return singletonList(fromPascalCaseToCamelCase( - descriptor.getKlass().toJavaClass().getSimpleName())); + String fallbackName = fromPascalCaseToCamelCase( + unwrapAnonymous(descriptor.getKlass().toJavaClass()).getSimpleName()); + + Class typeParam = extractDescriptorTypeParameter(descriptor.getClass()); + String derivedName = null; + + if (typeParam != null) { + Class targetClass = unwrapAnonymous(typeParam); + derivedName = fromPascalCaseToCamelCase(targetClass.getSimpleName()); + } + + if (derivedName != null && !derivedName.equals(fallbackName)) { + return Arrays.asList(fallbackName, derivedName); + } + + return singletonList(fallbackName); }); } + private Class extractDescriptorTypeParameter(Class clazz) { + while (clazz != null && clazz != Object.class) { + Type genericSuperclass = clazz.getGenericSuperclass(); + + if (genericSuperclass instanceof ParameterizedType pt + && pt.getRawType() instanceof Class rawClass + && Descriptor.class.isAssignableFrom(rawClass)) { + + Type[] args = pt.getActualTypeArguments(); + + if (args.length > 0) { + Type typeArg = args[0]; + + if (typeArg instanceof Class clazzArg) { + return clazzArg; + } + + if (typeArg instanceof ParameterizedType nestedPt + && nestedPt.getRawType() instanceof Class nestedClass) { + return nestedClass; + } + } + } + clazz = clazz.getSuperclass(); + } + return null; + } + private static String fromPascalCaseToCamelCase(String s) { StringBuilder sb = new StringBuilder(s); sb.setCharAt(0, Character.toLowerCase(s.charAt(0))); return sb.toString(); } + + private Class unwrapAnonymous(Class clazz) { + while (clazz.isAnonymousClass()) { + clazz = clazz.getSuperclass(); + } + return clazz; + } } diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/impl/configurators/DescriptorConfiguratorTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/impl/configurators/DescriptorConfiguratorTest.java new file mode 100644 index 0000000000..1a4344ea1b --- /dev/null +++ b/plugin/src/test/java/io/jenkins/plugins/casc/impl/configurators/DescriptorConfiguratorTest.java @@ -0,0 +1,114 @@ +package io.jenkins.plugins.casc.impl.configurators; + +import static org.junit.Assert.assertEquals; + +import hudson.model.Describable; +import hudson.model.Descriptor; +import java.util.Arrays; +import java.util.Collections; +import org.jenkinsci.Symbol; +import org.junit.Test; + +public class DescriptorConfiguratorTest { + + public static class DummyTask implements Describable { + @Override + public Descriptor getDescriptor() { + throw new UnsupportedOperationException("Not required for name extraction tests"); + } + } + + public static class ParameterizedTask implements Describable> { + @Override + public Descriptor> getDescriptor() { + throw new UnsupportedOperationException(); + } + } + + public static class DummyTaskDescriptor extends Descriptor { + public DummyTaskDescriptor() { + super(DummyTask.class); + } + } + + @Symbol({"primary", "alias"}) + public static class MultiSymbolDescriptor extends Descriptor { + public MultiSymbolDescriptor() { + super(DummyTask.class); + } + } + + public abstract static class SimulatedBuildStepDescriptor> extends Descriptor { + protected SimulatedBuildStepDescriptor(Class clazz) { + super(clazz); + } + } + + public static class DeepTaskDescriptor extends SimulatedBuildStepDescriptor { + public DeepTaskDescriptor() { + super(DummyTask.class); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static class ParameterizedDescriptor extends Descriptor> { + public ParameterizedDescriptor() { + super((Class) ParameterizedTask.class); + } + } + + @SuppressWarnings("rawtypes") + public static class RawDescriptor extends Descriptor { + @SuppressWarnings("unchecked") + public RawDescriptor(Class clazz) { + super(clazz); + } + } + + @Test + public void testMultipleSymbols() { + DescriptorConfigurator configurator = new DescriptorConfigurator(new MultiSymbolDescriptor()); + + assertEquals("Should use the first symbol as primary name", "primary", configurator.getName()); + assertEquals( + "Should return all symbols as aliases", Arrays.asList("primary", "alias"), configurator.getNames()); + } + + @Test + public void testNameResolvedFromGenericExtraction() { + DescriptorConfigurator configurator = new DescriptorConfigurator(new DummyTaskDescriptor()); + + assertEquals("Should extract 'DummyTask' and convert to camelCase", "dummyTask", configurator.getName()); + assertEquals(Collections.singletonList("dummyTask"), configurator.getNames()); + } + + @Test + public void testNameResolvedFromParameterizedType() { + DescriptorConfigurator configurator = new DescriptorConfigurator(new ParameterizedDescriptor()); + + assertEquals( + "Should unwrap ParameterizedType to its raw class name", "parameterizedTask", configurator.getName()); + } + + @Test + public void testNameResolvedFromDeepInheritance() { + DescriptorConfigurator configurator = new DescriptorConfigurator(new DeepTaskDescriptor()); + + assertEquals( + "Should bypass type erasure and extract from superclass hierarchy", + "dummyTask", + configurator.getName()); + } + + @Test + @SuppressWarnings("rawtypes") + public void testFallbackWithAnonymousTargetClass() { + + Class anonymousTarget = new DummyTask() {}.getClass(); + Descriptor rawDescriptor = new RawDescriptor(anonymousTarget); + DescriptorConfigurator configurator = new DescriptorConfigurator(rawDescriptor); + + assertEquals( + "Should unwrap anonymous target class via the fallback logic", "dummyTask", configurator.getName()); + } +}