Skip to content

Commit 8fc2e5d

Browse files
committed
Support '.' for inner class separators for now
Signed-off-by: BoykoAlex <alex.boyko@broadcom.com>
1 parent cba0086 commit 8fc2e5d

File tree

3 files changed

+242
-7
lines changed

3 files changed

+242
-7
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/JavaType.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
* <li>{@link ArrayTypeName} — an array type ({@code String[]}, {@code int[][]}, etc.)</li>
3131
* </ul>
3232
* <p>
33-
* Instances are created via the {@link #parse(String)} factory method, which accepts
34-
* fully qualified type strings using {@code $} for inner classes
35-
* (as returned by {@code Class.getName()}).
33+
* Instances are created via the {@link #parse(String)} factory method, which accepts
34+
* fully qualified type strings using either {@code $} or {@code .} for inner classes.
35+
* When {@code .} is used, the parser applies a heuristic based on Java naming conventions
36+
* (package segments are lowercase, class names start with uppercase) to detect inner class
37+
* boundaries.
3638
*
3739
* @author Alex Boyko
3840
*/
@@ -76,7 +78,12 @@ public interface JavaType {
7678
/**
7779
* Parse a type string into a {@link JavaType} instance.
7880
* <p>
79-
* Inner classes are denoted with {@code $} (e.g. {@code "java.util.Map$Entry"}).
81+
* Inner classes may be denoted with {@code $} (e.g. {@code "java.util.Map$Entry"})
82+
* or with {@code .} (e.g. {@code "java.util.Map.Entry"}). When {@code .} is used,
83+
* the parser applies a heuristic based on Java naming conventions to detect inner
84+
* class boundaries: consecutive {@code .}-separated segments that both start with
85+
* an uppercase letter are treated as an inner class boundary.
86+
* <p>
8087
* Supports simple types, inner classes, parameterized types, wildcards,
8188
* primitive types, and array types.
8289
* <p>
@@ -86,19 +93,20 @@ public interface JavaType {
8693
* Examples of accepted input:
8794
* <ul>
8895
* <li>{@code "java.util.Map"}</li>
89-
* <li>{@code "java.util.Map$Entry"}</li>
96+
* <li>{@code "java.util.Map$Entry"} or {@code "java.util.Map.Entry"}</li>
9097
* <li>{@code "java.util.Map<java.lang.String, java.util.List<java.lang.Integer>>"}</li>
9198
* <li>{@code "java.util.List<? extends com.example.Foo>"}</li>
9299
* <li>{@code "int"}, {@code "boolean"}</li>
93100
* <li>{@code "int[]"}, {@code "java.lang.String[][]"}</li>
94101
* <li>{@code "java.util.List<java.lang.String>[]"}</li>
95102
* </ul>
96103
*
97-
* @param typeString the type string (using {@code $} for inner classes)
104+
* @param typeString the type string
98105
* @return the parsed {@link JavaType}
99106
*/
100107
static JavaType parse(String typeString) {
101-
String sig = Signature.createTypeSignature(typeString.trim(), true);
108+
String normalized = NormalizeUtils.normalizeInnerClasses(typeString.trim());
109+
String sig = Signature.createTypeSignature(normalized, true);
102110
return parseFromSignature(sig);
103111
}
104112

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.jdt.refactoring;
12+
13+
import java.util.regex.Matcher;
14+
import java.util.regex.Pattern;
15+
16+
/**
17+
* Utility class for normalizing Java type strings.
18+
* <p>
19+
* Provides methods to convert fully qualified type names that use {@code .} for
20+
* inner class boundaries into the canonical form using {@code $}.
21+
*
22+
* @author Alex Boyko
23+
*/
24+
public class NormalizeUtils {
25+
26+
/**
27+
* Matches a fully qualified name token: sequences of word characters, {@code .} and {@code $}.
28+
* Everything else (e.g. {@code <}, {@code >}, {@code ,}, {@code ?}, {@code []},
29+
* whitespace, {@code extends}, {@code super}) is left untouched.
30+
*/
31+
private static final Pattern NAME_TOKEN = Pattern.compile("[\\w.$]+");
32+
33+
private NormalizeUtils() {
34+
// utility class
35+
}
36+
37+
/**
38+
* Normalize a type string so that inner class boundaries use {@code $} instead of {@code .}.
39+
* <p>
40+
* Applies a heuristic based on Java naming conventions: when two consecutive
41+
* {@code .}-separated segments both start with an uppercase letter, the {@code .}
42+
* between them is treated as an inner class boundary and replaced with {@code $}.
43+
* <p>
44+
* The method extracts each fully qualified name token from the type string
45+
* (splitting around structural characters like {@code <}, {@code >}, {@code ,},
46+
* and whitespace) and applies the heuristic independently to each token.
47+
* Input that already uses {@code $} is returned unchanged.
48+
*
49+
* @param typeString the type string to normalize
50+
* @return the normalized type string with {@code $} for inner class boundaries
51+
*/
52+
static String normalizeInnerClasses(String typeString) {
53+
if (typeString.isEmpty() || typeString.indexOf('$') >= 0) {
54+
return typeString;
55+
}
56+
57+
Matcher matcher = NAME_TOKEN.matcher(typeString);
58+
StringBuilder result = new StringBuilder(typeString.length());
59+
60+
while (matcher.find()) {
61+
matcher.appendReplacement(result, Matcher.quoteReplacement(applyUppercaseHeuristic(matcher.group())));
62+
}
63+
matcher.appendTail(result);
64+
65+
return result.toString();
66+
}
67+
68+
/**
69+
* Apply the uppercase heuristic to a single name token (no type argument
70+
* delimiters, commas, whitespace, or array brackets — just a dotted name,
71+
* possibly with {@code $}).
72+
* <p>
73+
* Splits on {@code .}, finds the first segment starting with an uppercase letter
74+
* (the outermost class), and converts subsequent {@code .} separators between
75+
* uppercase-starting segments to {@code $}.
76+
*/
77+
private static String applyUppercaseHeuristic(String token) {
78+
String[] parts = token.split("\\.");
79+
if (parts.length <= 1) {
80+
return token;
81+
}
82+
83+
boolean foundClass = false;
84+
StringBuilder sb = new StringBuilder(token.length());
85+
86+
for (int i = 0; i < parts.length; i++) {
87+
if (i > 0) {
88+
if (foundClass && !parts[i].isEmpty() && Character.isUpperCase(parts[i].charAt(0))) {
89+
sb.append('$');
90+
} else {
91+
sb.append('.');
92+
}
93+
}
94+
sb.append(parts[i]);
95+
if (!foundClass && !parts[i].isEmpty() && Character.isUpperCase(parts[i].charAt(0))) {
96+
foundClass = true;
97+
}
98+
}
99+
100+
return sb.toString();
101+
}
102+
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.jdt.refactoring;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
15+
import org.junit.jupiter.api.Test;
16+
17+
/**
18+
* Unit tests for {@link NormalizeUtils#normalizeInnerClasses(String)}.
19+
*/
20+
class NormalizeUtilsTest {
21+
22+
@Test
23+
void normalizeNoInnerClass() {
24+
assertEquals("java.util.Map", NormalizeUtils.normalizeInnerClasses("java.util.Map"));
25+
}
26+
27+
@Test
28+
void normalizeAlreadyDollar() {
29+
assertEquals("java.util.Map$Entry", NormalizeUtils.normalizeInnerClasses("java.util.Map$Entry"));
30+
}
31+
32+
@Test
33+
void normalizeSimpleInnerClass() {
34+
assertEquals("java.util.Map$Entry", NormalizeUtils.normalizeInnerClasses("java.util.Map.Entry"));
35+
}
36+
37+
@Test
38+
void normalizeDeepInnerClass() {
39+
assertEquals("com.example.Outer$Middle$Inner",
40+
NormalizeUtils.normalizeInnerClasses("com.example.Outer.Middle.Inner"));
41+
}
42+
43+
@Test
44+
void normalizeParameterizedInnerClass() {
45+
assertEquals("java.util.Map$Entry<java.lang.String, java.lang.Integer>",
46+
NormalizeUtils.normalizeInnerClasses("java.util.Map.Entry<java.lang.String, java.lang.Integer>"));
47+
}
48+
49+
@Test
50+
void normalizeInnerClassInTypeArgument() {
51+
assertEquals(
52+
"java.util.Map<java.lang.String, java.util.List<java.util.Map$Entry<java.lang.String, ?>>>",
53+
NormalizeUtils.normalizeInnerClasses(
54+
"java.util.Map<java.lang.String, java.util.List<java.util.Map.Entry<java.lang.String, ?>>>"));
55+
}
56+
57+
@Test
58+
void normalizeInnerClassArray() {
59+
assertEquals("java.util.Map$Entry[]",
60+
NormalizeUtils.normalizeInnerClasses("java.util.Map.Entry[]"));
61+
}
62+
63+
@Test
64+
void normalizeTopLevelClassUnchanged() {
65+
assertEquals("com.example.MyService", NormalizeUtils.normalizeInnerClasses("com.example.MyService"));
66+
}
67+
68+
@Test
69+
void normalizePrimitiveUnchanged() {
70+
assertEquals("int", NormalizeUtils.normalizeInnerClasses("int"));
71+
}
72+
73+
@Test
74+
void normalizeSimpleParameterizedUnchanged() {
75+
assertEquals("java.util.List<java.lang.String>",
76+
NormalizeUtils.normalizeInnerClasses("java.util.List<java.lang.String>"));
77+
}
78+
79+
@Test
80+
void normalizeEmptyString() {
81+
assertEquals("", NormalizeUtils.normalizeInnerClasses(""));
82+
}
83+
84+
@Test
85+
void normalizeUnqualifiedName() {
86+
assertEquals("MyService", NormalizeUtils.normalizeInnerClasses("MyService"));
87+
}
88+
89+
@Test
90+
void normalizeUnboundedWildcard() {
91+
assertEquals("?", NormalizeUtils.normalizeInnerClasses("?"));
92+
}
93+
94+
@Test
95+
void normalizeWildcardExtendsInnerClass() {
96+
assertEquals("? extends com.example.Outer$Inner",
97+
NormalizeUtils.normalizeInnerClasses("? extends com.example.Outer.Inner"));
98+
}
99+
100+
@Test
101+
void normalizeWildcardSuperInnerClass() {
102+
assertEquals("? super com.example.Outer$Inner",
103+
NormalizeUtils.normalizeInnerClasses("? super com.example.Outer.Inner"));
104+
}
105+
106+
@Test
107+
void normalizeMultiDimensionalInnerClassArray() {
108+
assertEquals("java.util.Map$Entry[][]",
109+
NormalizeUtils.normalizeInnerClasses("java.util.Map.Entry[][]"));
110+
}
111+
112+
@Test
113+
void normalizeInnerClassBothOuterAndTypeArgument() {
114+
assertEquals("com.example.Outer$Inner<com.example.Foo$Bar>",
115+
NormalizeUtils.normalizeInnerClasses("com.example.Outer.Inner<com.example.Foo.Bar>"));
116+
}
117+
118+
@Test
119+
void normalizeParameterizedInnerClassArray() {
120+
assertEquals("java.util.Map$Entry<java.lang.String, java.lang.Integer>[]",
121+
NormalizeUtils.normalizeInnerClasses("java.util.Map.Entry<java.lang.String, java.lang.Integer>[]"));
122+
}
123+
124+
}

0 commit comments

Comments
 (0)