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)