Skip to content

Commit a4bd431

Browse files
authored
feat(jdk-codemodel): resolve identifier symbols via javac element API (#22)
1 parent 56f6f5d commit a4bd431

3 files changed

Lines changed: 311 additions & 1 deletion

File tree

jdk-codemodel/src/main/java/build/codemodel/jdk/JdkExpressionConverter.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import build.codemodel.jdk.expression.PrefixOperator;
6969
import build.codemodel.jdk.expression.PrefixUnary;
7070
import build.codemodel.jdk.expression.SwitchExpression;
71+
import build.codemodel.jdk.expression.Symbol;
7172
import build.codemodel.jdk.expression.Ternary;
7273
import build.codemodel.jdk.expression.UnknownExpression;
7374
import build.codemodel.jdk.statement.ExpressionStatement;
@@ -102,6 +103,7 @@
102103
import java.util.List;
103104
import java.util.Optional;
104105
import java.util.function.Function;
106+
import javax.lang.model.element.Element;
105107
import javax.lang.model.type.TypeKind;
106108
import javax.lang.model.type.TypeMirror;
107109

@@ -218,7 +220,43 @@ public Expression visitLiteral(final LiteralTree t, final Void v) {
218220

219221
@Override
220222
public Expression visitIdentifier(final IdentifierTree t, final Void v) {
221-
return Identifier.of(codeModel, t.getName().toString());
223+
final var identifier = Identifier.of(codeModel, t.getName().toString());
224+
resolveSymbol(t).ifPresent(identifier::addTrait);
225+
return identifier;
226+
}
227+
228+
private Optional<Symbol> resolveSymbol(final IdentifierTree t) {
229+
if (trees == null || compilationUnit == null) {
230+
return Optional.empty();
231+
}
232+
final var name = t.getName().toString();
233+
final var path = trees.getPath(compilationUnit, t);
234+
if (path == null) {
235+
return Optional.empty();
236+
}
237+
final var typeMirror = trees.getTypeMirror(path);
238+
final TypeUsage typeUsage = typeMirror != null && typeMirror.getKind() != TypeKind.ERROR
239+
? typeResolver.apply(typeMirror)
240+
: UnknownTypeUsage.create(codeModel);
241+
242+
if ("this".equals(name)) {
243+
return Optional.of(new Symbol.ThisReference(typeUsage));
244+
}
245+
if ("super".equals(name)) {
246+
return Optional.of(new Symbol.SuperReference(typeUsage));
247+
}
248+
249+
final Element element = trees.getElement(path);
250+
if (element == null) {
251+
return Optional.empty();
252+
}
253+
return Optional.ofNullable(switch (element.getKind()) {
254+
case LOCAL_VARIABLE -> new Symbol.LocalVariable(typeUsage);
255+
case PARAMETER -> new Symbol.Parameter(typeUsage);
256+
case FIELD, ENUM_CONSTANT -> new Symbol.Field(typeUsage);
257+
case CLASS, INTERFACE, ENUM, ANNOTATION_TYPE, RECORD -> new Symbol.TypeReference(typeUsage);
258+
default -> null;
259+
});
222260
}
223261

224262
@Override
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package build.codemodel.jdk.expression;
2+
3+
/*-
4+
* #%L
5+
* JDK Code Model
6+
* %%
7+
* Copyright (C) 2026 Workday, Inc.
8+
* %%
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* #L%
21+
*/
22+
23+
import build.codemodel.foundation.descriptor.Singular;
24+
import build.codemodel.foundation.descriptor.Trait;
25+
import build.codemodel.foundation.usage.TypeUsage;
26+
27+
/**
28+
* A {@link Trait} on an {@link Identifier} that captures the result of javac symbol resolution:
29+
* what the identifier refers to and, for variable-kind symbols, its declared type.
30+
*
31+
* <p>Exactly one {@link Symbol} may be present on an {@link Identifier} ({@link Singular}).
32+
*
33+
* @author reed.vonredwitz
34+
* @since Apr-2026
35+
*/
36+
@Singular
37+
public sealed interface Symbol extends Trait
38+
permits Symbol.LocalVariable, Symbol.Parameter, Symbol.Field,
39+
Symbol.TypeReference, Symbol.ThisReference, Symbol.SuperReference {
40+
41+
/**
42+
* A local variable reference, e.g. {@code x} in {@code int x = 1; return x;}.
43+
*
44+
* @param declaredType the declared type of the variable
45+
*/
46+
record LocalVariable(TypeUsage declaredType) implements Symbol {}
47+
48+
/**
49+
* A method or constructor parameter reference.
50+
*
51+
* @param declaredType the declared type of the parameter
52+
*/
53+
record Parameter(TypeUsage declaredType) implements Symbol {}
54+
55+
/**
56+
* A field reference, e.g. {@code field} in {@code this.field} or a bare {@code field}.
57+
*
58+
* @param declaredType the declared type of the field
59+
*/
60+
record Field(TypeUsage declaredType) implements Symbol {}
61+
62+
/**
63+
* A type name reference, e.g. {@code String} in {@code String.valueOf(...)}.
64+
*
65+
* @param type the type being referenced
66+
*/
67+
record TypeReference(TypeUsage type) implements Symbol {}
68+
69+
/**
70+
* A {@code this} reference.
71+
*
72+
* @param declaredType the type of the enclosing instance
73+
*/
74+
record ThisReference(TypeUsage declaredType) implements Symbol {}
75+
76+
/**
77+
* A {@code super} reference.
78+
*
79+
* @param declaredType the type of the direct superclass
80+
*/
81+
record SuperReference(TypeUsage declaredType) implements Symbol {}
82+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package build.codemodel.jdk;
2+
3+
import build.codemodel.foundation.usage.NamedTypeUsage;
4+
import build.codemodel.imperative.Return;
5+
import build.codemodel.jdk.descriptor.MethodBodyDescriptor;
6+
import build.codemodel.jdk.expression.Identifier;
7+
import build.codemodel.jdk.expression.Symbol;
8+
import build.codemodel.objectoriented.descriptor.MethodDescriptor;
9+
import com.google.testing.compile.JavaFileObjects;
10+
import org.junit.jupiter.api.Test;
11+
12+
import java.util.List;
13+
import java.util.Optional;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
/**
18+
* Tests for {@link Symbol} resolution on {@link Identifier} expressions.
19+
*
20+
* @author reed.vonredwitz
21+
* @since Apr-2026
22+
*/
23+
class SymbolResolutionTests {
24+
25+
@Test
26+
void shouldResolveLocalVariable() {
27+
final var source = JavaFileObjects.forSourceString("com.example.Foo", """
28+
package com.example;
29+
public class Foo {
30+
public String bar() {
31+
String result = "hello";
32+
return result;
33+
}
34+
}
35+
""");
36+
final var codeModel = JdkInitializerTests.runInternal(
37+
new JdkInitializer(List.of(), List.of(), List.of(source)));
38+
39+
final var typeName = codeModel.getNameProvider().getTypeName(Optional.empty(), "com.example.Foo");
40+
final var descriptor = codeModel.getTypeDescriptor(typeName).orElseThrow();
41+
final var method = descriptor.traits(MethodDescriptor.class)
42+
.filter(m -> m.methodName().name().toString().equals("bar"))
43+
.findFirst().orElseThrow();
44+
final var body = method.getTrait(MethodBodyDescriptor.class).orElseThrow().body();
45+
46+
final var ret = body.statements()
47+
.filter(s -> s instanceof Return)
48+
.map(s -> (Return) s)
49+
.findFirst().orElseThrow();
50+
51+
final var identifier = (Identifier) ret.expression().orElseThrow();
52+
assertThat(identifier.name()).isEqualTo("result");
53+
54+
final var symbol = identifier.getTrait(Symbol.class).orElseThrow();
55+
assertThat(symbol).isInstanceOf(Symbol.LocalVariable.class);
56+
final var local = (Symbol.LocalVariable) symbol;
57+
assertThat(local.declaredType()).isInstanceOf(NamedTypeUsage.class);
58+
assertThat(local.declaredType().toString()).contains("String");
59+
}
60+
61+
@Test
62+
void shouldResolveParameter() {
63+
final var source = JavaFileObjects.forSourceString("com.example.Foo", """
64+
package com.example;
65+
public class Foo {
66+
public String bar(String input) {
67+
return input;
68+
}
69+
}
70+
""");
71+
final var codeModel = JdkInitializerTests.runInternal(
72+
new JdkInitializer(List.of(), List.of(), List.of(source)));
73+
74+
final var typeName = codeModel.getNameProvider().getTypeName(Optional.empty(), "com.example.Foo");
75+
final var descriptor = codeModel.getTypeDescriptor(typeName).orElseThrow();
76+
final var method = descriptor.traits(MethodDescriptor.class)
77+
.filter(m -> m.methodName().name().toString().equals("bar"))
78+
.findFirst().orElseThrow();
79+
final var body = method.getTrait(MethodBodyDescriptor.class).orElseThrow().body();
80+
81+
final var ret = body.statements()
82+
.filter(s -> s instanceof Return)
83+
.map(s -> (Return) s)
84+
.findFirst().orElseThrow();
85+
86+
final var identifier = (Identifier) ret.expression().orElseThrow();
87+
final var symbol = identifier.getTrait(Symbol.class).orElseThrow();
88+
assertThat(symbol).isInstanceOf(Symbol.Parameter.class);
89+
assertThat(((Symbol.Parameter) symbol).declaredType().toString()).contains("String");
90+
}
91+
92+
@Test
93+
void shouldResolveField() {
94+
final var source = JavaFileObjects.forSourceString("com.example.Foo", """
95+
package com.example;
96+
public class Foo {
97+
private String value = "hello";
98+
public String bar() {
99+
return value;
100+
}
101+
}
102+
""");
103+
final var codeModel = JdkInitializerTests.runInternal(
104+
new JdkInitializer(List.of(), List.of(), List.of(source)));
105+
106+
final var typeName = codeModel.getNameProvider().getTypeName(Optional.empty(), "com.example.Foo");
107+
final var descriptor = codeModel.getTypeDescriptor(typeName).orElseThrow();
108+
final var method = descriptor.traits(MethodDescriptor.class)
109+
.filter(m -> m.methodName().name().toString().equals("bar"))
110+
.findFirst().orElseThrow();
111+
final var body = method.getTrait(MethodBodyDescriptor.class).orElseThrow().body();
112+
113+
final var ret = body.statements()
114+
.filter(s -> s instanceof Return)
115+
.map(s -> (Return) s)
116+
.findFirst().orElseThrow();
117+
118+
final var identifier = (Identifier) ret.expression().orElseThrow();
119+
final var symbol = identifier.getTrait(Symbol.class).orElseThrow();
120+
assertThat(symbol).isInstanceOf(Symbol.Field.class);
121+
assertThat(((Symbol.Field) symbol).declaredType().toString()).contains("String");
122+
}
123+
124+
@Test
125+
void shouldResolveThisReference() {
126+
final var source = JavaFileObjects.forSourceString("com.example.Foo", """
127+
package com.example;
128+
public class Foo {
129+
public Foo bar() {
130+
return this;
131+
}
132+
}
133+
""");
134+
final var codeModel = JdkInitializerTests.runInternal(
135+
new JdkInitializer(List.of(), List.of(), List.of(source)));
136+
137+
final var typeName = codeModel.getNameProvider().getTypeName(Optional.empty(), "com.example.Foo");
138+
final var descriptor = codeModel.getTypeDescriptor(typeName).orElseThrow();
139+
final var method = descriptor.traits(MethodDescriptor.class)
140+
.filter(m -> m.methodName().name().toString().equals("bar"))
141+
.findFirst().orElseThrow();
142+
final var body = method.getTrait(MethodBodyDescriptor.class).orElseThrow().body();
143+
144+
final var ret = body.statements()
145+
.filter(s -> s instanceof Return)
146+
.map(s -> (Return) s)
147+
.findFirst().orElseThrow();
148+
149+
final var identifier = (Identifier) ret.expression().orElseThrow();
150+
final var symbol = identifier.getTrait(Symbol.class).orElseThrow();
151+
assertThat(symbol).isInstanceOf(Symbol.ThisReference.class);
152+
assertThat(((Symbol.ThisReference) symbol).declaredType().toString()).contains("Foo");
153+
}
154+
155+
@Test
156+
void shouldResolveTypeReference() {
157+
final var source = JavaFileObjects.forSourceString("com.example.Foo", """
158+
package com.example;
159+
public class Foo {
160+
public String bar() {
161+
return String.valueOf(42);
162+
}
163+
}
164+
""");
165+
final var codeModel = JdkInitializerTests.runInternal(
166+
new JdkInitializer(List.of(), List.of(), List.of(source)));
167+
168+
final var typeName = codeModel.getNameProvider().getTypeName(Optional.empty(), "com.example.Foo");
169+
final var descriptor = codeModel.getTypeDescriptor(typeName).orElseThrow();
170+
final var method = descriptor.traits(MethodDescriptor.class)
171+
.filter(m -> m.methodName().name().toString().equals("bar"))
172+
.findFirst().orElseThrow();
173+
final var body = method.getTrait(MethodBodyDescriptor.class).orElseThrow().body();
174+
175+
final var ret = body.statements()
176+
.filter(s -> s instanceof Return)
177+
.map(s -> (Return) s)
178+
.findFirst().orElseThrow();
179+
180+
// String.valueOf(42) is a MethodInvocation whose receiver is a MemberSelect on "String"
181+
// The MemberSelect target is an Identifier("String") tagged as TypeReference
182+
final var methodInvocation = ret.expression().orElseThrow();
183+
assertThat(methodInvocation).isInstanceOf(build.codemodel.jdk.expression.MethodInvocation.class);
184+
final var invocation = (build.codemodel.jdk.expression.MethodInvocation) methodInvocation;
185+
final var receiver = (Identifier) invocation.target().orElseThrow();
186+
assertThat(receiver.name()).isEqualTo("String");
187+
final var symbol = receiver.getTrait(Symbol.class).orElseThrow();
188+
assertThat(symbol).isInstanceOf(Symbol.TypeReference.class);
189+
}
190+
}

0 commit comments

Comments
 (0)