From 4aa3d71668741e22e6a62ff3c9ae84485fb5e539 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Thu, 16 Apr 2026 18:01:12 +0530 Subject: [PATCH 1/4] Resolve Descriptor generic type to derive natural names in DescriptorConfigurator --- .../configurators/DescriptorConfigurator.java | 50 +++++++- .../DescriptorConfiguratorTest.java | 114 ++++++++++++++++++ 2 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 plugin/src/test/java/io/jenkins/plugins/casc/impl/configurators/DescriptorConfiguratorTest.java 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..edd4402fed 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,15 +66,53 @@ 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())); + Class typeParam = extractDescriptorTypeParameter(descriptor.getClass()); + + Class targetClass = typeParam != null + ? typeParam + : descriptor.getKlass().toJavaClass(); + + while (targetClass.isAnonymousClass()) { + targetClass = targetClass.getSuperclass(); + } + + return singletonList(fromPascalCaseToCamelCase(targetClass.getSimpleName())); }); } + private Class extractDescriptorTypeParameter(Class clazz) { + while (clazz != null && clazz != Object.class) { + Type genericSuperclass = clazz.getGenericSuperclass(); + + if (genericSuperclass instanceof ParameterizedType pt) { + Type rawType = pt.getRawType(); + + if (rawType instanceof Class && Descriptor.class.isAssignableFrom((Class) rawType)) { + Type[] args = pt.getActualTypeArguments(); + + if (args.length > 0) { + Type typeArg = args[0]; + + if (typeArg instanceof Class) { + return (Class) typeArg; + } else if (typeArg instanceof ParameterizedType) { + Type nestedRawType = ((ParameterizedType) typeArg).getRawType(); + if (nestedRawType instanceof Class) { + return (Class) nestedRawType; + } + } + } + } + } + clazz = clazz.getSuperclass(); + } + return null; + } + private static String fromPascalCaseToCamelCase(String s) { + if (s == null || s.isEmpty()) { + return s != null ? s : ""; + } StringBuilder sb = new StringBuilder(s); sb.setCharAt(0, Character.toLowerCase(s.charAt(0))); return sb.toString(); 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()); + } +} From 76ee5dc0024f2476ecbd52f2c132c937332c4372 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Thu, 16 Apr 2026 18:17:16 +0530 Subject: [PATCH 2/4] Combine if's --- .../configurators/DescriptorConfigurator.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) 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 edd4402fed..93c3ed8e30 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 @@ -84,24 +84,24 @@ private Class extractDescriptorTypeParameter(Class clazz) { while (clazz != null && clazz != Object.class) { Type genericSuperclass = clazz.getGenericSuperclass(); - if (genericSuperclass instanceof ParameterizedType pt) { - Type rawType = pt.getRawType(); - - if (rawType instanceof Class && Descriptor.class.isAssignableFrom((Class) rawType)) { - Type[] args = pt.getActualTypeArguments(); - - if (args.length > 0) { - Type typeArg = args[0]; - - if (typeArg instanceof Class) { - return (Class) typeArg; - } else if (typeArg instanceof ParameterizedType) { - Type nestedRawType = ((ParameterizedType) typeArg).getRawType(); - if (nestedRawType instanceof Class) { - return (Class) nestedRawType; - } - } - } + if (genericSuperclass instanceof ParameterizedType pt + && pt.getRawType() instanceof Class rawClass + && Descriptor.class.isAssignableFrom(rawClass)) { + + Type[] args = pt.getActualTypeArguments(); + if (args.length == 0) { + return null; + } + + 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(); From 3f96ccb1fc00e459af476ef72513a49a5b29d4f0 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Thu, 16 Apr 2026 21:21:48 +0530 Subject: [PATCH 3/4] Fix test failures --- .../configurators/DescriptorConfigurator.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) 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 93c3ed8e30..10c95505b2 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 @@ -66,17 +66,22 @@ private List resolvePossibleNames(Descriptor descriptor) { return Optional.ofNullable(descriptor.getClass().getAnnotation(Symbol.class)) .map(s -> Arrays.asList(s.value())) .orElseGet(() -> { + String fallbackName = fromPascalCaseToCamelCase( + unwrapAnonymous(descriptor.getKlass().toJavaClass()).getSimpleName()); + Class typeParam = extractDescriptorTypeParameter(descriptor.getClass()); + String derivedName = null; - Class targetClass = typeParam != null - ? typeParam - : descriptor.getKlass().toJavaClass(); + if (typeParam != null) { + Class targetClass = unwrapAnonymous(typeParam); + derivedName = fromPascalCaseToCamelCase(targetClass.getSimpleName()); + } - while (targetClass.isAnonymousClass()) { - targetClass = targetClass.getSuperclass(); + if (derivedName != null && !derivedName.equals(fallbackName)) { + return Arrays.asList(fallbackName, derivedName); } - return singletonList(fromPascalCaseToCamelCase(targetClass.getSimpleName())); + return singletonList(fallbackName); }); } @@ -111,10 +116,17 @@ private Class extractDescriptorTypeParameter(Class clazz) { private static String fromPascalCaseToCamelCase(String s) { if (s == null || s.isEmpty()) { - return s != null ? s : ""; + throw new IllegalStateException("Cannot derive configurator name from an empty class name"); } 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; + } } From 7200565f41c51fec15554381ba63704a5ff308fc Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Fri, 17 Apr 2026 01:04:25 +0530 Subject: [PATCH 4/4] Remove dead codes --- .../configurators/DescriptorConfigurator.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) 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 10c95505b2..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 @@ -94,19 +94,18 @@ private Class extractDescriptorTypeParameter(Class clazz) { && Descriptor.class.isAssignableFrom(rawClass)) { Type[] args = pt.getActualTypeArguments(); - if (args.length == 0) { - return null; - } - Type typeArg = args[0]; + if (args.length > 0) { + Type typeArg = args[0]; - if (typeArg instanceof Class clazzArg) { - return clazzArg; - } + if (typeArg instanceof Class clazzArg) { + return clazzArg; + } - if (typeArg instanceof ParameterizedType nestedPt - && nestedPt.getRawType() instanceof Class nestedClass) { - return nestedClass; + if (typeArg instanceof ParameterizedType nestedPt + && nestedPt.getRawType() instanceof Class nestedClass) { + return nestedClass; + } } } clazz = clazz.getSuperclass(); @@ -115,9 +114,6 @@ private Class extractDescriptorTypeParameter(Class clazz) { } private static String fromPascalCaseToCamelCase(String s) { - if (s == null || s.isEmpty()) { - throw new IllegalStateException("Cannot derive configurator name from an empty class name"); - } StringBuilder sb = new StringBuilder(s); sb.setCharAt(0, Character.toLowerCase(s.charAt(0))); return sb.toString();