Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion com.vogella.ide.debugtools/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Require-Bundle: org.eclipse.jface,
org.eclipse.e4.core.di.annotations,
org.eclipse.core.resources;bundle-version="3.23.100",
org.eclipse.core.runtime;bundle-version="3.34.100",
org.eclipse.pde.core;bundle-version="3.21.100"
org.eclipse.pde.core;bundle-version="3.21.100",
org.eclipse.ui.console;bundle-version="3.15.0",
org.eclipse.ui;bundle-version="3.207.400"
Bundle-RequiredExecutionEnvironment: JavaSE-21
Import-Package: jakarta.annotation;version="[2.1.0,3.0.0)",
jakarta.inject;version="[2.0.0,3.0.0)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,63 @@
import org.eclipse.pde.core.plugin.*;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.osgi.service.resolver.*;
import org.eclipse.ui.console.*; // Requires 'org.eclipse.ui.console' dependency
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;

/**
* Eclipse e4 Handler to detect cyclic dependencies between plug-ins in the workspace.
* Detects cycles from both Require-Bundle and Import-Package dependencies.
*/
public class DetectCyclicDependenciesHandler {

private static final String CONSOLE_NAME = "Cyclic Dependency Analysis";

@Execute
public void execute(@Named(IServiceConstants.ACTIVE_SHELL) Shell shell) {
try {
CyclicDependencyDetector detector = new CyclicDependencyDetector();
List<CycleInfo> cycles = detector.detectCycles();

// Clear and prepare the console
MessageConsole console = findConsole(CONSOLE_NAME);
console.clearConsole();
MessageConsoleStream out = console.newMessageStream();

// Bring Console View to front
showConsoleView(console);

if (cycles.isEmpty()) {
out.println("No cyclic dependencies found in workspace plug-ins.");
MessageDialog.openInformation(shell, "Cyclic Dependencies",
"No cyclic dependencies found in workspace plug-ins.");
} else {
StringBuilder message = new StringBuilder();
message.append("Found ").append(cycles.size()).append(" cycle(s):\n\n");
StringBuilder dialogMessage = new StringBuilder();
dialogMessage.append("Found ").append(cycles.size()).append(" cycle(s). See Console for details.\n\n");

// Console Header
out.println("=================================================");
out.println(" CYCLIC DEPENDENCIES DETECTED ");
out.println("=================================================");

for (int i = 0; i < cycles.size(); i++) {
CycleInfo cycleInfo = cycles.get(i);
message.append("Cycle ").append(i + 1).append(":\n");
List<String> cycle = cycleInfo.cycle;
for (int j = 0; j < cycle.size() - 1; j++) {
message.append(" ").append(cycle.get(j));
String depType = cycleInfo.getEdgeType(cycle.get(j), cycle.get(j + 1));
message.append(" -[").append(depType).append("]-> \n");
}
message.append("\n");

// 1. Build string for Dialog (Simplified)
dialogMessage.append("Cycle ").append(i + 1).append(": ");
dialogMessage.append(cycleInfo.cycle.get(0)).append(" ...\n");

// 2. Generate and Print ASCII Art to Eclipse Console
out.println("\nCycle " + (i + 1) + ":");
out.println(generateAsciiArt(cycleInfo));
}

out.println("=================================================");

// Show a dialog, but refer them to the console for the big ASCII art
MessageDialog.openWarning(shell, "Cyclic Dependencies Detected",
message.toString());
dialogMessage.toString());
}
} catch (Exception e) {
MessageDialog.openError(shell, "Error",
Expand All @@ -54,13 +77,91 @@ public void execute(@Named(IServiceConstants.ACTIVE_SHELL) Shell shell) {
"com.vogella.ide.debugtools", "Error detecting cycles", e));
}
}

/**
* Generates a vertical ASCII art flow for the cycle.
*/
private String generateAsciiArt(CycleInfo cycleInfo) {
StringBuilder sb = new StringBuilder();
List<String> cycle = cycleInfo.cycle;

int maxLen = 0;
for (String node : cycle) maxLen = Math.max(maxLen, node.length());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop to find the maximum length can be expressed more concisely and efficiently using a Java Stream. This would also naturally handle the case of an empty list, although that's unlikely here.

Suggested change
for (String node : cycle) maxLen = Math.max(maxLen, node.length());
int maxLen = cycle.stream().mapToInt(String::length).max().orElse(0);

int boxWidth = maxLen + 4;

String horizontalBorder = " +" + "-".repeat(boxWidth - 2) + "+";

for (int i = 0; i < cycle.size() - 1; i++) {
String current = cycle.get(i);
String next = cycle.get(i + 1);
String type = cycleInfo.getEdgeType(current, next);
sb.append(horizontalBorder).append("\n");
sb.append(String.format(" | %-" + (boxWidth - 4) + "s |\n", current));
sb.append(horizontalBorder).append("\n");
if (i < cycle.size() - 2) {
sb.append(" |\n");
sb.append(" | [").append(type).append("]\n");
sb.append(" v\n");
} else {
sb.append(" |\n");
sb.append(" | [").append(type).append("]\n");
sb.append(" ^ (Loops back to start)\n");
sb.append(" |______________________|\n");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ASCII art for the loop-back connection uses a hardcoded string (|______________________|). This will not scale visually if the plug-in names are very long, causing the box width to increase significantly. To make the visualization more robust and look better with varying name lengths, consider making this part of the drawing dynamic based on boxWidth.

}
}

return sb.toString();
}

/**
* Holds information about a cycle including the edge types
* Finds or creates the console with the given name.
*/
private MessageConsole findConsole(String name) {
ConsolePlugin plugin = ConsolePlugin.getDefault();
IConsoleManager conMan = plugin.getConsoleManager();
IConsole[] existing = conMan.getConsoles();
for (IConsole console : existing) {
if (name.equals(console.getName())) {
return (MessageConsole) console;
}
}

// No console found, so create a new one
MessageConsole myConsole = new MessageConsole(name, null);
conMan.addConsoles(new IConsole[]{myConsole});
return myConsole;
}

/**
* Forces the Console view to open and display our specific console.
*/
private void showConsoleView(IConsole myConsole) {
try {
org.eclipse.ui.IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
if (window == null) {
Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active window."));
return;
}
IWorkbenchPage page = window.getActivePage();
if (page == null) {
Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active page."));
return;
}
String id = IConsoleConstants.ID_CONSOLE_VIEW;
IConsoleView view = (IConsoleView) page.showView(id);
view.display(myConsole);
} catch (PartInitException e) {
// Log error if view cannot be opened, but don't fail the whole operation
Platform.getLog(getClass()).log(new Status(Status.WARNING,
"com.vogella.ide.debugtools", "Could not open console view", e));
}
}
Comment on lines +138 to +158

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method interacts with the UI and must be called on the UI thread. While the current usage from the @Execute handler is safe, it's a good practice to make UI-interacting methods robust by wrapping their logic in PlatformUI.getWorkbench().getDisplay().asyncExec(). This prevents potential SWTExceptions if the method is ever called from a background thread in the future.

    private void showConsoleView(IConsole myConsole) {
        PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
            try {
                org.eclipse.ui.IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
                if (window == null) {
                    Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active window."));
                    return;
                }
                IWorkbenchPage page = window.getActivePage();
                if (page == null) {
                    Platform.getLog(getClass()).log(new Status(Status.WARNING, "com.vogella.ide.debugtools", "Could not open console view: no active page."));
                    return;
                }
                String id = IConsoleConstants.ID_CONSOLE_VIEW;
                IConsoleView view = (IConsoleView) page.showView(id);
                view.display(myConsole);
            } catch (PartInitException e) {
                // Log error if view cannot be opened, but don't fail the whole operation
                Platform.getLog(getClass()).log(new Status(Status.WARNING, 
                    "com.vogella.ide.debugtools", "Could not open console view", e));
            }
        });
    }


// --- Nested Helper Classes (CycleInfo, CyclicDependencyDetector) remain unchanged ---

