content: //Gets all functions within the program and allows one to see the complexity depth of each function. The graph related code has been taken from and inspired by Ghidra's base: https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Base/src/main/java/ghidra/app/cmd/module/ComplexityDepthModularizationCmd.java#L43
  //@author Max 'Libra' Kersten for Trellix
  //@category 
  //@keybinding
  //@menupath
  //@toolbar
  
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.net.HttpURLConnection;
  import java.net.URI;
  import java.net.URL;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  
  import com.google.gson.Gson;
  import com.google.gson.JsonSyntaxException;
  
  import ghidra.app.decompiler.DecompInterface;
  import ghidra.app.decompiler.DecompileResults;
  import ghidra.app.decompiler.DecompiledFunction;
  import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
  import ghidra.app.script.GhidraScript;
  import ghidra.graph.GDirectedGraph;
  import ghidra.graph.GraphAlgorithms;
  import ghidra.graph.GraphFactory;
  import ghidra.program.model.address.Address;
  import ghidra.program.model.block.BasicBlockModel;
  import ghidra.program.model.block.CodeBlock;
  import ghidra.program.model.block.CodeBlockIterator;
  import ghidra.program.model.block.CodeBlockReference;
  import ghidra.program.model.block.CodeBlockReferenceIterator;
  import ghidra.program.model.block.graph.CodeBlockEdge;
  import ghidra.program.model.block.graph.CodeBlockVertex;
  import ghidra.program.model.listing.Function;
  import ghidra.program.model.listing.Variable;
  import ghidra.program.model.symbol.SourceType;
  import ghidra.util.SystemUtilities;
  import ghidra.util.exception.CancelledException;
  
  public class GhidrAI extends GhidraScript {
  
  	/*
  	 * The number of seconds the decompiler will run before timing out
  	 */
  	private final int DECOMPILER_TIMEOUT = 600;
  
  	/*
  	 * The number of threads via which the AI LLM provider is contacted. This
  	 * decreases the wait time on the LLM as multiple functions are handled at the
  	 * same time. If your LLM cannot handle multiple connections, adjust this value.
  	 */
  	private final int THREAD_COUNT = 10;
  
  	/*
  	 * The number of milliseconds to wait before the HTTP request times out
  	 */
  	private final int POST_REQUEST_TIMEOUT = 120_000;
  
  	/*
  	 * The HTTP URL to the LLM API
  	 */
  	private String API_URL;
  
  	/*
  	 * True if functions should be renamed, false if not. Only functions starting
  	 * with FUN_ are renamed if this is true.
  	 */
  	private boolean RENAME_FUNCTION;
  
  	/*
  	 * True if variables within functions should be renamed
  	 */
  	private boolean RENAME_VARIABLES;
  
  	/*
  	 * The Google JSON handling library which is included in Ghidra, used to convert
  	 * the JSON response from the LLM proxy
  	 */
  	private Gson gson;
  
  	/*
  	 * Ghidra's automatic analysis manager, used to check if one or more analyzers
  	 * are still running
  	 */
  	private AutoAnalysisManager autoAnalysisManager;
  
  	@Override
  	protected void run() throws Exception {
  		/*
  		 * Required input:
  		 * 
  		 * - AI API URL
  		 * 
  		 * - Rename function based on suggestion: yes/no
  		 * 
  		 * - Rename variables based on suggestions: yes/no
  		 */
  
  		// Get all provided values
  		API_URL = askString("AI API URL", "The URL where the AI's API is accessible at");
  		RENAME_FUNCTION = askYesNo("", "Rename the function based on the AI's suggestion");
  		RENAME_VARIABLES = askYesNo("", "Rename variables based on the AI's suggestions");
  
  		// Create a string to provide the feedback to the user
  		String inputFeedback = "Received input:\n";
  		inputFeedback += "\tAI URL:                     " + API_URL + "\n";
  		inputFeedback += "\tRename functions:           " + RENAME_FUNCTION + "\n";
  		inputFeedback += "\tRename variables:           " + RENAME_VARIABLES;
  
  		// Log the input feedback
  		println(inputFeedback);
  
  		// Now that all checks are done, the variables in the class are initialised
  		gson = new Gson();
  		autoAnalysisManager = AutoAnalysisManager.getAnalysisManager(currentProgram);
  		DecompInterface decompiler = new DecompInterface();
  		decompiler.openProgram(currentProgram);
  
  		/*
  		 * Notify the user of the current activity. Generating the complexity graph
  		 * takes a bit, but each step is relatively small. As such, it is summarised as
  		 * a single-step task to the user
  		 */
  		monitor.initialize(1, "Generating the complexity graph for the current program");
  
  		// Generate the complexity graph
  		GDirectedGraph<CodeBlockVertex, CodeBlockEdge> callGraph = createCallGraph();
  		Map<CodeBlockVertex, Integer> complexityDepth = GraphAlgorithms.getComplexityDepth(callGraph);
  		List<List<Function>> partition = createFunctionList(complexityDepth);
  
  		// Remove the empty levels from the list
  		partition = cleanAndReverseList(partition);
  
  		int currentFunctionCount = 0;
  		int totalFunctionCount = 0;
  		for (List<Function> list : partition) {
  			totalFunctionCount += list.size();
  		}
  
  		// Increment the monitor, marking this task as complete
  		monitor.increment();
  
  		/*
  		 * To iterate over the list from start to end (rather than end to start), you
  		 * need to reverse the list. This can be done using
  		 * Collections.reverse(partition).
  		 */
  
  		/*
  		 * Declare the executor service variable. It is instantiated per level within
  		 * the partition.
  		 */
  		ExecutorService executor;
  
  		/*
  		 * Iterate backwards over the list, handling the functions with the least
  		 * complexity depth first
  		 */
  		for (int i = 0; i < partition.size(); i++) {
  			// Get the functions for the complexity level
  			List<Function> list = partition.get(i);
  
  			if (list.isEmpty()) {
  				continue;
  			}
  
  			/*
  			 * Create the monitor message, taking the potential plurality of the function
  			 * count into account
  			 */
  			String listSize = "Iterating over " + list.size() + " function";
  			if (list.size() > 1) {
  				listSize += "s";
  			}
  			String monitorMessage = listSize + " in level " + i + "/" + partition.size();
  
  			/*
  			 * Initialise the monitor with the message. Each thread will call back to the
  			 * thread safe increment method
  			 */
  			monitor.initialize(list.size(), monitorMessage);
  
  			/*
  			 * The number of threads is equal to the globally set maximum number of thread
  			 * counts unless the size of the list is less than that.
  			 */
  			if (list.size() > THREAD_COUNT) {
  				executor = Executors.newFixedThreadPool(THREAD_COUNT);
  			} else {
  				executor = Executors.newFixedThreadPool(list.size());
  			}
  
  			// Iterate over each function within this complexity level
  			for (Function function : list) {
  				// Exclude thunk and external functions
  				if (function.isThunk() || function.isExternal()) {
  					continue;
  				}
  
  				/*
  				 * Set the current value for the current function, so we can keep the analyst
  				 * updated based on the number of functions (as only the current and total
  				 * number of levels in the graph is shown via the monitor message
  				 */
  				currentFunctionCount++;
  				/*
  				 * Create a worker to schedule in the thread pool. The worker will connect with
  				 * the LLM and use the response to modify the given function based on the
  				 * predefined settings with regards to renaming.
  				 */
  				FunctionWorker worker = new FunctionWorker(decompiler, function, totalFunctionCount,
  						currentFunctionCount);
  				// Schedule the worker in the executor
  				executor.execute(worker);
  			}
  
  			// Shut the executor down, meaning no new workers can be added
  			executor.shutdown();
  
  			// Wait until all workers have completed their work
  			while (executor.isTerminated() == false) {
  				// If the user cancels the script, cancel the execution
  				if (monitor.isCancelled()) {
  					throw new CancelledException();
  				}
  				/*
  				 * If the workers are running and the script isn't cancelled by the user, sleep
  				 * for one second to avoid using CPU cycles and check again after the sleep
  				 * finishes.
  				 */
  				Thread.sleep(1000);
  			}
  			/*
  			 * Before moving on to the next level, we wait for any ongoing automatic
  			 * analysis to complete
  			 */
  			waitUntilAutoAnalysisCompletes();
  		}
  	}
  
  	/**
  	 * A function to remove empty list entries from the list of lists. The functions
  	 * within each list within the given list are also iterated over, excluding
  	 * external and thunk functions, as these will not be handled afterwards
  	 * 
  	 * @param list the list which contains the levels, where each level is a list of
  	 *             functions
  	 * @return a list with the levels, where each level contains 1 or more
  	 *         functions, and all included functions are non-thunk and non-external
  	 */
  	private List<List<Function>> cleanAndReverseList(List<List<Function>> list) {
  		// The output variable
  		List<List<Function>> output = new ArrayList<>();
  
  		// Iterate backwards over the list
  		for (int i = list.size(); i-- > 0;) {
  			// Get the current level
  			List<Function> level = list.get(i);
  			// Skip empty levels
  			if (level.isEmpty()) {
  				continue;
  			}
  			// Create a new list of functions
  			List<Function> functions = new ArrayList<>();
  			// Iterate over the functions within the level
  			for (Function f : level) {
  				// Exclude external and thunk functions
  				if (f.isExternal() || f.isThunk()) {
  					continue;
  				}
  				// Add non-thunk and non-external functions to the list
  				functions.add(f);
  			}
  			// Add the filtered level to the output variable
  			output.add(functions);
  		}
  
  		/*
  		 * Return the list with the levels, in the original order with the applied
  		 * filters
  		 */
  		return output;
  	}
  
  	/**
  	 * Prints the given message via the
  	 * {@link ghidra.app.script.GhidraScript#println(String)} method, depending on
  	 * the running mode and boolean.
  	 * 
  	 * @param message       the message to print
  	 * @param printHeadless true if the message should be printed when executing
  	 *                      headless, false if not
  	 */
  	private void log(String message, boolean printHeadless) {
  		if (SystemUtilities.isInHeadlessMode()) {
  			if (printHeadless == false) {
  				return;
  			}
  		}
  		println(message);
  	}
  
  	/**
  	 * This method only returns once the auto analysis manager is done analyzing. If
  	 * the user cancels, a cancelled exception is thrown.
  	 * 
  	 * @throws InterruptedException
  	 * @throws CancelledException
  	 */
  	private void waitUntilAutoAnalysisCompletes() throws InterruptedException, CancelledException {
  		while (autoAnalysisManager.isAnalyzing()) {
  			if (monitor.isCancelled()) {
  				throw new CancelledException();
  			}
  			Thread.sleep(1000); // Sleep to avoid consuming extra CPU cycles in-between checks
  		}
  	}
  
  	/**
  	 * A thread safe method to increment the monitor
  	 */
  	private synchronized void incrementMonitorThreadSafe() {
  		// Does not check for cancellation
  		monitor.incrementProgress();
  	}
  
  	/**
  	 * A thread safe method to verify if a method's name is unique.
  	 * 
  	 * @param functionName The function name to verify
  	 * @return true if the name is unique, false if not
  	 */
  	private synchronized boolean isUniqueFunctionName(String functionName) {
  		for (Function function : currentProgram.getFunctionManager().getFunctionsNoStubs(currentProgram.getMinAddress(),
  				true)) {
  			if (function.getName().equalsIgnoreCase(functionName)) {
  				return false;
  			}
  		}
  		return true;
  	}
  
  	/**
  	 * Returns a unique function name. If the name is not unique, an underscore is
  	 * appended
  	 * 
  	 * @param functionName the name to check
  	 * @return the unique name
  	 */
  	private synchronized String getUniqueFunctionName(String functionName) {
  		if (isUniqueFunctionName(functionName)) {
  			return functionName;
  		}
  
  		functionName += "_";
  		return getUniqueFunctionName(functionName);
  	}
  
  	/**
  	 * The class where the threaded execution to handle a given function is handled
  	 */
  	private class FunctionWorker implements Runnable {
  
  		private DecompInterface decompiler;
  		private Function function;
  		int totalFunctionCount;
  		int currentFunctionCount;
  
  		public FunctionWorker(DecompInterface decompiler, Function function, int totalFunctionCount,
  				int currentFunctionCount) {
  			this.decompiler = decompiler;
  			this.function = function;
  			this.totalFunctionCount = totalFunctionCount;
  			this.currentFunctionCount = currentFunctionCount;
  		}
  
  		/**
  		 * The run method of the thread, not to be confused with the script's main run
  		 * method!
  		 */
  		@Override
  		public void run() {
  			// Decompile the given function
  			DecompileResults results = decompiler.decompileFunction(function, DECOMPILER_TIMEOUT, monitor);
  			// Get the decompiled function
  			DecompiledFunction dFunction = results.getDecompiledFunction();
  			// Get the pseudo-C representation of the decompiled function
  			String code = dFunction.getC();
  			// Get the plate comment, if present
  			String tempComment = getPlateComment(function.getEntryPoint());
  			// If the comment is non-null, it exists
  			if (tempComment != null) {
  				/*
  				 * Remove the comment from the pseudo-C representation to avoid overloading the
  				 * LLM's context window
  				 */
  				code = code.replace(tempComment, "");
  			}
  
  			// Get the function's variables
  			Variable[] variables = function.getAllVariables();
  
  			// Contact the LLM and store the JSON-based response
  			JsonResponse response = contactLLM(code);
  			// If the response is null, an error occurred
  			if (response == null) {
  				// Print an error if there is an error
  				printerr(
  						"The LLM's return value cannot be parsed as the format is invalid or the LLM did not respond at all");
  				// Return from the thread
  				return;
  			}
  
  			// Declare and initialize the comment
  			String comment = "";
  
  			// Store the old function name
  			String oldFunctionName = function.getName();
  			// Get the new function name from the LLM response
  			String newFunctionName = response.getFunctionName();
  			// If the new name is present and is not an empty or whitespace-only string
  			if (newFunctionName != null && newFunctionName.isBlank() == false) {
  				// If functions should be renamed
  				if (RENAME_FUNCTION) {
  					// Only rename functions with default names
  					if (function.getName().toLowerCase().startsWith("fun_")) {
  						try {
  							// Set the new name
  							function.setName(getUniqueFunctionName(newFunctionName), SourceType.IMPORTED);
  							// Log the rename to non-headless instances
  							log(oldFunctionName + " -> " + newFunctionName, false);
  							// Add the name change to the comment
  							comment += "Changed " + oldFunctionName + " into " + newFunctionName + "\n\n";
  						} catch (Exception ex) {
  							printerr("Function renaming failed:\n" + ex.toString());
  						}
  					}
  				}
  				/*
  				 * If the function should not be renamed, or if the function name does not start
  				 * with fun_, the suggested name is still added to the comment
  				 */
  				if (RENAME_FUNCTION == false || function.getName().toLowerCase().startsWith("fun_") == false) {
  					comment += "AI suggested function name: " + newFunctionName + "\n\n";
  				}
  			}
  
  			// If one or more variables are present in the function and LLM response
  			if (variables.length > 0 && response.getVariableNames() != null) {
  				// Set the next part of the comment
  				comment += "Variables:\n";
  				// Iterate over all variables
  				for (Variable variable : variables) {
  					// Get the old name
  					String oldVariableName = variable.getName();
  					// Get the new name
  					String newVariableName = response.getVariableNames().get(oldVariableName);
  					// If the new name is non-null and is not an empty or whitespace-only string
  					if (newVariableName != null && newVariableName.isBlank() == false) {
  						// If variables should be renamed
  						if (RENAME_VARIABLES) {
  							try {
  								// Set the name
  								variable.setName(newVariableName, SourceType.IMPORTED);
  							} catch (Exception ex) {
  								printerr("Variable renaming failed:\n" + ex.toString());
  							}
  						}
  						// Add the renaming to the comment
  						comment += "\t\tAI variable name: \"" + oldVariableName + "\" -> \"" + newVariableName + "\"\n";
  					}
  				}
  				// Log the variables in Ghidra, when running non-headless
  				log("Variables for " + function.getName() + " renamed", false);
  				comment += "\n";
  			}
  
  			// Get the summary from the LLM
  			String summary = response.getSummary();
  			// If the summary is non-null and is not an empty or whitespace-only string
  			if (summary != null && summary.isBlank() == false) {
  				// Add the summary to the comment
  				comment += "AI function summary: " + summary;
  			}
  
  			// If the comment is not whitespace-only or empty
  			if (comment.isBlank() == false) {
  				// Set the comment to the function
  				setComment(function, comment, false);
  				// Log the summary addition for non-headless instances
  				log("Summary for " + function.getName() + " added", false);
  			}
  
  			/*
  			 * This message is always logged (when running headless and non-headless) to
  			 * give an indication of the progress
  			 */
  			log("Completed " + function.getName() + " (" + currentFunctionCount + "/" + totalFunctionCount + ")", true);
  			// Increment the monitor to update the progress in a thread safe manner
  			incrementMonitorThreadSafe();
  		}
  	}
  
  	/**
  	 * Send the HTTP POST request to LLM
  	 * 
  	 * @param body the body of comma separated strings to check
  	 * @return the JSON response from the server
  	 * @throws IOException
  	 */
  	private String sendPostRequest(Map<String, String> headers, String body) throws IOException {
  		URL url = URI.create(API_URL).toURL();
  		HttpURLConnection connection = (HttpURLConnection) url.openConnection();
  		connection.setRequestMethod("POST");
  
  		for (Map.Entry<String, String> entry : headers.entrySet()) {
  			connection.setRequestProperty(entry.getKey(), entry.getValue());
  		}
  
  		connection.setConnectTimeout(POST_REQUEST_TIMEOUT);
  		connection.setReadTimeout(POST_REQUEST_TIMEOUT);
  
  		connection.setDoOutput(true);
  		try (OutputStream outputStream = connection.getOutputStream()) {
  			byte[] rawBody = body.getBytes("utf-8");
  			outputStream.write(rawBody, 0, rawBody.length);
  		}
  
  		// TODO use response code to potentially throw an exception
  		int responseCode = connection.getResponseCode();
  
  		try (BufferedReader bufferedReader = new BufferedReader(
  				new InputStreamReader(connection.getInputStream(), "utf-8"))) {
  			StringBuilder response = new StringBuilder();
  			String line = null;
  			while ((line = bufferedReader.readLine()) != null) {
  				response.append(line.trim());
  			}
  			return response.toString();
  		}
  	}
  
  	/**
  	 * Prompt the LLM by providing the pseudo-C code as the argument
  	 * 
  	 * @param code the function's pseudo-C code
  	 * @return the LLM's JSON response
  	 * @throws IOException
  	 */
  	public String promptLlmApi(String code) throws IOException {
  		Map<String, String> headers = new HashMap<>();
  		headers.put("Content-Type", "application/json");
  		headers.put("Accept", "application/json");
  
  		String prompt = "For the following decompiled code from Ghidra, suggest a new name for the function, summarise the function, and suggest new names for each of the variables in the function. State the old name of the variable and the new name. The response should be a JSON object with \"functionName\" as the key for the suggested function name, the key \"summary\" should contain the function's summary, and a nested JSON object named \"variableNames\" with the old variable names as keys, where the value of each key is the newly suggested name. The response should only the the requested JSON object, nothing else.\n\n"
  				+ code;
  
  		return sendPostRequest(headers, prompt);
  	}
  
  	/**
  	 * Contact the LLM with the pseudo-C code and prompt
  	 * 
  	 * @param code the function's pseudo-C code
  	 * @return a Java object in which the LLM's JSON response is already parsed
  	 */
  	private JsonResponse contactLLM(String code) {
  		try {
  			String response = promptLlmApi(code);
  
  			if (response == null) {
  				// return null;
  			}
  			JsonResponse jsonResponse = gson.fromJson(response, JsonResponse.class);
  			return jsonResponse;
  		} catch (IOException ex) {
  			printerr(ex.toString());
  			return null;
  		} catch (JsonSyntaxException ex) {
  			printerr(ex.toString());
  			return null;
  		} catch (Exception ex) {
  			printerr(ex.toString());
  			return null;
  		}
  	}
  
  	/**
  	 * Helper function to set a comment at a given function, with the indication if
  	 * this comment should be at the top or bottom of any already existing function
  	 * comment at this function
  	 * 
  	 * @param function  the function to set a comment at
  	 * @param comment   the comment to set at the given function
  	 * @param pushToTop true if the function's existing comment (if any) should be
  	 *                  put below this comment, false if not
  	 */
  	private void setComment(Function function, String comment, boolean pushToTop) {
  		// Get the old comment, which is null if no comment is present
  		String oldComment = function.getComment();
  		// Check for the existence of the old comment
  		if (oldComment != null) {
  			// If it is present, trim the comment to avoid redundant whitespace
  			oldComment = oldComment.trim();
  			// If the string, post trimming, is not empty nor blank (which also checks if it
  			// is empty)
  			if (oldComment.isEmpty() == false && oldComment.isBlank() == false) {
  				// If the comment should be at the top, place it at the top
  				if (pushToTop) {
  					comment += "\n\n" + oldComment;
  				} else { // Else put it at the bottom
  					comment = oldComment += "\n\n" + comment;
  				}
  			}
  		}
  
  		/**
  		 * Set the comment, which contains the prior comment at the correct placement
  		 * with regards to the given boolean if it existed
  		 */
  		function.setComment(comment);
  	}
  
  	/**
  	 * The class used to store the HTTP LLM API response in
  	 */
  	private class JsonResponse {
  		private String functionName;
  		private String summary;
  		private Map<String, String> variableNames;
  
  		@SuppressWarnings("unused")
  		public JsonResponse() {
  
  		}
  
  		/**
  		 * Gets the AI generated function name
  		 * 
  		 * @return the AI generated function name
  		 */
  		public String getFunctionName() {
  			return functionName;
  		}
  
  		/**
  		 * Gets the AI generated summary
  		 * 
  		 * @return the AI generated summary
  		 */
  		public String getSummary() {
  			return summary;
  		}
  
  		/**
  		 * Gets the mapping of the variable names (as keys) and the AI generated
  		 * variable names (as a value, one for each key)
  		 * 
  		 * @return gets the AI generated variable names for the current variable names
  		 */
  		public Map<String, String> getVariableNames() {
  			return variableNames;
  		}
  	}
  
  	// From here onwards, the code is from the Ghidra source code
  
  	private int getMaxLevel(Map<CodeBlockVertex, Integer> levelMap) {
  		int maxLevel = -1;
  		for (Integer level : levelMap.values()) {
  			if (level > maxLevel) {
  				maxLevel = level;
  			}
  		}
  		return maxLevel;
  	}
  
  	private Function getFunctionFromCodeBlockVertex(CodeBlockVertex vertex) {
  		Address startAddress = vertex.getCodeBlock().getFirstStartAddress();
  		Function function = getFunctionAt(startAddress);
  		return function;
  	}
  
  	private List<List<Function>> createFunctionList(Map<CodeBlockVertex, Integer> levelMap) {
  		List<List<Function>> levelList = new ArrayList<>();
  		int maxLevel = getMaxLevel(levelMap);
  		for (int i = 0; i <= maxLevel; i++) {
  			levelList.add(new ArrayList<Function>());
  		}
  		for (CodeBlockVertex vertex : levelMap.keySet()) {
  			int reverseLevel = maxLevel - levelMap.get(vertex);
  			Function function = getFunctionFromCodeBlockVertex(vertex);
  			if (function != null) {
  				levelList.get(reverseLevel).add(function);
  			}
  		}
  		return levelList;
  	}
  
  	protected GDirectedGraph<CodeBlockVertex, CodeBlockEdge> createCallGraph() throws CancelledException {
  
  		Map<CodeBlock, CodeBlockVertex> instanceMap = new HashMap<>();
  		GDirectedGraph<CodeBlockVertex, CodeBlockEdge> graph = GraphFactory.createDirectedGraph();
  		CodeBlockIterator codeBlocks = new BasicBlockModel(currentProgram, true).getCodeBlocks(monitor);
  		while (codeBlocks.hasNext()) {
  			CodeBlock block = codeBlocks.next();
  
  			CodeBlockVertex fromVertex = instanceMap.get(block);
  			if (fromVertex == null) {
  				fromVertex = new CodeBlockVertex(block);
  				instanceMap.put(block, fromVertex);
  				graph.addVertex(fromVertex);
  			}
  
  			// destinations section
  			addEdgesForDestinations(graph, fromVertex, block, instanceMap);
  		}
  		return graph;
  	}
  
  	private void addEdgesForDestinations(GDirectedGraph<CodeBlockVertex, CodeBlockEdge> graph,
  			CodeBlockVertex fromVertex, CodeBlock sourceBlock, Map<CodeBlock, CodeBlockVertex> instanceMap)
  			throws CancelledException {
  
  		CodeBlockReferenceIterator iterator = sourceBlock.getDestinations(monitor);
  		while (iterator.hasNext()) {
  			monitor.checkCancelled();
  
  			CodeBlockReference destination = iterator.next();
  			CodeBlock targetBlock = getDestinationBlock(destination);
  			if (targetBlock == null) {
  				continue; // no block found
  			}
  
  			CodeBlockVertex targetVertex = instanceMap.get(targetBlock);
  			if (targetVertex == null) {
  				targetVertex = new CodeBlockVertex(targetBlock);
  				instanceMap.put(targetBlock, targetVertex);
  			}
  
  			graph.addVertex(targetVertex);
  			graph.addEdge(new CodeBlockEdge(fromVertex, targetVertex));
  		}
  	}
  
  	private CodeBlock getDestinationBlock(CodeBlockReference destination) throws CancelledException {
  
  		Address targetAddress = destination.getDestinationAddress();
  		CodeBlock targetBlock = new BasicBlockModel(currentProgram, true).getFirstCodeBlockContaining(targetAddress,
  				monitor);
  		if (targetBlock == null) {
  			return null; // no code found for call; external?
  		}
  
  		return targetBlock;
  	}
  }
  