From 805c303715084c6ba236193589b51e1adbd90961 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 31 Mar 2026 15:41:07 +0800 Subject: [PATCH 1/4] fix: export explicit jdeps entries for ECJ compatibility Mark EXPLICIT jdeps classpath entries as exported so downstream projects can resolve types referenced in this project's public API. ECJ aggressively resolves all types in the API chain, while javac may not record them in jdeps of downstream targets. Co-Authored-By: Claude Opus 4.6 --- .../core/model/discovery/JavaAspectsClasspathInfo.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java index 6c166b96..7d5a6c5a 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java @@ -375,6 +375,11 @@ public CompileAndRuntimeClasspath compute() throws CoreException { classpathBuilder.addCompileEntry(entry); } else { entry.getAccessRules().add(new AccessRule(PATTERN_EVERYTHING, IAccessRule.K_ACCESSIBLE)); + // Export EXPLICIT jdeps entries so downstream projects can resolve types + // referenced in this project's public API. This addresses ECJ vs javac differences: + // ECJ aggressively resolves all types in the API chain, while javac may not + // record them in jdeps of downstream targets. + entry.setExported(true); classpathBuilder.addCompileEntry(entry); } } else if (LOG.isDebugEnabled()) { From fed430eaf5346a8f355b3a2cb07061f3aef26d73 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 31 Mar 2026 15:41:43 +0800 Subject: [PATCH 2/4] perf: cache fallback library lookups and index ijar/hjar paths - Register fallback libraries discovered during jdeps resolution to avoid repeated expensive lookups for the same jar across targets. - Index class jars under ijar/hjar paths when no interfaceJar exists, since jdeps files typically reference ijar/hjar paths. Co-Authored-By: Claude Opus 4.6 --- .../discovery/JavaAspectsClasspathInfo.java | 2 ++ .../core/model/discovery/JavaAspectsInfo.java | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java index 7d5a6c5a..2b0d475a 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java @@ -358,6 +358,8 @@ public CompileAndRuntimeClasspath compute() throws CoreException { var libraryArtifact = new LibraryArtifact(artifact, classJar, srcJars); var targetKey = targetLabel != null ? TargetKey.forPlainTarget(targetLabel) : null; library = new BlazeJarLibrary(libraryArtifact, targetKey); + // Register back to avoid repeated fallback for the same jar across multiple targets + aspectsInfo.registerFallbackLibrary(artifact.getRelativePath(), library); } var entry = resolveLibrary(library); if (entry != null) { diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsInfo.java index ea32e190..6be5dbc9 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsInfo.java @@ -261,6 +261,18 @@ private void addLibrary(BlazeJarLibrary library) { var classJar = libraryArtifact.getClassJar(); if (classJar != null) { libraryByJdepsRootRelativePath.put(classJar.getRelativePath(), library); + + // When there is no interfaceJar (e.g., libraries discovered from runtime classpath), + // also index under potential ijar/hjar paths so that jdeps lookups can find them. + // jdeps files typically reference ijar/hjar paths, not class jar paths. + if (interfaceJar == null) { + var classPath = classJar.getRelativePath(); + if (classPath.endsWith(".jar")) { + var base = classPath.substring(0, classPath.length() - ".jar".length()); + libraryByJdepsRootRelativePath.putIfAbsent(base + "-ijar.jar", library); + libraryByJdepsRootRelativePath.putIfAbsent(base + "-hjar.jar", library); + } + } } } @@ -276,6 +288,14 @@ public BlazeJarLibrary getLibraryByJdepsRootRelativePath(String relativePath) { return libraryByJdepsRootRelativePath.get(relativePath); } + /** + * Registers a fallback library discovered during jdeps resolution so that subsequent lookups for the same path + * don't need to repeat the fallback logic. + */ + public void registerFallbackLibrary(String relativePath, BlazeJarLibrary library) { + libraryByJdepsRootRelativePath.putIfAbsent(relativePath, library); + } + /** * Checks if a JAR file comes from an external Bazel repository. *

