Ivan Ponomarev created LANG-1818:
------------------------------------

             Summary: ClassUtils.getShortClassName(Class) misinterprets '$' in 
legitimate class names
                 Key: LANG-1818
                 URL: https://issues.apache.org/jira/browse/LANG-1818
             Project: Commons Lang
          Issue Type: Improvement
            Reporter: Ivan Ponomarev


h3.Description

{{ClassUtils.getShortClassName(String)}} operates on JVM binary names and 
necessarily relies on heuristics. As documented in its Javadoc, it cannot 
reliably distinguish package names, outer classes, and inner classes in all 
cases when given only a {{String}}.

However, these limitations should not apply to {{getShortClassName(Class)}} 
(and {{getShortClassName(Object))}}, which currently delegate to the 
string-based method:

{code:java}
public static String getShortClassName(final Class<?> cls) {
    if (cls == null) {
        return StringUtils.EMPTY;
    }
    return getShortClassName(cls.getName());
}
{code}

When a {{Class<?>}} instance is available, we have unambiguous metadata. 
Delegating to the string-based implementation causes legitimate $ characters in 
class identifiers to be incorrectly interpreted as inner-class separators.

h3.Motivation

Although uncommon, {{$}} is a valid character in Java identifiers and is used 
in practice (e.g. generated code, DSL-heavy libraries, test fixtures, Selenide 
library exports {{$}} and {{$$}} names). Treating every $ as an inner-class 
separator leads to incorrect results even for top-level and member classes.

While this ambiguity is unavoidable for {{getShortClassName(String)}}, it is 
avoidable for {{getShortClassName(Class)}}.

h3.Reproducer

{code:java}
class $trange {}

class Pa$$word {}

class ClassUtilsShortClassNameTest {

    class $Inner {}

    class Inner {
        class Ne$ted {}
    }

    @Test
    void testDollarSignImmediatelyAfterPackage() {
        assertEquals("$trange", ClassUtils.getShortClassName($trange.class));
        // Actual (before fix): ".trange"
    }

    @Test
    void testDollarSignWithinName() {
        assertEquals("Pa$$word", ClassUtils.getShortClassName(Pa$$word.class));
        // Actual (before fix): "Pa..word"
    }

    @Test
    void testMultipleDollarSigns() {
        assertEquals(
            getClass().getSimpleName() + ".$Inner",
            ClassUtils.getShortClassName($Inner.class)
        );
        // Actual (before fix): "ClassUtilsShortClassNameTest..Inner"
    }

    @Test
    void testNe$tedClassName() {
        assertEquals(
            getClass().getSimpleName() + ".Inner.Ne$ted",
            ClassUtils.getShortClassName(Inner.Ne$ted.class)
        );
        // Actual (before fix): "ClassUtilsShortClassNameTest.Inner.Ne.ted"
    }
}

{code}

h3.Proposed fix

* Reimplement {{getShortClassName(Class)}} using {{Class}} metadata 
({{getSimpleName()}}, {{getDeclaringClass()}}, {{getEnclosingClass()}}, array 
component handling) instead of delegating to the string-based method.
* Preserve existing behavior for local and anonymous classes 
(compiler-generated ordinal names) by falling back to the legacy string-based 
logic, ensuring backward compatibility with existing tests.
* Leave {{getShortClassName(String)}} unchanged and continue to document its 
inherent ambiguity.

As a result: {{getShortCanonicalName(String)}} continues to rely on the 
string-based implementation and remains subject to the documented limitations; 
this is expected and unchanged. This change improves correctness for the 
Class-based APIs without altering behavior for purely string-based usage.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to