diff --git a/com.vogella.ide.debugtools/META-INF/MANIFEST.MF b/com.vogella.ide.debugtools/META-INF/MANIFEST.MF index 5914a54..2d55edd 100644 --- a/com.vogella.ide.debugtools/META-INF/MANIFEST.MF +++ b/com.vogella.ide.debugtools/META-INF/MANIFEST.MF @@ -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)", diff --git a/com.vogella.ide.debugtools/src/com/vogella/ide/debugtools/handlers/DetectCyclicDependenciesHandler.java b/com.vogella.ide.debugtools/src/com/vogella/ide/debugtools/handlers/DetectCyclicDependenciesHandler.java index 8be507a..0afd1e1 100644 --- a/com.vogella.ide.debugtools/src/com/vogella/ide/debugtools/handlers/DetectCyclicDependenciesHandler.java +++ b/com.vogella.ide.debugtools/src/com/vogella/ide/debugtools/handlers/DetectCyclicDependenciesHandler.java @@ -12,6 +12,10 @@ 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. @@ -19,33 +23,52 @@ */ 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 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 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", @@ -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 cycle = cycleInfo.cycle; + + int maxLen = 0; + for (String node : cycle) maxLen = Math.max(maxLen, node.length()); + 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"); + } + } + + 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)); + } + } + + // --- Nested Helper Classes (CycleInfo, CyclicDependencyDetector) remain unchanged --- + private static class CycleInfo { List cycle; - Map edgeTypes; // Key: "from->to", Value: "Require-Bundle" or "Import-Package: pkg.name" + Map edgeTypes; CycleInfo(List cycle) { this.cycle = cycle; @@ -76,9 +177,6 @@ String getEdgeType(String from, String to) { } } - /** - * Core logic for detecting cyclic dependencies - */ private static class CyclicDependencyDetector { private Map> dependencyGraph; private Set visited; @@ -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; @@ -113,18 +211,14 @@ public int hashCode() { public List 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 packageToBundle = new HashMap<>(); Map workspaceModels = new HashMap<>(); @@ -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) { @@ -147,15 +239,12 @@ private void buildDependencyGraph() throws CoreException { } } - // Second pass: build dependency graph for (Map.Entry entry : workspaceModels.entrySet()) { String pluginId = entry.getKey(); IPluginModelBase model = entry.getValue(); Set 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) { @@ -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<>(); @@ -204,7 +284,6 @@ private void findAllCycles() { private void detectCycleFromNode(String node) { visited.add(node); recursionStack.add(node); - Set dependencies = dependencyGraph.get(node); if (dependencies != null) { for (DependencyEdge edge : dependencies) { @@ -214,7 +293,6 @@ 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); @@ -222,77 +300,51 @@ private void detectCycleFromNode(String node) { } } } - recursionStack.remove(node); } private CycleInfo extractCycle(String current, String cycleStart, DependencyEdge finalEdge) { LinkedList 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 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; } private boolean isDuplicateCycle(CycleInfo newCycleInfo) { List normalized = normalizeCycle(newCycleInfo.cycle); - for (CycleInfo existingCycleInfo : cycles) { List normalizedExisting = normalizeCycle(existingCycleInfo.cycle); - if (normalized.equals(normalizedExisting)) { - return true; - } + if (normalized.equals(normalizedExisting)) return true; } return false; } private List normalizeCycle(List cycle) { if (cycle.size() <= 1) return new ArrayList<>(cycle); - - // Remove the duplicate last element for comparison List 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 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; } } diff --git a/updatesite/category.xml b/updatesite/category.xml index 0ed0839..9929f38 100644 --- a/updatesite/category.xml +++ b/updatesite/category.xml @@ -9,6 +9,9 @@ + + +