From 9921802d64226d5a3b03586b26714444d40858ec Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 31 Mar 2026 15:42:00 +0800 Subject: [PATCH 3/4] refactor: always log unresolvable compile jars at warn level Remove the unnecessary LOG.isDebugEnabled() guard around a warn-level log statement so unresolvable compile jars are always reported. Co-Authored-By: Claude Opus 4.6 --- .../eclipse/core/model/discovery/JavaAspectsClasspathInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java index 2b0d475a..6d390eaa 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/JavaAspectsClasspathInfo.java @@ -384,7 +384,7 @@ public CompileAndRuntimeClasspath compute() throws CoreException { entry.setExported(true); classpathBuilder.addCompileEntry(entry); } - } else if (LOG.isDebugEnabled()) { + } else { LOG.warn("Unable to resolve compile jar: {}", jdepsDependency); } } From 931ef786309c430ec752aaa74091104d7669da25 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 31 Mar 2026 15:42:22 +0800 Subject: [PATCH 4/4] fix: compute longest common prefix for sibling packages in empty source roots When a Bazel package contains source files from sibling Java packages (e.g. dto/ and merge/) without files in the root package, findCommonParentPackagePrefix returned null because it only checked if a detected package was a prefix of all others. This caused "an empty package fragment root must map to one Java package" errors. Now falls back to computing the actual longest common prefix path, allowing targets like messier_merger_lib with sources spanning sibling sub-packages to be provisioned correctly. Also adds support for merging multiple targets sharing an empty source root into a single Eclipse project, and collects dependencies from unprovisioned sibling targets for complete classpath computation. Co-Authored-By: Claude Opus 4.6 --- .../discovery/BaseProvisioningStrategy.java | 23 +- .../ProjectPerTargetProvisioningStrategy.java | 262 ++++++++++++++---- 2 files changed, 232 insertions(+), 53 deletions(-) diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java index d37697e7..593a8bdb 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/BaseProvisioningStrategy.java @@ -1262,7 +1262,28 @@ private IPath findCommonParentPackagePrefix(Collection detectedJavaPackag } } - return null; + // none of the detected packages is a prefix of all others (sibling packages case) + // compute the actual longest common prefix path + // e.g. for [com/foo/bar/dto, com/foo/bar/merge] this yields com/foo/bar + IPath commonPrefix = null; + for (IPath path : detectedJavaPackagesForSourceDirectory) { + if (commonPrefix == null) { + commonPrefix = path; + } else { + var minLen = Math.min(commonPrefix.segmentCount(), path.segmentCount()); + var commonLen = 0; + for (var i = 0; i < minLen; i++) { + if (commonPrefix.segment(i).equals(path.segment(i))) { + commonLen = i + 1; + } else { + break; + } + } + commonPrefix = commonPrefix.uptoSegment(commonLen); + } + } + + return (commonPrefix == null) || commonPrefix.isEmpty() ? null : commonPrefix; } private IProject findProjectForLocation(IPath location) { diff --git a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java index bbac2b75..4f17288b 100644 --- a/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java +++ b/bundles/com.salesforce.bazel.eclipse.core/src/com/salesforce/bazel/eclipse/core/model/discovery/ProjectPerTargetProvisioningStrategy.java @@ -1,6 +1,7 @@ package com.salesforce.bazel.eclipse.core.model.discovery; import static java.lang.String.format; +import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; import static org.eclipse.core.runtime.SubMonitor.SUPPRESS_ALL_LABELS; @@ -8,13 +9,15 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.SubMonitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,10 +32,12 @@ import com.salesforce.bazel.eclipse.core.model.BazelTarget; import com.salesforce.bazel.eclipse.core.model.BazelWorkspace; import com.salesforce.bazel.eclipse.core.model.BazelWorkspaceBlazeInfo; +import com.salesforce.bazel.eclipse.core.model.discovery.projects.JavaProjectInfo; import com.salesforce.bazel.eclipse.core.util.trace.TracingSubMonitor; import com.salesforce.bazel.sdk.aspects.intellij.IntellijAspects; import com.salesforce.bazel.sdk.aspects.intellij.IntellijAspects.OutputGroup; import com.salesforce.bazel.sdk.command.BazelBuildWithIntelliJAspectsCommand; +import com.salesforce.bazel.sdk.command.querylight.BazelRuleAttribute; import com.salesforce.bazel.sdk.model.BazelLabel; /** @@ -62,21 +67,17 @@ public class ProjectPerTargetProvisioningStrategy extends BaseProvisioningStrate * packages that may not exist in the workspace. */ private void addPackageForLabel(Label label, BazelWorkspace workspace, Set packages) { - // Skip external packages as they may not have corresponding workspace packages if (label.isExternal()) { - LOG.trace("Skipping external label: {}", label); return; } - - var bazelPackage = workspace.getBazelPackage(new BazelLabel(label)); - - // Only add if the package exists and is accessible - if (!bazelPackage.exists()) { - LOG.trace("Skipping non-existing package label: {}", label); - return; + try { + var bazelPackage = workspace.getBazelPackage(new BazelLabel(label)); + if (bazelPackage.exists()) { + packages.add(bazelPackage); + } + } catch (IllegalArgumentException e) { + LOG.trace("Skipping label not rooted at workspace: {}", label); } - - packages.add(bazelPackage); } /** @@ -93,36 +94,42 @@ private void addPackageForLabel(Label label, BazelWorkspace workspace, Set packages) { + var targetKey = TargetKey.forPlainTarget(target.getLabel().toPrimitive()); + var targetInfo = aspectsInfo.get(targetKey); + + if (targetInfo != null) { + for (var dep : targetInfo.getDependencies()) { + addPackageForLabel(dep.getTargetKey().getLabel(), workspace, packages); + } + + var runtimeClasspath = aspectsInfo.getRuntimeClasspath(targetKey); + if (runtimeClasspath != null) { + for (var jar : runtimeClasspath) { + if (jar.targetKey != null) { + addPackageForLabel(jar.targetKey.getLabel(), workspace, packages); + } + } + } + } + } + private Set collectDependencyPackages(Collection bazelProjects, JavaAspectsInfo aspectsInfo, BazelWorkspace workspace) throws CoreException { Set packages = new HashSet<>(); for (BazelProject bazelProject : bazelProjects) { - // Skip projects that don't have a proper target yet if (!bazelProject.isTargetProject()) { - LOG.trace("Skipping project {} - not a target project or target is null", bazelProject.getName()); continue; } - // Collect all dependency labels from the aspects info - var targetKey = TargetKey.forPlainTarget(bazelProject.getBazelTarget().getLabel().toPrimitive()); - var targetInfo = aspectsInfo.get(targetKey); - - if (targetInfo != null) { - // Collect packages from direct dependencies - for (var dep : targetInfo.getDependencies()) { - addPackageForLabel(dep.getTargetKey().getLabel(), workspace, packages); - } + // Collect deps from the primary target + collectDependencyPackagesForTarget(bazelProject.getBazelTarget(), aspectsInfo, workspace, packages); - // Collect packages from runtime dependencies - var runtimeClasspath = aspectsInfo.getRuntimeClasspath(targetKey); - if (runtimeClasspath != null) { - for (var jar : runtimeClasspath) { - if (jar.targetKey != null) { - addPackageForLabel(jar.targetKey.getLabel(), workspace, packages); - } - } - } + // Also collect deps from unprovisioned sibling targets in the same package + for (BazelTarget sibling : getUnprovisionedSiblingTargets(bazelProject)) { + collectDependencyPackagesForTarget(sibling, aspectsInfo, workspace, packages); } } @@ -136,19 +143,37 @@ public Map computeClasspaths(Collectio try { var monitor = SubMonitor.convert(progress, "Computing Bazel project classpaths", 1 + bazelProjects.size()); + // Build targets list including unprovisioned sibling targets List targetsToBuild = new ArrayList<>(bazelProjects.size()); + Set addedLabels = new HashSet<>(); + Map> siblingTargetsMap = new HashMap<>(); + for (BazelProject bazelProject : bazelProjects) { monitor.checkCanceled(); if (!bazelProject.isTargetProject()) { - throw new CoreException( - Status.error( - format( - "Unable to compute classpath for project '%s'. Please check the setup. This is not a Bazel target project created by the project per target strategy.", - bazelProjects))); + LOG.warn("Skipping non-target project '{}' in classpath computation", bazelProject.getName()); + continue; + } + + // Add the primary target + var primaryLabel = bazelProject.getBazelTarget().getLabel(); + if (addedLabels.add(primaryLabel)) { + targetsToBuild.add(primaryLabel); } - targetsToBuild.add(bazelProject.getBazelTarget().getLabel()); + // Discover and add unprovisioned sibling targets from the same package + var siblings = getUnprovisionedSiblingTargets(bazelProject); + siblingTargetsMap.put(bazelProject, siblings); + for (BazelTarget sibling : siblings) { + if (addedLabels.add(sibling.getLabel())) { + targetsToBuild.add(sibling.getLabel()); + } + } + } + + if (targetsToBuild.isEmpty()) { + return Map.of(); } var workspaceRoot = workspace.getLocation().toPath(); @@ -188,19 +213,27 @@ public Map computeClasspaths(Collectio var aspectsInfo = new JavaAspectsInfo(result, workspace, aspects); // Performance optimization: Pre-load all dependency packages to avoid repeated Bazel queries - // This utilizes the existing batch optimization in BazelWorkspace.open() - var packagesToPreload = collectDependencyPackages(bazelProjects, aspectsInfo, workspace); - if (!packagesToPreload.isEmpty()) { - LOG.debug( - "Pre-loading {} dependency packages to optimize classpath computation", - packagesToPreload.size()); - workspace.open(packagesToPreload); + try { + var packagesToPreload = collectDependencyPackages(bazelProjects, aspectsInfo, workspace); + if (!packagesToPreload.isEmpty()) { + LOG.debug( + "Pre-loading {} dependency packages to optimize classpath computation", + packagesToPreload.size()); + workspace.open(packagesToPreload); + } + } catch (Exception e) { + LOG.warn("Failed to pre-load dependency packages, continuing without optimization", e); } for (BazelProject bazelProject : bazelProjects) { monitor.subTask(bazelProject.getName()); monitor.checkCanceled(); + if (!bazelProject.isTargetProject()) { + monitor.worked(1); + continue; + } + // build index of classpath info var classpathInfo = new JavaAspectsClasspathInfo(aspectsInfo, workspace, availableDependencies, bazelProject); @@ -208,12 +241,21 @@ public Map computeClasspaths(Collectio // remove old marker deleteClasspathContainerProblems(bazelProject); - // add the target + // add the primary target var problem = classpathInfo.addTarget(bazelProject.getBazelTarget()); if (!problem.isOK()) { createClasspathContainerProblem(bazelProject, problem); } + // add unprovisioned sibling targets so their deps are included in the classpath + var siblings = siblingTargetsMap.getOrDefault(bazelProject, List.of()); + for (BazelTarget sibling : siblings) { + var siblingProblem = classpathInfo.addTarget(sibling); + if (!siblingProblem.isOK()) { + createClasspathContainerProblem(bazelProject, siblingProblem); + } + } + // compute the classpath var classpath = classpathInfo.compute(); @@ -229,23 +271,139 @@ public Map computeClasspaths(Collectio } } + private JavaProjectInfo collectJavaInfoForAnalysis(BazelPackage bazelPackage, BazelTarget sampleTarget) + throws CoreException { + var javaInfo = new JavaProjectInfo(bazelPackage); + var attributes = sampleTarget.getRuleAttributes(); + var srcs = attributes.getStringList(BazelRuleAttribute.SRCS); + if (srcs != null) { + for (String src : srcs) { + javaInfo.addSrc(src, null); + } + } + javaInfo.analyzeProjectRecommendations(false, new NullProgressMonitor()); + return javaInfo; + } + @Override protected List doProvisionProjects(Collection targets, TracingSubMonitor monitor) throws CoreException { monitor.setWorkRemaining(targets.size()); List result = new ArrayList<>(); - for (BazelTarget target : targets) { - monitor.subTask(target.getLabel().toString()); - // provision project - var project = provisionProjectForTarget(target, monitor); - if (project != null) { - result.add(project); + var targetsByPackage = targets.stream() + .collect(groupingBy(BazelTarget::getBazelPackage, LinkedHashMap::new, toList())); + + for (var entry : targetsByPackage.entrySet()) { + var bazelPackage = entry.getKey(); + var packageTargets = entry.getValue(); + + if (packageTargets.size() > 1 && hasEmptySourceRoot(bazelPackage, packageTargets)) { + LOG.debug("Detected empty source root for package '{}', merging {} targets into one project", + bazelPackage, packageTargets.size()); + var project = provisionMergedTargetProject(bazelPackage, packageTargets, monitor); + if (project != null) { + result.add(project); + } + } else { + for (BazelTarget target : packageTargets) { + monitor.subTask(target.getLabel().toString()); + var project = provisionProjectForTarget(target, monitor); + if (project != null) { + result.add(project); + } + } } } return result; } + /** + * Returns unprovisioned sibling java_* targets in the same package. For merged projects, these are the targets that + * were skipped during provisioning but whose dependencies should be included in classpath computation. + */ + private List getUnprovisionedSiblingTargets(BazelProject bazelProject) throws CoreException { + var primaryTarget = bazelProject.getBazelTarget(); + var bazelPackage = primaryTarget.getBazelPackage(); + List siblings = new ArrayList<>(); + for (BazelTarget t : bazelPackage.getBazelTargets()) { + if (t.getTargetName().equals(primaryTarget.getTargetName())) { + continue; + } + if (t.hasBazelProject()) { + continue; + } + if (isJavaRule(t.getRuleClass())) { + siblings.add(t); + } + } + return siblings; + } + + /** + * Merges multiple targets sharing an empty source root into a single target project. The primary target (the + * java_library with the most sources) is used for the project identity, but all targets' source files are included. + */ + private BazelProject provisionMergedTargetProject(BazelPackage bazelPackage, List allTargets, + TracingSubMonitor monitor) throws CoreException { + var primaryTarget = selectPrimaryTarget(allTargets); + monitor.subTask(primaryTarget.getLabel().toString()); + monitor = monitor.split(1, "Provisioning merged project for " + primaryTarget.getLabel()); + + var project = provisionTargetProject(primaryTarget, monitor.slice(1)); + + // Build Java info from ALL targets in the package + var javaInfo = collectJavaInfo(project, allTargets, monitor.slice(1)); + + // Configure links and classpath using combined info + linkSourcesIntoProject(project, javaInfo, monitor.slice(1)); + linkGeneratedSourcesIntoProject(project, javaInfo, monitor.slice(1)); + linkJarsIntoProject(project, javaInfo, monitor.slice(1)); + configureRawClasspath(project, javaInfo, monitor.slice(1)); + + return project; + } + + /** + * Selects the primary target from a list of targets. Prefers the java_library with the most source files. + */ + private BazelTarget selectPrimaryTarget(List targets) throws CoreException { + BazelTarget primary = null; + int maxSrcs = -1; + for (BazelTarget target : targets) { + if (!"java_library".equals(target.getRuleClass())) { + continue; + } + var srcs = target.getRuleAttributes().getStringList(BazelRuleAttribute.SRCS); + int count = (srcs != null) ? srcs.size() : 0; + if (count > maxSrcs) { + maxSrcs = count; + primary = target; + } + } + return primary != null ? primary : targets.get(0); + } + + private boolean hasEmptySourceRoot(BazelPackage bazelPackage, List targets) { + for (BazelTarget target : targets) { + try { + var javaInfo = collectJavaInfoForAnalysis(bazelPackage, target); + if (javaInfo.getSourceInfo().hasSourceDirectories() + && javaInfo.getSourceInfo().getSourceDirectories().stream().anyMatch(IPath::isEmpty)) { + return true; + } + } catch (CoreException e) { + LOG.debug("Failed to analyze target '{}': {}", target, e.getMessage()); + } + } + return false; + } + + private boolean isJavaRule(String ruleClass) { + return "java_library".equals(ruleClass) || "java_import".equals(ruleClass) + || "java_binary".equals(ruleClass) || "java_test".equals(ruleClass); + } + protected BazelProject provisionJavaBinaryProject(BazelTarget target, TracingSubMonitor monitor) throws CoreException { // TODO: create a shared launch configuration