diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java index 002f8072..86a82282 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java @@ -98,6 +98,132 @@ public DependencyInfo(String key, String value) { } } + /** + * Empty reasons for ImportClassContent operation + */ + public enum ImportClassContentErrorReason { + NULL_ARGUMENTS("NullArgs"), + INVALID_URI("InvalidURI"), + URI_PARSE_FAILED("ParseFail"), + FILE_NOT_FOUND("NotFound"), + FILE_NOT_EXISTS("NotExists"), + NOT_JAVA_PROJECT("NotJava"), + PROJECT_NOT_EXISTS("ProjNotExists"), + NOT_COMPILATION_UNIT("NotCU"), + NO_IMPORTS("NoImports"), + OPERATION_CANCELLED("Cancelled"), + TIME_LIMIT_EXCEEDED("Timeout"), + NO_RESULTS("NoResults"), + PROCESSING_EXCEPTION("Error"); + + private final String message; + + ImportClassContentErrorReason(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } + + /** + * Empty reasons for ProjectDependencies operation + */ + public enum ProjectDependenciesErrorReason { + NULL_ARGUMENTS("NullArgs"), + INVALID_URI("InvalidURI"), + URI_PARSE_FAILED("ParseFail"), + MALFORMED_URI("MalformedURI"), + OPERATION_CANCELLED("Cancelled"), + RESOLVER_NULL_RESULT("ResolverNull"), + NO_DEPENDENCIES("NoDeps"), + PROCESSING_EXCEPTION("Error"); + + private final String message; + + ProjectDependenciesErrorReason(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } + + /** + * Error context information for operations + */ + public static class ErrorContext { + public final String errorValue; // The value that caused the error (e.g., invalid URI, null parsedPath, etc.) + + public ErrorContext(String errorValue) { + this.errorValue = errorValue; + } + } + + /** + * Result wrapper for getImportClassContent method + */ + public static class ImportClassContentResult { + public List classInfoList; + public String emptyReason; // Reason why the result is empty + public boolean isEmpty; + public ErrorContext errorContext; // Error context (only set when isEmpty = true) + + public ImportClassContentResult(List classInfoList) { + this.classInfoList = classInfoList; + this.emptyReason = null; + this.isEmpty = false; + this.errorContext = null; + } + + public ImportClassContentResult(ImportClassContentErrorReason errorReason) { + this.classInfoList = Collections.emptyList(); + this.emptyReason = errorReason.getMessage(); // Use enum message + this.isEmpty = true; + this.errorContext = null; + } + + public ImportClassContentResult(ImportClassContentErrorReason errorReason, String errorValue) { + this.classInfoList = Collections.emptyList(); + this.emptyReason = errorReason.getMessage(); + this.isEmpty = true; + this.errorContext = new ErrorContext(errorValue); + } + } + + /** + * Result wrapper for getProjectDependencies method + */ + public static class ProjectDependenciesResult { + public List dependencyInfoList; + public String emptyReason; // Reason why the result is empty + public boolean isEmpty; + public ErrorContext errorContext; // Error context (only set when isEmpty = true) + + public ProjectDependenciesResult(List dependencyInfoList) { + this.dependencyInfoList = dependencyInfoList; + this.emptyReason = null; + this.isEmpty = false; + this.errorContext = null; + } + + public ProjectDependenciesResult(ProjectDependenciesErrorReason errorReason) { + this.dependencyInfoList = new ArrayList<>(); + this.emptyReason = errorReason.getMessage(); // Use enum message + this.isEmpty = true; + this.errorContext = null; + } + + public ProjectDependenciesResult(ProjectDependenciesErrorReason errorReason, String errorValue) { + this.dependencyInfoList = new ArrayList<>(); + this.emptyReason = errorReason.getMessage(); + this.isEmpty = true; + this.errorContext = new ErrorContext(errorValue); + } + } + private static class Classpath { public String source; public String destination; @@ -353,30 +479,33 @@ public static boolean checkImportStatus() { /** * Get import class content for Copilot integration. * This method extracts information about imported classes from a Java file. - * Uses a time-controlled strategy: prioritizes internal classes, adds external classes only if time permits. + * Uses a time-controlled strategy: prioritizes internal classes, adds external + * classes only if time permits. * * @param arguments List containing the file URI as the first element - * @param monitor Progress monitor for cancellation support + * @param monitor Progress monitor for cancellation support * @return List of ImportClassInfo containing class information and JavaDoc */ - public static List getImportClassContent(List arguments, IProgressMonitor monitor) { + public static ImportClassContentResult getImportClassContent(List arguments, + IProgressMonitor monitor) { + // Record start time for timeout control + long startTime = System.currentTimeMillis(); + final long TIMEOUT_MS = 80; // 80ms timeout + if (arguments == null || arguments.isEmpty()) { - return Collections.emptyList(); + return new ImportClassContentResult(ImportClassContentErrorReason.NULL_ARGUMENTS); } - // Time control: total budget 80ms, early return at 75ms - long startTime = System.currentTimeMillis(); - final long TIME_BUDGET_MS = 80; - final long EARLY_RETURN_MS = 75; - try { String fileUri = (String) arguments.get(0); - + if (fileUri == null || fileUri.trim().isEmpty()) { + return new ImportClassContentResult(ImportClassContentErrorReason.INVALID_URI, fileUri); + } // Parse URI manually to avoid restricted API java.net.URI uri = new java.net.URI(fileUri); String filePath = uri.getPath(); if (filePath == null) { - return Collections.emptyList(); + return new ImportClassContentResult(ImportClassContentErrorReason.URI_PARSE_FAILED, filePath); } IPath path = new Path(filePath); @@ -385,19 +514,26 @@ public static List getImportClassContent(List arguments IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IFile file = root.getFileForLocation(path); if (file == null || !file.exists()) { - return Collections.emptyList(); + return new ImportClassContentResult(ImportClassContentErrorReason.FILE_NOT_FOUND, filePath); + } + if (!file.exists()) { + return new ImportClassContentResult(ImportClassContentErrorReason.FILE_NOT_EXISTS, filePath); } // Get the Java project IJavaProject javaProject = JavaCore.create(file.getProject()); - if (javaProject == null || !javaProject.exists()) { - return Collections.emptyList(); + if (javaProject == null) { + return new ImportClassContentResult(ImportClassContentErrorReason.NOT_JAVA_PROJECT, filePath); + } + if (!javaProject.exists()) { + String projectName = javaProject.getProject().getName(); + return new ImportClassContentResult(ImportClassContentErrorReason.PROJECT_NOT_EXISTS, projectName); } // Find the compilation unit IJavaElement javaElement = JavaCore.create(file); if (!(javaElement instanceof org.eclipse.jdt.core.ICompilationUnit)) { - return Collections.emptyList(); + return new ImportClassContentResult(ImportClassContentErrorReason.NOT_COMPILATION_UNIT, filePath); } org.eclipse.jdt.core.ICompilationUnit compilationUnit = (org.eclipse.jdt.core.ICompilationUnit) javaElement; @@ -409,72 +545,102 @@ public static List getImportClassContent(List arguments org.eclipse.jdt.core.IImportDeclaration[] imports = compilationUnit.getImports(); Set processedTypes = new HashSet<>(); + // Check if file has no imports + if (imports == null || imports.length == 0) { + return new ImportClassContentResult(ImportClassContentErrorReason.NO_IMPORTS); + } + // Phase 1: Priority - Resolve project source classes (internal) for (org.eclipse.jdt.core.IImportDeclaration importDecl : imports) { - // Check time budget before each operation - long elapsed = System.currentTimeMillis() - startTime; - if (monitor.isCanceled() || elapsed >= EARLY_RETURN_MS) { - return classInfoList; // Early return if approaching time limit + // Check cancellation before each operation + if (monitor.isCanceled()) { + return new ImportClassContentResult(ImportClassContentErrorReason.OPERATION_CANCELLED); } String importName = importDecl.getElementName(); boolean isStatic = (importDecl.getFlags() & org.eclipse.jdt.core.Flags.AccStatic) != 0; - + if (isStatic) { // Handle static imports - delegate to ContextResolver - ContextResolver.resolveStaticImport(javaProject, importName, classInfoList, processedTypes, monitor); + ContextResolver.resolveStaticImport(javaProject, importName, classInfoList, processedTypes, + monitor); } else if (importName.endsWith(".*")) { // Handle package imports - delegate to ContextResolver String packageName = importName.substring(0, importName.length() - 2); - ContextResolver.resolvePackageTypes(javaProject, packageName, classInfoList, processedTypes, monitor); + ContextResolver.resolvePackageTypes(javaProject, packageName, classInfoList, processedTypes, + monitor); } else { // Handle single type imports - delegate to ContextResolver ContextResolver.resolveSingleType(javaProject, importName, classInfoList, processedTypes, monitor); } } - // Phase 2: If time permits, resolve external dependencies - long elapsedAfterInternal = System.currentTimeMillis() - startTime; - if (elapsedAfterInternal < EARLY_RETURN_MS && !monitor.isCanceled()) { - // Calculate remaining time budget for external classes - long remainingTime = TIME_BUDGET_MS - elapsedAfterInternal; + // Phase 2: Resolve external dependencies if not cancelled and within time limit + if (!monitor.isCanceled()) { + // Check if we have exceeded the timeout before starting external resolution + long currentTime = System.currentTimeMillis(); + long elapsedTime = currentTime - startTime; - // Only proceed with external if we have reasonable time left (at least 15ms) - if (remainingTime >= 15) { - List externalClasses = new ArrayList<>(); - - for (org.eclipse.jdt.core.IImportDeclaration importDecl : imports) { - // Check time before each external resolution - long currentElapsed = System.currentTimeMillis() - startTime; - if (monitor.isCanceled() || currentElapsed >= EARLY_RETURN_MS) { - break; - } + if (elapsedTime >= TIMEOUT_MS) { + // Return early due to timeout, but still return what we have collected so far + if (classInfoList.isEmpty()) { + return new ImportClassContentResult(ImportClassContentErrorReason.TIME_LIMIT_EXCEEDED, String.valueOf(elapsedTime) + "ms"); + } + return new ImportClassContentResult(classInfoList); + } + + List externalClasses = new ArrayList<>(); - String importName = importDecl.getElementName(); - boolean isStatic = (importDecl.getFlags() & org.eclipse.jdt.core.Flags.AccStatic) != 0; - - // Skip package imports (*.* ) - too broad for external dependencies - if (importName.endsWith(".*")) { - continue; - } - - // Resolve external (binary) types with simplified content - if (!isStatic) { - ContextResolver.resolveBinaryType(javaProject, importName, externalClasses, - processedTypes, Integer.MAX_VALUE, monitor); - } + for (org.eclipse.jdt.core.IImportDeclaration importDecl : imports) { + // Check cancellation before each external resolution + if (monitor.isCanceled()) { + break; } - // Append external classes after project sources - classInfoList.addAll(externalClasses); + // Check timeout before each external resolution + currentTime = System.currentTimeMillis(); + elapsedTime = currentTime - startTime; + if (elapsedTime >= TIMEOUT_MS) { + // Timeout reached, stop processing external dependencies but keep existing results + break; + } + + String importName = importDecl.getElementName(); + boolean isStatic = (importDecl.getFlags() & org.eclipse.jdt.core.Flags.AccStatic) != 0; + + // Skip package imports (*.* ) - too broad for external dependencies + if (importName.endsWith(".*")) { + continue; + } + + // Resolve external (binary) types with simplified content + if (!isStatic) { + ContextResolver.resolveBinaryType(javaProject, importName, externalClasses, + processedTypes, Integer.MAX_VALUE, monitor); + } } - } - return classInfoList; + // Append external classes after project sources + classInfoList.addAll(externalClasses); + } + // Success case - return the resolved class information + if (classInfoList.isEmpty()) { + return new ImportClassContentResult(ImportClassContentErrorReason.NO_RESULTS); + } + return new ImportClassContentResult(classInfoList); } catch (Exception e) { JdtlsExtActivator.logException("Error in getImportClassContent", e); - return Collections.emptyList(); + // Try to get context from arguments if available + String errorUri = null; + try { + if (arguments != null && !arguments.isEmpty()) { + errorUri = (String) arguments.get(0); + } + } catch (Exception ignored) { + // Ignore any further exceptions when trying to get context + } + return new ImportClassContentResult(ImportClassContentErrorReason.PROCESSING_EXCEPTION, errorUri); } } @@ -510,21 +676,68 @@ private static String getSeverityString(int severity) { * @param monitor Progress monitor for cancellation support * @return List of DependencyInfo containing key-value pairs of project information */ - public static List getProjectDependencies(List arguments, IProgressMonitor monitor) { + public static ProjectDependenciesResult getProjectDependencies(List arguments, + IProgressMonitor monitor) { if (arguments == null || arguments.isEmpty()) { - return new ArrayList<>(); + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.NULL_ARGUMENTS); } - String fileUri = (String) arguments.get(0); - List resolverResult = ProjectResolver.resolveProjectDependencies(fileUri, monitor); - - // Convert ProjectResolver.DependencyInfo to ProjectCommand.DependencyInfo - List result = new ArrayList<>(); - for (ProjectResolver.DependencyInfo info : resolverResult) { - result.add(new DependencyInfo(info.key, info.value)); + try { + String fileUri = (String) arguments.get(0); + if (fileUri == null || fileUri.trim().isEmpty()) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.INVALID_URI, fileUri); + } + + // Validate URI format + String parsedPath = null; + try { + java.net.URI uri = new java.net.URI(fileUri); + parsedPath = uri.getPath(); + if (parsedPath == null) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.URI_PARSE_FAILED, parsedPath); + } + } catch (java.net.URISyntaxException e) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.MALFORMED_URI, fileUri); + } + + // Check if monitor is cancelled before processing + if (monitor.isCanceled()) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.OPERATION_CANCELLED); + } + List resolverResult = ProjectResolver.resolveProjectDependencies(fileUri, + monitor); + // Check if resolver returned null (should not happen, but defensive + // programming) + if (resolverResult == null) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.RESOLVER_NULL_RESULT); + } + // Convert ProjectResolver.DependencyInfo to ProjectCommand.DependencyInfo + List result = new ArrayList<>(); + for (ProjectResolver.DependencyInfo info : resolverResult) { + if (info != null) { + result.add(new DependencyInfo(info.key, info.value)); + } + } + + // Check if no dependencies were resolved + if (result.isEmpty()) { + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.NO_DEPENDENCIES); + } + + return new ProjectDependenciesResult(result); + } catch (Exception e) { + JdtlsExtActivator.logException("Error in getProjectDependenciesWithReason", e); + // Try to get context from arguments if available + String errorUri = null; + try { + if (arguments != null && !arguments.isEmpty()) { + errorUri = (String) arguments.get(0); + } + } catch (Exception ignored) { + // Ignore any further exceptions when trying to get context + } + return new ProjectDependenciesResult(ProjectDependenciesErrorReason.PROCESSING_EXCEPTION, errorUri); } - - return result; } private static final class LinkedFolderVisitor implements IResourceVisitor { diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java index bf815c50..2d1ee610 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java @@ -71,11 +71,11 @@ public class ContextResolver { */ public static class ImportClassInfo { public String uri; // File URI (required) - public String className; // Human-readable class description with JavaDoc appended (required) + public String value; // Human-readable class description with JavaDoc appended (required) public ImportClassInfo(String uri, String value) { this.uri = uri; - this.className = value; + this.value = value; } }