Skip to content

Commit 6ae7f7c

Browse files
committed
Release 0.3.11
1 parent f064369 commit 6ae7f7c

9 files changed

Lines changed: 317 additions & 63 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.10</revision>
32+
<revision>0.3.11</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/classpath/ClasspathClassLocator.java

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import java.io.IOException;
44
import java.nio.file.Files;
55
import java.nio.file.Path;
6+
import java.util.ArrayList;
67
import java.util.List;
78
import java.util.Map;
89
import java.util.Optional;
10+
import java.util.Set;
911
import java.util.concurrent.ConcurrentHashMap;
1012
import java.util.zip.ZipEntry;
1113
import java.util.zip.ZipFile;
@@ -16,14 +18,20 @@ public final class ClasspathClassLocator implements ClassLocator {
1618

1719
private final MavenCoordinateExtractor coordinateExtractor;
1820
private final Map<String, Path> classEntryJarMemo;
21+
private final Set<String> duplicateWarningMemo;
1922

2023
public ClasspathClassLocator() {
21-
this(new MavenCoordinateExtractor(), new ConcurrentHashMap<>());
24+
this(new MavenCoordinateExtractor(), new ConcurrentHashMap<>(), ConcurrentHashMap.newKeySet());
2225
}
2326

24-
ClasspathClassLocator(MavenCoordinateExtractor coordinateExtractor, Map<String, Path> classEntryJarMemo) {
27+
ClasspathClassLocator(
28+
MavenCoordinateExtractor coordinateExtractor,
29+
Map<String, Path> classEntryJarMemo,
30+
Set<String> duplicateWarningMemo
31+
) {
2532
this.coordinateExtractor = coordinateExtractor;
2633
this.classEntryJarMemo = classEntryJarMemo;
34+
this.duplicateWarningMemo = duplicateWarningMemo;
2735
}
2836

2937
@Override
@@ -106,21 +114,28 @@ private ClassResolutionResult locateInExternalJars(String fqcn, String classEntr
106114
);
107115
}
108116

117+
List<Path> matches = new ArrayList<>();
109118
for (Path entry : classpath.classpathEntries()) {
110119
if (!Files.isRegularFile(entry) || !entry.getFileName().toString().endsWith(".jar")) {
111120
continue;
112121
}
113122
if (!containsZipEntry(entry, classEntryPath)) {
114123
continue;
115124
}
116-
classEntryJarMemo.put(memoKey, entry.toAbsolutePath().normalize());
125+
matches.add(entry.toAbsolutePath().normalize());
126+
}
127+
128+
if (!matches.isEmpty()) {
129+
Path selectedJar = matches.get(0);
130+
classEntryJarMemo.put(memoKey, selectedJar);
131+
logDuplicateMatchesIfAny(memoKey, fqcn, selectedJar, matches.subList(1, matches.size()));
117132
return new ClassResolutionResult(
118133
fqcn,
119134
LocationKind.EXTERNAL_JAR,
120-
entry.toAbsolutePath().normalize(),
135+
selectedJar,
121136
classEntryPath,
122137
Optional.empty(),
123-
coordinateExtractor.extract(entry)
138+
coordinateExtractor.extract(selectedJar)
124139
);
125140
}
126141

@@ -162,4 +177,37 @@ private void logDuration(long startedAt) {
162177
long durationMillis = (System.nanoTime() - startedAt) / 1_000_000L;
163178
LOGGER.log(System.Logger.Level.DEBUG, "class lookup duration: {0}ms", durationMillis);
164179
}
180+
181+
private void logDuplicateMatchesIfAny(String memoKey, String fqcn, Path selectedJar, List<Path> alternateJars) {
182+
if (alternateJars == null || alternateJars.isEmpty()) {
183+
return;
184+
}
185+
186+
String warningKey = memoKey + "::duplicates";
187+
if (!duplicateWarningMemo.add(warningKey)) {
188+
return;
189+
}
190+
191+
String selected = describeJar(selectedJar);
192+
String alternates = alternateJars.stream()
193+
.map(this::describeJar)
194+
.reduce((left, right) -> left + "; " + right)
195+
.orElse("");
196+
LOGGER.log(
197+
System.Logger.Level.WARNING,
198+
"Multiple classpath matches found for {0}. Using {1}; ignored {2}",
199+
fqcn,
200+
selected,
201+
alternates
202+
);
203+
}
204+
205+
private String describeJar(Path jarPath) {
206+
Optional<MavenCoordinates> coordinates = coordinateExtractor.extract(jarPath);
207+
if (coordinates.isEmpty()) {
208+
return jarPath.toString();
209+
}
210+
MavenCoordinates coordinate = coordinates.get();
211+
return coordinate.groupId() + ":" + coordinate.artifactId() + ":" + coordinate.version() + " (" + jarPath + ")";
212+
}
165213
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ private List<Path> runBuildClasspath(
122122
}
123123

124124
List<String> args = new ArrayList<>(options.buildArgs());
125+
args.add("-Dmdep.includeScope=test");
125126
args.add("-Dmdep.outputFile=" + outputFile);
126127

127128
MavenInvokerClient.MavenExecutionResult executionResult;
@@ -135,7 +136,7 @@ private List<Path> runBuildClasspath(
135136
} catch (MavenInvocationException exception) {
136137
throw invocationFailure(
137138
moduleRoot,
138-
"dependency:build-classpath -Dmdep.outputFile=" + outputFile,
139+
"dependency:build-classpath -Dmdep.includeScope=test -Dmdep.outputFile=" + outputFile,
139140
exception,
140141
ResolutionFailureCategory.CLASSPATH_RESOLUTION_FAILED
141142
);
@@ -145,7 +146,7 @@ private List<Path> runBuildClasspath(
145146
throw commandFailure(
146147
BuildToolType.MAVEN,
147148
moduleRoot,
148-
"dependency:build-classpath -Dmdep.outputFile=" + outputFile,
149+
"dependency:build-classpath -Dmdep.includeScope=test -Dmdep.outputFile=" + outputFile,
149150
executionResult,
150151
ResolutionFailureCategory.CLASSPATH_RESOLUTION_FAILED
151152
);

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

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.jaipilot.cli.files;
22

33
import com.jaipilot.cli.classpath.BuildToolClassResolutionService;
4+
import com.jaipilot.cli.classpath.ClasspathResolutionException;
45
import com.jaipilot.cli.classpath.ClassResolutionResult;
56
import com.jaipilot.cli.classpath.LocationKind;
67
import com.jaipilot.cli.classpath.ResolutionOptions;
8+
import com.jaipilot.cli.classpath.ResolutionFailure;
79
import com.jaipilot.cli.classpath.ResolvedSource;
810
import com.jaipilot.cli.process.BuildTool;
911
import com.jaipilot.cli.util.JavaSourceFormatter;
@@ -311,20 +313,21 @@ private String readRequestedContextSource(Path projectRoot, Path preferredSource
311313
return dependencySource.get().content();
312314
}
313315

314-
Optional<DependencySource> classpathResolvedSource = resolveDependencySourceViaClasspathIfPresent(
315-
projectRoot,
316-
preferredSourcePath,
317-
requestedPath
318-
);
316+
Optional<DependencySource> classpathResolvedSource;
317+
try {
318+
classpathResolvedSource = resolveDependencySourceViaClasspathIfPresent(
319+
projectRoot,
320+
preferredSourcePath,
321+
requestedPath
322+
);
323+
} catch (ClasspathResolutionException exception) {
324+
throw classpathResolutionFailure(requestedPath, exception);
325+
}
319326
if (classpathResolvedSource.isPresent()) {
320327
return classpathResolvedSource.get().content();
321328
}
322329

323-
throw new IllegalStateException(
324-
"Unable to resolve requested context class path " + requestedPath
325-
+ ". Checked workspace sources and dependency sources. "
326-
+ "Ensure the class is on the module test classpath (profiles/build args) and dependency sources are available."
327-
);
330+
throw new IllegalStateException(unresolvedContextMessage(requestedPath));
328331
}
329332

330333
private Optional<DependencySource> resolveDependencySourceViaClasspathIfPresent(
@@ -339,35 +342,31 @@ private Optional<DependencySource> resolveDependencySourceViaClasspathIfPresent(
339342

340343
Path normalizedProjectRoot = projectRoot.toAbsolutePath().normalize();
341344
Path moduleRoot = resolveContextModuleRoot(normalizedProjectRoot, preferredSourcePath);
342-
try {
343-
Optional<ResolvedContextSource> resolved = contextSourceResolver.resolve(
344-
normalizedProjectRoot,
345-
moduleRoot,
346-
requestedFqcn.get()
347-
);
348-
if (resolved.isEmpty()) {
349-
return Optional.empty();
350-
}
351-
String content = resolved.get().content();
352-
String resolvedContextPath = normalizeContextPath(resolved.get().contextPath());
353-
if (!resolvedContextPath.isBlank()) {
354-
dependencySourceContentCache.put(resolvedContextPath, content);
355-
missingDependencySourcePaths.remove(resolvedContextPath);
356-
}
357-
358-
String requestedContextPath = normalizeContextPath(requestedPath);
359-
if (!requestedContextPath.isBlank()) {
360-
dependencySourceContentCache.put(requestedContextPath, content);
361-
missingDependencySourcePaths.remove(requestedContextPath);
362-
}
363-
364-
String contextPath = resolvedContextPath.isBlank()
365-
? contextPathFromFqcn(requestedFqcn.get())
366-
: resolvedContextPath;
367-
return Optional.of(new DependencySource(contextPath, content));
368-
} catch (RuntimeException ignored) {
345+
Optional<ResolvedContextSource> resolved = contextSourceResolver.resolve(
346+
normalizedProjectRoot,
347+
moduleRoot,
348+
requestedFqcn.get()
349+
);
350+
if (resolved.isEmpty()) {
369351
return Optional.empty();
370352
}
353+
String content = resolved.get().content();
354+
String resolvedContextPath = normalizeContextPath(resolved.get().contextPath());
355+
if (!resolvedContextPath.isBlank()) {
356+
dependencySourceContentCache.put(resolvedContextPath, content);
357+
missingDependencySourcePaths.remove(resolvedContextPath);
358+
}
359+
360+
String requestedContextPath = normalizeContextPath(requestedPath);
361+
if (!requestedContextPath.isBlank()) {
362+
dependencySourceContentCache.put(requestedContextPath, content);
363+
missingDependencySourcePaths.remove(requestedContextPath);
364+
}
365+
366+
String contextPath = resolvedContextPath.isBlank()
367+
? contextPathFromFqcn(requestedFqcn.get())
368+
: resolvedContextPath;
369+
return Optional.of(new DependencySource(contextPath, content));
371370
}
372371

373372
private Path resolveContextModuleRoot(Path projectRoot, Path preferredSourcePath) {
@@ -1063,6 +1062,32 @@ private static String firstNonBlank(String... values) {
10631062
return null;
10641063
}
10651064

1065+
private String unresolvedContextMessage(String requestedPath) {
1066+
return "Unable to resolve requested context class path " + requestedPath
1067+
+ ". Checked workspace sources and dependency sources. "
1068+
+ "Ensure the class is on the module test classpath and dependency sources are available.";
1069+
}
1070+
1071+
private IllegalStateException classpathResolutionFailure(
1072+
String requestedPath,
1073+
ClasspathResolutionException exception
1074+
) {
1075+
ResolutionFailure failure = exception.failure();
1076+
if (failure == null) {
1077+
return new IllegalStateException(unresolvedContextMessage(requestedPath), exception);
1078+
}
1079+
1080+
String message = unresolvedContextMessage(requestedPath)
1081+
+ " Classpath resolver failure: "
1082+
+ failure.category()
1083+
+ " [tool=" + failure.buildTool()
1084+
+ ", moduleRoot=" + failure.moduleRoot()
1085+
+ ", action=" + failure.actionSummary()
1086+
+ ", output=" + failure.outputSnippet()
1087+
+ "]";
1088+
return new IllegalStateException(message, exception);
1089+
}
1090+
10661091
@FunctionalInterface
10671092
interface ContextSourceResolver {
10681093
Optional<ResolvedContextSource> resolve(Path projectRoot, Path moduleRoot, String requestedFqcn);

0 commit comments

Comments
 (0)