Skip to content

Commit c295914

Browse files
fix: resolve bug where workspace projects are silently dropped [IDE-1083] (#358)
* fix: resolve bug where workspace projects are silently dropped [IDE-1083] The `add` boolean in `getAccessibleTopLevelProjects()` was never reset to `true` at the start of each loop iteration. Once any project was identified as a sub-project (setting `add = false`), all subsequent independent projects were silently dropped from the workspace folders list. This caused only 1 of N projects to be scanned when a workspace contained both parent/child projects and unrelated independent projects. Additionally, diagnostic logging is added at each filtering stage (not accessible, derived, hidden, sub-project) so that future issues with missing workspace projects can be diagnosed from the logs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: resolve PMD violations in ResourceUtils [IDE-1083] Extract duplicate string literal " path=" to PATH_LOG_PREFIX constant (AvoidDuplicateLiterals) and narrow catch block from Exception to IOException (AvoidCatchingGenericException). Co-authored-by: Cursor <cursoragent@cursor.com> * fix: use debug log level for project filtering messages [IDE-1083] Add logDebug method to SnykLogger (IStatus.OK level) and switch all diagnostic log messages in getAccessibleTopLevelProjects from logInfo to logDebug to avoid noise in production logs. Co-authored-by: Cursor <cursoragent@cursor.com> * Revert "fix: use debug log level for project filtering messages [IDE-1083]" This reverts commit 0549fd1. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5c430bb commit c295914

File tree

2 files changed

+207
-5
lines changed

2 files changed

+207
-5
lines changed

plugin/src/main/java/io/snyk/eclipse/plugin/utils/ResourceUtils.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.snyk.eclipse.plugin.utils;
22

33
import java.io.ByteArrayOutputStream;
4+
import java.io.IOException;
45
import java.net.URL;
56
import java.nio.file.Path;
67
import java.util.ArrayList;
@@ -20,6 +21,8 @@
2021

2122
public class ResourceUtils {
2223

24+
private static final String PATH_LOG_PREFIX = " path=";
25+
2326
private static final Comparator<IProject> projectByPathComparator = new Comparator<IProject>() {
2427
@Override
2528
public int compare(IProject o1, IProject o2) {
@@ -46,7 +49,7 @@ private static byte[] getImageDataFromUrl(URL imageUrl) {
4649
ByteArrayOutputStream output = new ByteArrayOutputStream();
4750
openStream.transferTo(output);
4851
return output.toByteArray();
49-
} catch (Exception e) {
52+
} catch (IOException e) {
5053
SnykLogger.logError(e);
5154
return new byte[0];
5255
}
@@ -73,26 +76,44 @@ public static IProject getProjectByPath(Path path) {
7376
}
7477

7578
public static List<IProject> getAccessibleTopLevelProjects() {
76-
var projects = Arrays.stream(ResourcesPlugin.getWorkspace().getRoot().getProjects()).filter((project) -> {
79+
var allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
80+
SnykLogger.logInfo("Workspace contains " + allProjects.length + " project(s)");
81+
82+
for (IProject project : allProjects) {
83+
var path = getFullPath(project);
84+
if (!project.isAccessible()) {
85+
SnykLogger.logInfo("Project filtered (not accessible): " + project.getName() + PATH_LOG_PREFIX + path);
86+
} else if (project.isDerived()) {
87+
SnykLogger.logInfo("Project filtered (derived): " + project.getName() + PATH_LOG_PREFIX + path);
88+
} else if (project.isHidden()) {
89+
SnykLogger.logInfo("Project filtered (hidden): " + project.getName() + PATH_LOG_PREFIX + path);
90+
}
91+
}
92+
93+
var projects = Arrays.stream(allProjects).filter((project) -> {
7794
return project.isAccessible() && !project.isDerived() && !project.isHidden();
7895
}).sorted(projectByPathComparator).collect(Collectors.toList());
7996

97+
SnykLogger.logInfo("After filtering: " + projects.size() + " accessible project(s)");
98+
8099
Set<IProject> topLevel = new TreeSet<>(projectByPathComparator);
81-
boolean add = true;
82100
for (IProject iProject : projects) {
83101
var projectPath = ResourceUtils.getFullPath(iProject);
102+
boolean isSubProject = false;
84103
for (IProject tp : topLevel) {
85104
var topLevelPath = ResourceUtils.getFullPath(tp);
86105
if (projectPath.startsWith(topLevelPath)) {
87-
add = false;
106+
isSubProject = true;
107+
SnykLogger.logInfo("Project filtered (sub-project of " + tp.getName() + "): " + iProject.getName() + PATH_LOG_PREFIX + projectPath);
88108
break;
89109
}
90110
}
91-
if (add) {
111+
if (!isSubProject) {
92112
topLevel.add(iProject);
93113
}
94114
}
95115

116+
SnykLogger.logInfo("Top-level projects: " + topLevel.stream().map(p -> p.getName() + "=" + getFullPath(p)).collect(Collectors.joining(", ")));
96117
return new ArrayList<>(topLevel);
97118
}
98119
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package io.snyk.eclipse.plugin.utils;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.mockStatic;
7+
import static org.mockito.Mockito.when;
8+
9+
import java.nio.file.Path;
10+
import java.util.List;
11+
12+
import org.eclipse.core.resources.IProject;
13+
import org.eclipse.core.resources.IWorkspace;
14+
import org.eclipse.core.resources.IWorkspaceRoot;
15+
import org.eclipse.core.resources.ResourcesPlugin;
16+
import org.eclipse.core.runtime.IPath;
17+
import org.junit.jupiter.api.Test;
18+
import org.mockito.MockedStatic;
19+
20+
class ResourceUtilsTest {
21+
22+
private IProject createMockProject(String name, String absolutePath) {
23+
IProject project = mock(IProject.class);
24+
when(project.getName()).thenReturn(name);
25+
when(project.isAccessible()).thenReturn(true);
26+
when(project.isDerived()).thenReturn(false);
27+
when(project.isHidden()).thenReturn(false);
28+
29+
IPath location = mock(IPath.class);
30+
when(location.toPath()).thenReturn(Path.of(absolutePath));
31+
when(project.getLocation()).thenReturn(location);
32+
33+
return project;
34+
}
35+
36+
@Test
37+
void testIndependentProjectsAreAllReturned() {
38+
IProject projectA = createMockProject("GCASharedServicesMain",
39+
"/Users/test/github/repo-apc-common/GCASharedServicesMain");
40+
IProject projectB = createMockProject("ei.core.service",
41+
"/Users/test/github/repo-core-services/ei.core.service");
42+
IProject projectC = createMockProject("gc-ap-portfolio-foundation-service",
43+
"/Users/test/github/repo-portfolio-foundation-service/gc-ap-portfolio-foundation-service");
44+
45+
IWorkspaceRoot root = mock(IWorkspaceRoot.class);
46+
when(root.getProjects()).thenReturn(new IProject[] { projectA, projectB, projectC });
47+
48+
IWorkspace workspace = mock(IWorkspace.class);
49+
when(workspace.getRoot()).thenReturn(root);
50+
51+
try (MockedStatic<ResourcesPlugin> resourcesPluginMock = mockStatic(ResourcesPlugin.class);
52+
MockedStatic<SnykLogger> loggerMock = mockStatic(SnykLogger.class)) {
53+
resourcesPluginMock.when(ResourcesPlugin::getWorkspace).thenReturn(workspace);
54+
55+
List<IProject> result = ResourceUtils.getAccessibleTopLevelProjects();
56+
57+
assertEquals(3, result.size(), "All 3 independent projects should be returned");
58+
assertTrue(result.contains(projectA));
59+
assertTrue(result.contains(projectB));
60+
assertTrue(result.contains(projectC));
61+
}
62+
}
63+
64+
@Test
65+
void testSubProjectIsFilteredOut() {
66+
IProject parent = createMockProject("parent", "/Users/test/repos/parent");
67+
IProject child = createMockProject("child", "/Users/test/repos/parent/modules/child");
68+
69+
IWorkspaceRoot root = mock(IWorkspaceRoot.class);
70+
when(root.getProjects()).thenReturn(new IProject[] { parent, child });
71+
72+
IWorkspace workspace = mock(IWorkspace.class);
73+
when(workspace.getRoot()).thenReturn(root);
74+
75+
try (MockedStatic<ResourcesPlugin> resourcesPluginMock = mockStatic(ResourcesPlugin.class);
76+
MockedStatic<SnykLogger> loggerMock = mockStatic(SnykLogger.class)) {
77+
resourcesPluginMock.when(ResourcesPlugin::getWorkspace).thenReturn(workspace);
78+
79+
List<IProject> result = ResourceUtils.getAccessibleTopLevelProjects();
80+
81+
assertEquals(1, result.size(), "Only the parent project should be returned");
82+
assertTrue(result.contains(parent));
83+
}
84+
}
85+
86+
@Test
87+
void testProjectAfterSubProjectIsNotDropped() {
88+
// This is the exact bug scenario from IDE-1083:
89+
// After sorting by path, if a sub-project sets add=false and the flag is never
90+
// reset, all subsequent independent projects are silently dropped.
91+
IProject parent = createMockProject("parent", "/Users/test/repos/aaa-parent");
92+
IProject child = createMockProject("child", "/Users/test/repos/aaa-parent/modules/child");
93+
IProject independent = createMockProject("independent", "/Users/test/repos/zzz-independent");
94+
95+
IWorkspaceRoot root = mock(IWorkspaceRoot.class);
96+
when(root.getProjects()).thenReturn(new IProject[] { parent, child, independent });
97+
98+
IWorkspace workspace = mock(IWorkspace.class);
99+
when(workspace.getRoot()).thenReturn(root);
100+
101+
try (MockedStatic<ResourcesPlugin> resourcesPluginMock = mockStatic(ResourcesPlugin.class);
102+
MockedStatic<SnykLogger> loggerMock = mockStatic(SnykLogger.class)) {
103+
resourcesPluginMock.when(ResourcesPlugin::getWorkspace).thenReturn(workspace);
104+
105+
List<IProject> result = ResourceUtils.getAccessibleTopLevelProjects();
106+
107+
assertEquals(2, result.size(), "Parent and independent project should both be returned");
108+
assertTrue(result.contains(parent), "Parent project must be present");
109+
assertTrue(result.contains(independent), "Independent project must not be dropped after sub-project filtering");
110+
}
111+
}
112+
113+
@Test
114+
void testInaccessibleProjectIsFiltered() {
115+
IProject accessible = createMockProject("accessible", "/Users/test/repos/accessible");
116+
IProject inaccessible = createMockProject("inaccessible", "/Users/test/repos/inaccessible");
117+
when(inaccessible.isAccessible()).thenReturn(false);
118+
119+
IWorkspaceRoot root = mock(IWorkspaceRoot.class);
120+
when(root.getProjects()).thenReturn(new IProject[] { accessible, inaccessible });
121+
122+
IWorkspace workspace = mock(IWorkspace.class);
123+
when(workspace.getRoot()).thenReturn(root);
124+
125+
try (MockedStatic<ResourcesPlugin> resourcesPluginMock = mockStatic(ResourcesPlugin.class);
126+
MockedStatic<SnykLogger> loggerMock = mockStatic(SnykLogger.class)) {
127+
resourcesPluginMock.when(ResourcesPlugin::getWorkspace).thenReturn(workspace);
128+
129+
List<IProject> result = ResourceUtils.getAccessibleTopLevelProjects();
130+
131+
assertEquals(1, result.size());
132+
assertTrue(result.contains(accessible));
133+
}
134+
}
135+
136+
@Test
137+
void testDerivedProjectIsFiltered() {
138+
IProject normal = createMockProject("normal", "/Users/test/repos/normal");
139+
IProject derived = createMockProject("derived", "/Users/test/repos/derived");
140+
when(derived.isDerived()).thenReturn(true);
141+
142+
IWorkspaceRoot root = mock(IWorkspaceRoot.class);
143+
when(root.getProjects()).thenReturn(new IProject[] { normal, derived });
144+
145+
IWorkspace workspace = mock(IWorkspace.class);
146+
when(workspace.getRoot()).thenReturn(root);
147+
148+
try (MockedStatic<ResourcesPlugin> resourcesPluginMock = mockStatic(ResourcesPlugin.class);
149+
MockedStatic<SnykLogger> loggerMock = mockStatic(SnykLogger.class)) {
150+
resourcesPluginMock.when(ResourcesPlugin::getWorkspace).thenReturn(workspace);
151+
152+
List<IProject> result = ResourceUtils.getAccessibleTopLevelProjects();
153+
154+
assertEquals(1, result.size());
155+
assertTrue(result.contains(normal));
156+
}
157+
}
158+
159+
@Test
160+
void testHiddenProjectIsFiltered() {
161+
IProject visible = createMockProject("visible", "/Users/test/repos/visible");
162+
IProject hidden = createMockProject("hidden", "/Users/test/repos/hidden");
163+
when(hidden.isHidden()).thenReturn(true);
164+
165+
IWorkspaceRoot root = mock(IWorkspaceRoot.class);
166+
when(root.getProjects()).thenReturn(new IProject[] { visible, hidden });
167+
168+
IWorkspace workspace = mock(IWorkspace.class);
169+
when(workspace.getRoot()).thenReturn(root);
170+
171+
try (MockedStatic<ResourcesPlugin> resourcesPluginMock = mockStatic(ResourcesPlugin.class);
172+
MockedStatic<SnykLogger> loggerMock = mockStatic(SnykLogger.class)) {
173+
resourcesPluginMock.when(ResourcesPlugin::getWorkspace).thenReturn(workspace);
174+
175+
List<IProject> result = ResourceUtils.getAccessibleTopLevelProjects();
176+
177+
assertEquals(1, result.size());
178+
assertTrue(result.contains(visible));
179+
}
180+
}
181+
}

0 commit comments

Comments
 (0)