private static class CycleInfo {
List<String> cycle;
Map<String, String> edgeTypes; // Key: "from->to", Value: "Require-Bundle" or "Import-Package: pkg.name"
Map<String, String> edgeTypes;

CycleInfo(List<String> cycle) {
this.cycle = cycle;
Expand All @@ -76,9 +177,6 @@ String getEdgeType(String from, String to) {
}
}

/**
* Core logic for detecting cyclic dependencies
*/
private static class CyclicDependencyDetector {
private Map<String, Set<DependencyEdge>> dependencyGraph;
private Set<String> visited;
Expand All @@ -89,7 +187,7 @@ private static class CyclicDependencyDetector {

private static class DependencyEdge {
String target;
String type; // "Require-Bundle" or "Import-Package: package.name"
String type;

DependencyEdge(String target, String type) {
this.target = target;
Expand All @@ -113,18 +211,14 @@ public int hashCode() {
public List<CycleInfo> detectCycles() throws CoreException {
dependencyGraph = new HashMap<>();
cycles = new ArrayList<>();

buildDependencyGraph();
findAllCycles();

return cycles;
}

private void buildDependencyGraph() throws CoreException {
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IProject[] projects = root.getProjects();

// First pass: collect all workspace bundles and their exported packages
Map<String, String> packageToBundle = new HashMap<>();
Map<String, IPluginModelBase> workspaceModels = new HashMap<>();

Expand All @@ -134,8 +228,6 @@ private void buildDependencyGraph() throws CoreException {
if (model != null && model.getBundleDescription() != null) {
String pluginId = model.getPluginBase().getId();
workspaceModels.put(pluginId, model);

// Collect exported packages
BundleDescription bundleDesc = model.getBundleDescription();
ExportPackageDescription[] exports = bundleDesc.getExportPackages();
if (exports != null) {
Expand All @@ -147,15 +239,12 @@ private void buildDependencyGraph() throws CoreException {
}
}

// Second pass: build dependency graph
for (Map.Entry<String, IPluginModelBase> entry : workspaceModels.entrySet()) {
String pluginId = entry.getKey();
IPluginModelBase model = entry.getValue();
Set<DependencyEdge> dependencies = new HashSet<>();

BundleDescription bundleDesc = model.getBundleDescription();
if (bundleDesc != null) {
// Get Require-Bundle dependencies
BundleSpecification[] requiredBundles = bundleDesc.getRequiredBundles();
if (requiredBundles != null) {
for (BundleSpecification spec : requiredBundles) {
Expand All @@ -165,32 +254,23 @@ private void buildDependencyGraph() throws CoreException {
}
}
}

// Get Import-Package dependencies
ImportPackageSpecification[] importedPackages = bundleDesc.getImportPackages();
if (importedPackages != null) {
for (ImportPackageSpecification importSpec : importedPackages) {
String packageName = importSpec.getName();
String providingBundle = packageToBundle.get(packageName);

// Only add if it's a workspace bundle and not self-import
if (providingBundle != null && !providingBundle.equals(pluginId)) {
dependencies.add(new DependencyEdge(
providingBundle,
"Import-Package: " + packageName
));
dependencies.add(new DependencyEdge(providingBundle, "Import-Package: " + packageName));
}
}
}
}

dependencyGraph.put(pluginId, dependencies);
}
}

private void findAllCycles() {
visited = new HashSet<>();

for (String plugin : dependencyGraph.keySet()) {
if (!visited.contains(plugin)) {
recursionStack = new HashSet<>();
Expand All @@ -204,7 +284,6 @@ private void findAllCycles() {
private void detectCycleFromNode(String node) {
visited.add(node);
recursionStack.add(node);

Set<DependencyEdge> dependencies = dependencyGraph.get(node);
if (dependencies != null) {
for (DependencyEdge edge : dependencies) {
Expand All @@ -214,85 +293,58 @@ private void detectCycleFromNode(String node) {
parentEdge.put(dep, edge);
detectCycleFromNode(dep);
} else if (recursionStack.contains(dep)) {
// Cycle detected
CycleInfo cycle = extractCycle(node, dep, edge);
if (!isDuplicateCycle(cycle)) {
cycles.add(cycle);
}
}
}
}

recursionStack.remove(node);
}

private CycleInfo extractCycle(String current, String cycleStart, DependencyEdge finalEdge) {
LinkedList<String> path = new LinkedList<>();
path.addFirst(current); // Start with the node where recursion found cycle

// Reconstruct path from 'current' back to 'cycleStart'
path.addFirst(current);
String node = current;
while (!node.equals(cycleStart)) {
node = parent.get(node);
path.addFirst(node);
}
// Now 'path' is [cycleStart, ..., current]

// Create cycle list for CycleInfo: [cycleStart, ..., current, cycleStart]
List<String> cycleList = new ArrayList<>(path);
cycleList.add(cycleStart); // Close the cycle

cycleList.add(cycleStart);
CycleInfo cycleInfo = new CycleInfo(cycleList);

// Populate edge types for the cycle
// Edges from cycleStart to current
for (int i = 0; i < path.size() - 1; i++) {
String from = path.get(i);
String to = path.get(i + 1);
// The edge that leads to 'to' from 'from'
DependencyEdge edge = parentEdge.get(to);
cycleInfo.addEdge(from, to, edge.type);
}

// The final edge from 'current' back to 'cycleStart'
cycleInfo.addEdge(current, cycleStart, finalEdge.type);

return cycleInfo;
Comment on lines 306 to 324

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method was refactored to be more compact, but in the process, several helpful comments explaining the logic of cycle path reconstruction were removed. While the code is shorter, it's now less self-documenting, making it harder for other developers to understand the algorithm at a glance. Please consider re-adding some of the key comments to improve maintainability. For example, explaining how the path is reconstructed backwards from the current node to the cycleStart node.

}

private boolean isDuplicateCycle(CycleInfo newCycleInfo) {
List<String> normalized = normalizeCycle(newCycleInfo.cycle);

for (CycleInfo existingCycleInfo : cycles) {
List<String> normalizedExisting = normalizeCycle(existingCycleInfo.cycle);
if (normalized.equals(normalizedExisting)) {
return true;
}
if (normalized.equals(normalizedExisting)) return true;
}
return false;
}

private List<String> normalizeCycle(List<String> cycle) {
if (cycle.size() <= 1) return new ArrayList<>(cycle);

// Remove the duplicate last element for comparison
List<String> temp = new ArrayList<>(cycle.subList(0, cycle.size() - 1));

// Find the minimum element
int minIndex = 0;
for (int i = 1; i < temp.size(); i++) {
if (temp.get(i).compareTo(temp.get(minIndex)) < 0) {
minIndex = i;
}
if (temp.get(i).compareTo(temp.get(minIndex)) < 0) minIndex = i;
}

// Rotate to start with minimum element
List<String> normalized = new ArrayList<>();
for (int i = 0; i < temp.size(); i++) {
normalized.add(temp.get((minIndex + i) % temp.size()));
}
normalized.add(normalized.get(0)); // Add back the duplicate to complete the cycle

normalized.add(normalized.get(0));
return normalized;
Comment on lines 336 to 348

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to extractCycle, the explanatory comments in this method have been removed. The logic for normalizing a cycle (finding the lexicographically smallest representation by rotation to avoid duplicate cycle reporting) is not trivial. The removed comments were valuable for understanding the algorithm. Please consider restoring them to aid future maintenance.

}
Comment on lines 296 to 349

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In several methods within CyclicDependencyDetector, such as extractCycle and normalizeCycle, helpful explanatory comments have been removed. This code performs complex graph traversal and data manipulation, and the comments were important for understanding the algorithms used for cycle detection, extraction, and normalization. Their removal reduces the maintainability of the code. Please consider restoring them to help other developers (and your future self) understand the logic more easily.

}
Expand Down
3 changes: 3 additions & 0 deletions updatesite/category.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<bundle id="z.ex.search">
<category name="search_extension"/>
</bundle>
<bundle id="com.vogella.ide.debugtools">
<category name="debug_tools_vogella"/>
</bundle>
<category-def name="vogella_ide_extensions" label="IDE extensions from vogella"/>
<category-def name="vogella_minimal_asciidoc_editor" label="Minimal Asciidoc Editor"/>
<category-def name="search_extension" label="Search Extension">
Expand Down