Skip to content

Conversation

abhisripathi
Copy link

This PR fixes issue LANG-1771 where the input "[String" was incorrectly interpreted as "short[]" by the getShortCanonicalName method in ClassUtils.

✅ Root Cause:
The internal descriptor parser treated the first character 'S' from "String" as a JVM type abbreviation for short.

✅ Fix:

  • Added validation to only apply abbreviation map to single-character descriptors.
  • Malformed inputs like "[String" now return "String[]" as expected.

✅ Tests:

  • Added test_getShortCanonicalName_MalformedInput to ClassUtilsTest
  • Confirmed it fails before the fix and passes after
  • All other tests pass (67/67)

Closes: https://issues.apache.org/jira/browse/LANG-1771

Copy link
Member

@garydgregory garydgregory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abhisripathi
Please remove "countUpperCaseLetters" as it has nothing to do with this PR.

@@ -237,6 +237,14 @@ public void test_getAllSuperclasses_Class() {
assertNull(ClassUtils.getAllSuperclasses(null));
}

@Test
public void test_getShortCanonicalName_MalformedInput() {
String input = "[String";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline all of these local variables.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback, Gary!
I’ve removed the unrelated countUpperCaseLetters code and inlined the local variables in the test as requested.
Let me know if anything else needs to be updated.

@abhisripathi abhisripathi force-pushed the fix/LANG-1771-ShortCanonicalName branch from a5e5ac4 to 91368c0 Compare May 27, 2025 10:30
Copy link
Member

@garydgregory garydgregory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @abhisripathi
Thank you for your update. Please see my comments.

className = reverseAbbreviationMap.get(className.substring(0, 1));
}
// else if (!className.isEmpty()) {
// className = reverseAbbreviationMap.get(className.substring(0, 1));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this comment needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — I’ve removed the commented-out code to keep the file clean. It was left in unintentionally during the initial iteration. Thanks for pointing it out!

@@ -937,7 +937,7 @@ public void testIsAllUpperCase() {
assertFalse(StringUtils.isAllUpperCase("A1C"));
assertFalse(StringUtils.isAllUpperCase("A/C"));
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run mvn by itself to catch all build errors.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran mvn clean install and the full build passed successfully.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not the same as running 'mvn' solo which runs the default Maven goal which contains all build checks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran mvn standalone — all build checks, Javadoc, RAT, and Checkstyle passed with BUILD SUCCESS.
Let me know if anything else is needed!

// else if (!className.isEmpty()) {
// className = reverseAbbreviationMap.get(className.substring(0, 1));
// }
else if (!className.isEmpty() && className.length() == 1 && reverseAbbreviationMap.containsKey(className)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the length is 1, then it can't be empty, or am I missing something?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — since className.length() == 1, the string can’t be empty.
I'm planning to remove the !className.isEmpty() check and keep just className.length() == 1 — does that sound good?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — since className.length() == 1, the string can’t be empty. I'm planning to remove the !className.isEmpty() check and keep just className.length() == 1 — does that sound good?

You tell me! ;-) Does this change need more test coverage? Are all formats for canonical names tested?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the test suite and found that most formats (primitive, object arrays, inner classes, null) are already covered. I’ve added tests specifically for the malformed descriptors [String and [LString which are directly related to this fix.

@abhisripathi abhisripathi force-pushed the fix/LANG-1771-ShortCanonicalName branch from 91368c0 to b051362 Compare May 28, 2025 00:28
@abhisripathi abhisripathi force-pushed the fix/LANG-1771-ShortCanonicalName branch from b051362 to 4c7ecd9 Compare May 28, 2025 01:23
@garydgregory garydgregory changed the title Fix LANG-1771: Handle malformed array input '[String' in getShortCanonicalName [LANG-1771] Handle malformed array input '[String' in getShortCanonicalName May 28, 2025
@garydgregory
Copy link
Member

garydgregory commented May 28, 2025

-1: Hm, now that I look at this PR again, this test doesn't make sense:

assertEquals("String[]", ClassUtils.getShortCanonicalName("[String"));

We would be inventing a new syntax for array type names. I don't see where the JRE, Java Language Specification, or Java VM Specification allows such a syntax.

The legal values are:

  • "[Ljava.lang.String;" is equivalent to String[].class.getName()
  • "java.lang.String[]" is equivalent to String[].class.getCanonicalName()

For example:

        assertEquals("String[]", ClassUtils.getShortCanonicalName(String[].class.getName()));
        assertEquals("String[]", ClassUtils.getShortCanonicalName(String[].class.getCanonicalName()));
        assertEquals("String[]", ClassUtils.getShortCanonicalName("String[]"));

@abhisripathi
Copy link
Author

Hi Gary,
Just to clarify — the malformed input test case ("[String" → "String[]") was taken directly from the JIRA ticket (LANG-1771), which I was fixing.
I included it to demonstrate the behavior is now correct, but I didn't invent this test format myself.
Would you prefer that I remove this test case and only keep the logic fix, or is it fine to keep it since it directly validates the issue described?

@garydgregory
Copy link
Member

Hello @abhisripathi

I don't think we should return bad output for bad input; bad input should throw an IAE. Unfortunately, the current implementation is inconsistent, sometimes it throws an exception, sometimes it's GIGO.

For example (now in git master):

        // Note that we throw RuntimeException (but not which one) for the following bad inputs:
        assertThrows(RuntimeException.class, () -> ClassUtils.getShortCanonicalName(""));
        assertThrows(RuntimeException.class, () -> ClassUtils.getShortCanonicalName("["));
        assertThrows(RuntimeException.class, () -> ClassUtils.getShortCanonicalName("[]"));
        assertThrows(RuntimeException.class, () -> ClassUtils.getShortCanonicalName("[;"));
        assertThrows(RuntimeException.class, () -> ClassUtils.getShortCanonicalName("[];"));
        assertThrows(RuntimeException.class, () -> ClassUtils.getShortCanonicalName(" "));
        assertThrows(RuntimeException.class, () -> ClassUtils.getShortCanonicalName("[$"));
        assertThrows(RuntimeException.class, () -> ClassUtils.getShortCanonicalName("[$a"));

We should probably re-implement the heart of this parsing using regular expressions instead of custom code.

@abhisripathi
Copy link
Author

Hi @garydgregory

Thanks for the clarification, Gary — that makes sense.
Given the updated direction, would you prefer that I modify the implementation to throw an IllegalArgumentException for malformed input like "[String" instead of returning "String[]"?
Or would you recommend we pause on this patch entirely and revisit the method design as a whole (possibly around regex parsing as you suggested)?
I’m happy to adapt based on the preferred path forward.

Copy link
Member

@garydgregory garydgregory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abhisripathi
It looks to me like your assumptions on legal inputs are incorrect. Please see my comments.

@@ -237,6 +237,15 @@ public void test_getAllSuperclasses_Class() {
assertNull(ClassUtils.getAllSuperclasses(null));
}

@Test
public void test_getShortCanonicalName_MalformedInput() {
assertEquals("String[]", ClassUtils.getShortCanonicalName("[String"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see how "[String" is a legal name, but "[Ljava.lang.String;" is legal. Do you read the JLS to allow this? For an unpackaged class Foo, an array would be "[LFoo;".

}
@Test
public void test_getShortCanonicalName_MissingSemicolon() {
assertEquals("String[]", ClassUtils.getShortCanonicalName("[LString"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see how "[LString" is a legal name, but "[Ljava.lang.String;" is legal. Do you read the JLS to allow this? For an unpackaged class Foo, an array would be "[LFoo;".

@garydgregory
Copy link
Member

Closing in favor of #1437

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants