Skip to content

Commit 25cdf8d

Browse files
committed
Release 0.3.14
1 parent 9f88426 commit 25cdf8d

7 files changed

Lines changed: 485 additions & 80 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
<properties>
3131
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
32-
<revision>0.3.13</revision>
32+
<revision>0.3.14</revision>
3333
<maven.compiler.release>17</maven.compiler.release>
3434
<picocli.version>4.7.7</picocli.version>
3535
<javaparser.version>3.27.1</javaparser.version>

src/main/java/com/jaipilot/cli/JaiPilotVersionProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public static String resolveVersion() {
1313
Package commandPackage = JaiPilotCli.class.getPackage();
1414
String version = commandPackage == null ? null : commandPackage.getImplementationVersion();
1515
if (version == null || version.isBlank()) {
16-
version = "0.3.13";
16+
version = "0.3.14";
1717
}
1818
return version;
1919
}

src/main/java/com/jaipilot/cli/classpath/BuildToolClassResolutionService.java

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package com.jaipilot.cli.classpath;
22

3+
import java.io.IOException;
4+
import java.nio.charset.StandardCharsets;
5+
import java.nio.file.Files;
36
import java.nio.file.Path;
7+
import java.util.ArrayList;
8+
import java.util.LinkedHashSet;
9+
import java.util.List;
410
import java.util.Optional;
511
import java.util.Set;
612
import java.util.concurrent.ConcurrentHashMap;
@@ -111,4 +117,204 @@ public ResolvedSource resolveSourceOrThrow(
111117
"Source was not available."
112118
)));
113119
}
120+
121+
public Optional<ResolvedSource> resolveSourceByFqcn(
122+
String fqcnOrImport,
123+
Path projectRoot,
124+
Path moduleRoot,
125+
ResolutionOptions options
126+
) {
127+
ResolutionOptions normalizedOptions = options == null ? ResolutionOptions.defaults() : options;
128+
String normalizedFqcn = ClassNameParser.normalizeFqcn(fqcnOrImport);
129+
ResolvedClasspath classpath = classpathResolver.resolveTestClasspath(projectRoot, moduleRoot, normalizedOptions);
130+
131+
Optional<ResolvedSource> workspaceSource = resolveExactWorkspaceJava(
132+
normalizedFqcn,
133+
List.copyOf(workspaceSourceRoots(classpath)),
134+
classpath.moduleRoot()
135+
);
136+
if (workspaceSource.isPresent()) {
137+
return workspaceSource;
138+
}
139+
140+
Optional<ResolvedSource> generatedSource = resolveExactWorkspaceJava(
141+
normalizedFqcn,
142+
generatedSourceRoots(classpath.moduleRoot()),
143+
classpath.moduleRoot()
144+
);
145+
if (generatedSource.isPresent()) {
146+
return generatedSource;
147+
}
148+
149+
for (String candidateBinaryName : binaryNameCandidates(normalizedFqcn)) {
150+
ClassResolutionResult classResult = locate(
151+
candidateBinaryName,
152+
projectRoot,
153+
moduleRoot,
154+
normalizedOptions
155+
);
156+
if (classResult.kind() == LocationKind.NOT_FOUND) {
157+
continue;
158+
}
159+
Optional<ResolvedSource> resolved = resolveSource(classResult, projectRoot, moduleRoot, normalizedOptions);
160+
if (resolved.isPresent()) {
161+
return resolved;
162+
}
163+
}
164+
return Optional.empty();
165+
}
166+
167+
private Optional<ResolvedSource> resolveExactWorkspaceJava(
168+
String normalizedFqcn,
169+
List<Path> roots,
170+
Path moduleRoot
171+
) {
172+
for (String sourceEntry : sourceEntryCandidates(normalizedFqcn)) {
173+
for (Path root : roots) {
174+
Path candidate = root.resolve(sourceEntry).toAbsolutePath().normalize();
175+
if (!Files.isRegularFile(candidate)) {
176+
continue;
177+
}
178+
return Optional.of(new ResolvedSource(
179+
normalizedFqcn,
180+
SourceOrigin.WORKSPACE_FILE,
181+
candidate,
182+
readSourceFile(candidate, moduleRoot)
183+
));
184+
}
185+
}
186+
return Optional.empty();
187+
}
188+
189+
private List<Path> workspaceSourceRoots(ResolvedClasspath classpath) {
190+
LinkedHashSet<Path> roots = new LinkedHashSet<>();
191+
roots.addAll(classpath.mainSourceRoots());
192+
roots.addAll(classpath.testSourceRoots());
193+
return List.copyOf(roots);
194+
}
195+
196+
private List<Path> generatedSourceRoots(Path moduleRoot) {
197+
if (moduleRoot == null) {
198+
return List.of();
199+
}
200+
201+
LinkedHashSet<Path> roots = new LinkedHashSet<>();
202+
roots.add(moduleRoot.resolve("target/generated-sources"));
203+
roots.add(moduleRoot.resolve("target/generated-test-sources"));
204+
roots.add(moduleRoot.resolve("target"));
205+
roots.add(moduleRoot.resolve("build/generated/sources"));
206+
roots.add(moduleRoot.resolve("build/generated/test-sources"));
207+
roots.add(moduleRoot.resolve("build/generated"));
208+
return roots.stream()
209+
.map(Path::toAbsolutePath)
210+
.map(Path::normalize)
211+
.toList();
212+
}
213+
214+
private List<String> sourceEntryCandidates(String normalizedFqcn) {
215+
LinkedHashSet<String> candidates = new LinkedHashSet<>();
216+
candidates.add(normalizedFqcn.replace('.', '/') + ".java");
217+
218+
int firstDollar = normalizedFqcn.indexOf('$');
219+
if (firstDollar > 0) {
220+
String outer = normalizedFqcn.substring(0, firstDollar);
221+
candidates.add(outer.replace('.', '/') + ".java");
222+
}
223+
224+
String current = normalizedFqcn;
225+
while (true) {
226+
int lastDot = current.lastIndexOf('.');
227+
if (lastDot <= 0) {
228+
break;
229+
}
230+
String outer = current.substring(0, lastDot);
231+
String segment = lastSegment(outer);
232+
if (segment.isEmpty() || !Character.isUpperCase(segment.charAt(0))) {
233+
break;
234+
}
235+
candidates.add(outer.replace('.', '/') + ".java");
236+
current = outer;
237+
}
238+
return List.copyOf(candidates);
239+
}
240+
241+
private List<String> binaryNameCandidates(String normalizedFqcn) {
242+
LinkedHashSet<String> candidates = new LinkedHashSet<>();
243+
candidates.add(normalizedFqcn);
244+
245+
int firstDollar = normalizedFqcn.indexOf('$');
246+
if (firstDollar > 0) {
247+
candidates.add(normalizedFqcn.substring(0, firstDollar));
248+
}
249+
250+
List<Integer> trailingInnerBoundaries = trailingInnerBoundaries(normalizedFqcn);
251+
for (int depth = 1; depth <= trailingInnerBoundaries.size(); depth++) {
252+
char[] chars = normalizedFqcn.toCharArray();
253+
for (int index = 0; index < depth; index++) {
254+
chars[trailingInnerBoundaries.get(index)] = '$';
255+
}
256+
candidates.add(new String(chars));
257+
}
258+
259+
String current = normalizedFqcn;
260+
while (true) {
261+
int lastDot = current.lastIndexOf('.');
262+
if (lastDot <= 0) {
263+
break;
264+
}
265+
String outer = current.substring(0, lastDot);
266+
String segment = lastSegment(outer);
267+
if (segment.isEmpty() || !Character.isUpperCase(segment.charAt(0))) {
268+
break;
269+
}
270+
candidates.add(outer);
271+
current = outer;
272+
}
273+
return List.copyOf(candidates);
274+
}
275+
276+
private List<Integer> trailingInnerBoundaries(String normalizedFqcn) {
277+
List<Integer> boundaries = new ArrayList<>();
278+
int searchFrom = normalizedFqcn.length() - 1;
279+
while (true) {
280+
int boundary = normalizedFqcn.lastIndexOf('.', searchFrom);
281+
if (boundary <= 0) {
282+
break;
283+
}
284+
String segmentBeforeBoundary = segmentBefore(normalizedFqcn, boundary);
285+
if (segmentBeforeBoundary.isEmpty() || !Character.isUpperCase(segmentBeforeBoundary.charAt(0))) {
286+
break;
287+
}
288+
boundaries.add(boundary);
289+
searchFrom = boundary - 1;
290+
}
291+
return boundaries;
292+
}
293+
294+
private String segmentBefore(String value, int boundaryIndex) {
295+
int previousBoundary = value.lastIndexOf('.', boundaryIndex - 1);
296+
return value.substring(previousBoundary + 1, boundaryIndex);
297+
}
298+
299+
private String lastSegment(String value) {
300+
int boundary = value.lastIndexOf('.');
301+
if (boundary < 0) {
302+
return value;
303+
}
304+
return value.substring(boundary + 1);
305+
}
306+
307+
private String readSourceFile(Path sourcePath, Path moduleRoot) {
308+
try {
309+
return Files.readString(sourcePath, StandardCharsets.UTF_8);
310+
} catch (IOException exception) {
311+
throw new ClasspathResolutionException(new ResolutionFailure(
312+
ResolutionFailureCategory.SOURCE_NOT_AVAILABLE,
313+
null,
314+
moduleRoot,
315+
"read source " + sourcePath,
316+
exception.getMessage()
317+
), exception);
318+
}
319+
}
114320
}

src/main/java/com/jaipilot/cli/files/ProjectFileService.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import com.jaipilot.cli.classpath.BuildToolClassResolutionService;
44
import com.jaipilot.cli.classpath.ClasspathResolutionException;
5-
import com.jaipilot.cli.classpath.ClassResolutionResult;
6-
import com.jaipilot.cli.classpath.LocationKind;
75
import com.jaipilot.cli.classpath.ResolutionOptions;
86
import com.jaipilot.cli.classpath.ResolutionFailure;
97
import com.jaipilot.cli.classpath.ResolvedSource;
@@ -28,6 +26,8 @@
2826

2927
public final class ProjectFileService {
3028

29+
private static final System.Logger LOGGER = System.getLogger(ProjectFileService.class.getName());
30+
3131
private static final List<Path> JAVA_SOURCE_ROOTS = List.of(
3232
Path.of("src", "main", "java"),
3333
Path.of("src", "test", "java")
@@ -239,11 +239,28 @@ public List<String> readRequestedContextSources(Path projectRoot, List<String> r
239239
}
240240

241241
public List<String> readRequestedContextSources(Path projectRoot, Path preferredSourcePath, List<String> requestedPaths) {
242+
if (requestedPaths == null || requestedPaths.isEmpty()) {
243+
return List.of();
244+
}
242245
return requestedPaths.stream()
243-
.map(path -> readRequestedContextSource(projectRoot, preferredSourcePath, path))
246+
.map(path -> readRequestedContextSourceOrPlaceholder(projectRoot, preferredSourcePath, path))
244247
.toList();
245248
}
246249

250+
private String readRequestedContextSourceOrPlaceholder(Path projectRoot, Path preferredSourcePath, String requestedPath) {
251+
try {
252+
return readRequestedContextSource(projectRoot, preferredSourcePath, requestedPath);
253+
} catch (IllegalStateException exception) {
254+
LOGGER.log(
255+
System.Logger.Level.WARNING,
256+
"Context source unavailable for {0}; using placeholder. Reason: {1}",
257+
requestedPath,
258+
exception.getMessage()
259+
);
260+
return "class not found";
261+
}
262+
}
263+
247264
public List<String> readCachedContextEntries(Path projectRoot, List<String> contextPaths) {
248265
if (contextPaths == null || contextPaths.isEmpty()) {
249266
return List.of();
@@ -924,21 +941,12 @@ private static ContextSourceResolver defaultContextSourceResolver() {
924941
BuildToolClassResolutionService classResolutionService = new BuildToolClassResolutionService();
925942
return (projectRoot, moduleRoot, requestedFqcn) -> {
926943
ResolutionOptions options = new ResolutionOptions(List.of(), false, true);
927-
ClassResolutionResult classResult = classResolutionService.locate(
944+
Optional<ResolvedSource> resolvedSource = classResolutionService.resolveSourceByFqcn(
928945
requestedFqcn,
929946
projectRoot,
930947
moduleRoot,
931948
options
932949
);
933-
if (classResult.kind() == LocationKind.NOT_FOUND) {
934-
return Optional.empty();
935-
}
936-
Optional<ResolvedSource> resolvedSource = classResolutionService.resolveSource(
937-
classResult,
938-
projectRoot,
939-
moduleRoot,
940-
options
941-
);
942950
if (resolvedSource.isEmpty()) {
943951
return Optional.empty();
944952
}

0 commit comments

Comments
 (0)