summaryrefslogtreecommitdiff
path: root/src/main/java/org/javacs/CompileBatch.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/javacs/CompileBatch.java')
-rw-r--r--src/main/java/org/javacs/CompileBatch.java801
1 files changed, 799 insertions, 2 deletions
diff --git a/src/main/java/org/javacs/CompileBatch.java b/src/main/java/org/javacs/CompileBatch.java
index f12c486..672b347 100644
--- a/src/main/java/org/javacs/CompileBatch.java
+++ b/src/main/java/org/javacs/CompileBatch.java
@@ -4,15 +4,25 @@ import com.sun.source.tree.*;
import com.sun.source.util.*;
import java.io.IOException;
import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.logging.Level;
import java.util.logging.Logger;
import javax.lang.model.element.*;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.*;
import javax.tools.*;
import org.javacs.lsp.Range;
public class CompileBatch implements AutoCloseable {
+ public static final int MAX_COMPLETION_ITEMS = 50;
+
private final JavaCompilerService parent;
private final ReportProgress progress;
private final TaskPool.Borrow borrow;
@@ -111,8 +121,7 @@ public class CompileBatch implements AutoCloseable {
}
public Optional<Element> element(URI uri, int line, int character) {
- var root = root(uri);
- var path = CompileFocus.findPath(borrow.task, root, line, character);
+ var path = findPath(uri, line, character);
var el = trees.getElement(path);
return Optional.ofNullable(el);
}
@@ -381,5 +390,793 @@ public class CompileBatch implements AutoCloseable {
return sorted;
}
+ /** Find all overloads for the smallest method call that includes the cursor */
+ public Optional<MethodInvocation> methodInvocation(URI file, int line, int character) {
+ LOG.info(String.format("Find method invocation around %s(%d,%d)...", file, line, character));
+ var cursor = findPath(file, line, character);
+ for (var path = cursor; path != null; path = path.getParentPath()) {
+ if (path.getLeaf() instanceof MethodInvocationTree) {
+ // Find all overloads of method
+ LOG.info(String.format("...`%s` is a method invocation", path.getLeaf()));
+ var invoke = (MethodInvocationTree) path.getLeaf();
+ var method = trees.getElement(trees.getPath(path.getCompilationUnit(), invoke.getMethodSelect()));
+ var results = new ArrayList<ExecutableElement>();
+ for (var m : method.getEnclosingElement().getEnclosedElements()) {
+ if (m.getKind() == ElementKind.METHOD && m.getSimpleName().equals(method.getSimpleName())) {
+ results.add((ExecutableElement) m);
+ }
+ }
+ // Figure out which parameter is active
+ var activeParameter = invoke.getArguments().indexOf(cursor.getLeaf());
+ LOG.info(String.format("...active parameter `%s` is %d", cursor.getLeaf(), activeParameter));
+ // Figure out which method is active, if possible
+ Optional<ExecutableElement> activeMethod =
+ method instanceof ExecutableElement
+ ? Optional.of((ExecutableElement) method)
+ : Optional.empty();
+ return Optional.of(new MethodInvocation(invoke, activeMethod, activeParameter, results));
+ } else if (path.getLeaf() instanceof NewClassTree) {
+ // Find all overloads of method
+ LOG.info(String.format("...`%s` is a constructor invocation", path.getLeaf()));
+ var invoke = (NewClassTree) path.getLeaf();
+ var method = trees.getElement(path);
+ var results = new ArrayList<ExecutableElement>();
+ for (var m : method.getEnclosingElement().getEnclosedElements()) {
+ if (m.getKind() == ElementKind.CONSTRUCTOR) {
+ results.add((ExecutableElement) m);
+ }
+ }
+ // Figure out which parameter is active
+ var activeParameter = invoke.getArguments().indexOf(cursor.getLeaf());
+ LOG.info(String.format("...active parameter `%s` is %d", cursor.getLeaf(), activeParameter));
+ // Figure out which method is active, if possible
+ Optional<ExecutableElement> activeMethod =
+ method instanceof ExecutableElement
+ ? Optional.of((ExecutableElement) method)
+ : Optional.empty();
+ return Optional.of(new MethodInvocation(invoke, activeMethod, activeParameter, results));
+ }
+ }
+ return Optional.empty();
+ }
+
+ public List<Completion> completeIdentifiers(
+ URI uri, int line, int character, boolean insideClass, boolean insideMethod, String partialName) {
+ LOG.info(String.format("Completing identifiers starting with `%s`...", partialName));
+
+ var root = root(uri);
+ var result = new ArrayList<Completion>();
+
+ // Add snippets
+ if (!insideClass) {
+ // If no package declaration is present, suggest package [inferred name];
+ if (root.getPackage() == null) {
+ var name = FileStore.suggestedPackageName(Paths.get(uri));
+ result.add(Completion.ofSnippet("package " + name, "package " + name + ";\n\n"));
+ }
+ // If no class declaration is present, suggest class [file name]
+ var hasClassDeclaration = false;
+ for (var t : root.getTypeDecls()) {
+ if (!(t instanceof ErroneousTree)) {
+ hasClassDeclaration = true;
+ }
+ }
+ if (!hasClassDeclaration) {
+ var name = Paths.get(uri).getFileName().toString();
+ name = name.substring(0, name.length() - ".java".length());
+ result.add(Completion.ofSnippet("class " + name, "class " + name + " {\n $0\n}"));
+ }
+ }
+ // Add identifiers
+ completeScopeIdentifiers(uri, line, character, partialName, result);
+ // Add keywords
+ if (!insideClass) {
+ addKeywords(TOP_LEVEL_KEYWORDS, partialName, result);
+ } else if (!insideMethod) {
+ addKeywords(CLASS_BODY_KEYWORDS, partialName, result);
+ } else {
+ addKeywords(METHOD_BODY_KEYWORDS, partialName, result);
+ }
+
+ return result;
+ }
+
+ public List<Completion> completeAnnotations(URI uri, int line, int character, String partialName) {
+ var result = new ArrayList<Completion>();
+ // Add @Override ... snippet
+ if ("Override".startsWith(partialName)) {
+ // TODO filter out already-implemented methods using thisMethods
+ var alreadyShown = new HashSet<String>();
+ for (var method : superMethods(uri, line, character)) {
+ var mods = method.getModifiers();
+ if (mods.contains(Modifier.STATIC) || mods.contains(Modifier.PRIVATE)) continue;
+
+ var label = "@Override " + ShortTypePrinter.printMethod(method);
+ var snippet = "Override\n" + new TemplatePrinter().printMethod(method) + " {\n $0\n}";
+ var override = Completion.ofSnippet(label, snippet);
+ if (!alreadyShown.contains(label)) {
+ result.add(override);
+ alreadyShown.add(label);
+ }
+ }
+ }
+ // Add @Override, @Test, other simple class names
+ completeScopeIdentifiers(uri, line, character, partialName, result);
+ return result;
+ }
+
+ /** Find all case options in the switch expression surrounding line:character */
+ public List<Completion> completeCases(URI uri, int line, int character) {
+ var cursor = findPath(uri, line, character);
+ LOG.info(String.format("Complete enum constants following `%s`...", cursor.getLeaf()));
+
+ // Find surrounding switch
+ var path = cursor;
+ while (!(path.getLeaf() instanceof SwitchTree)) path = path.getParentPath();
+ var leaf = (SwitchTree) path.getLeaf();
+ path = new TreePath(path, leaf.getExpression());
+ LOG.info(String.format("...found switch expression `%s`", leaf.getExpression()));
+
+ // Get members of switched type
+ var type = trees.getTypeMirror(path);
+ if (type == null) {
+ LOG.info("...no type at " + leaf.getExpression());
+ return Collections.emptyList();
+ }
+ LOG.info(String.format("...switched expression has type `%s`", type));
+ var types = borrow.task.getTypes();
+ var definition = types.asElement(type);
+ if (definition == null) {
+ LOG.info("...type has no definition, completing identifiers instead");
+ return completeIdentifiers(uri, line, character, true, true, ""); // TODO pass partial name
+ }
+ LOG.info(String.format("...switched expression has definition `%s`", definition));
+ var result = new ArrayList<Completion>();
+ for (var member : definition.getEnclosedElements()) {
+ if (member.getKind() == ElementKind.ENUM_CONSTANT) result.add(Completion.ofElement(member));
+ }
+
+ return result;
+ }
+
+ /** Find all members of expression ending at line:character */
+ public List<Completion> completeMembers(URI uri, int line, int character, boolean isReference) {
+ var path = findPath(uri, line, character);
+ var types = borrow.task.getTypes();
+ var scope = trees.getScope(path);
+ var element = trees.getElement(path);
+
+ if (element instanceof PackageElement) {
+ var result = new ArrayList<Completion>();
+ var p = (PackageElement) element;
+
+ LOG.info(String.format("...completing members of package %s", p.getQualifiedName()));
+
+ // Add class-names resolved as Element by javac
+ for (var member : p.getEnclosedElements()) {
+ // If the package member is a TypeElement, like a class or interface, check if it's accessible
+ if (member instanceof TypeElement) {
+ if (trees.isAccessible(scope, (TypeElement) member)) {
+ result.add(Completion.ofElement(member));
+ }
+ }
+ // Otherwise, just assume it's accessible and add it to the list
+ else result.add(Completion.ofElement(member));
+ }
+ // Add sub-package names resolved as String by guava ClassPath
+ var parent = p.getQualifiedName().toString();
+ var subs = subPackages(parent);
+ for (var sub : subs) {
+ result.add(Completion.ofPackagePart(sub, Parser.lastName(sub)));
+ }
+
+ return result;
+ } else if (element instanceof TypeElement && isReference) {
+ var result = new ArrayList<Completion>();
+ var t = (TypeElement) element;
+
+ LOG.info(String.format("...completing static methods of %s", t.getQualifiedName()));
+
+ // Add members
+ for (var member : t.getEnclosedElements()) {
+ if (member.getKind() == ElementKind.METHOD
+ && trees.isAccessible(scope, member, (DeclaredType) t.asType())) {
+ result.add(Completion.ofElement(member));
+ }
+ }
+
+ // Add ::new
+ result.add(Completion.ofKeyword("new"));
+
+ return result;
+ } else if (element instanceof TypeElement && !isReference) {
+ var result = new ArrayList<Completion>();
+ var t = (TypeElement) element;
+
+ LOG.info(String.format("...completing static members of %s", t.getQualifiedName()));
+
+ // Add static members
+ for (var member : t.getEnclosedElements()) {
+ // TODO if this is a member reference :: then include non-statics
+ if (member.getModifiers().contains(Modifier.STATIC)
+ && trees.isAccessible(scope, member, (DeclaredType) t.asType())) {
+ result.add(Completion.ofElement(member));
+ }
+ }
+
+ // Add .class
+ result.add(Completion.ofKeyword("class"));
+ result.add(Completion.ofKeyword("this"));
+ result.add(Completion.ofKeyword("super"));
+
+ return result;
+ } else {
+ var type = trees.getTypeMirror(path);
+ if (type == null) {
+ LOG.warning(String.format("`...%s` has not type", path.getLeaf()));
+ return List.of();
+ }
+ if (!hasMembers(type)) {
+ LOG.warning("...don't know how to complete members of type " + type);
+ return Collections.emptyList();
+ }
+
+ var result = new ArrayList<Completion>();
+ var ts = supersWithSelf(type);
+ var alreadyAdded = new HashSet<String>();
+ LOG.info(String.format("...completing virtual members of %s and %d supers", type, ts.size()));
+ for (var t : ts) {
+ var e = types.asElement(t);
+ if (e == null) {
+ LOG.warning(String.format("...can't convert supertype `%s` to element, skipping", t));
+ continue;
+ }
+ for (var member : e.getEnclosedElements()) {
+ // Don't add statics
+ if (member.getModifiers().contains(Modifier.STATIC)) continue;
+ // Don't add constructors
+ if (member.getSimpleName().contentEquals("<init>")) continue;
+ // Skip overridden members from superclass
+ if (alreadyAdded.contains(member.toString())) continue;
+
+ // If type is a DeclaredType, check accessibility of member
+ if (type instanceof DeclaredType) {
+ if (trees.isAccessible(scope, member, (DeclaredType) type)) {
+ result.add(Completion.ofElement(member));
+ alreadyAdded.add(member.toString());
+ }
+ }
+ // Otherwise, accessibility rules are very complicated
+ // Give up and just declare that everything is accessible
+ else {
+ result.add(Completion.ofElement(member));
+ alreadyAdded.add(member.toString());
+ }
+ }
+ }
+ if (type instanceof ArrayType) {
+ result.add(Completion.ofKeyword("length"));
+ }
+ return result;
+ }
+ }
+
+ public static String[] TOP_LEVEL_KEYWORDS = {
+ "package",
+ "import",
+ "public",
+ "private",
+ "protected",
+ "abstract",
+ "class",
+ "interface",
+ "extends",
+ "implements",
+ };
+
+ private static String[] CLASS_BODY_KEYWORDS = {
+ "public",
+ "private",
+ "protected",
+ "static",
+ "final",
+ "native",
+ "synchronized",
+ "abstract",
+ "default",
+ "class",
+ "interface",
+ "void",
+ "boolean",
+ "int",
+ "long",
+ "float",
+ "double",
+ };
+
+ private static String[] METHOD_BODY_KEYWORDS = {
+ "new",
+ "assert",
+ "try",
+ "catch",
+ "finally",
+ "throw",
+ "return",
+ "break",
+ "case",
+ "continue",
+ "default",
+ "do",
+ "while",
+ "for",
+ "switch",
+ "if",
+ "else",
+ "instanceof",
+ "var",
+ "final",
+ "class",
+ "void",
+ "boolean",
+ "int",
+ "long",
+ "float",
+ "double",
+ };
+
+ private List<ExecutableElement> virtualMethods(DeclaredType type) {
+ var result = new ArrayList<ExecutableElement>();
+ for (var member : type.asElement().getEnclosedElements()) {
+ if (member instanceof ExecutableElement) {
+ var method = (ExecutableElement) member;
+ if (!method.getSimpleName().contentEquals("<init>")
+ && !method.getModifiers().contains(Modifier.STATIC)) {
+ result.add(method);
+ }
+ }
+ }
+ return result;
+ }
+
+ private TypeMirror enclosingClass(URI uri, int line, int character) {
+ var cursor = findPath(uri, line, character);
+ var path = cursor;
+ while (!(path.getLeaf() instanceof ClassTree)) path = path.getParentPath();
+ var enclosingClass = trees.getElement(path);
+
+ return enclosingClass.asType();
+ }
+
+ private void collectSuperMethods(TypeMirror thisType, List<ExecutableElement> result) {
+ var types = borrow.task.getTypes();
+
+ for (var superType : types.directSupertypes(thisType)) {
+ if (superType instanceof DeclaredType) {
+ var type = (DeclaredType) superType;
+ result.addAll(virtualMethods(type));
+ collectSuperMethods(type, result);
+ }
+ }
+ }
+
+ private List<ExecutableElement> superMethods(URI uri, int line, int character) {
+ var thisType = enclosingClass(uri, line, character);
+ var result = new ArrayList<ExecutableElement>();
+
+ collectSuperMethods(thisType, result);
+
+ return result;
+ }
+
+ static boolean matchesPartialName(CharSequence candidate, CharSequence partialName) {
+ if (candidate.length() < partialName.length()) return false;
+ for (int i = 0; i < partialName.length(); i++) {
+ if (candidate.charAt(i) != partialName.charAt(i)) return false;
+ }
+ return true;
+ }
+
+ private boolean isImported(URI uri, String qualifiedName) {
+ var root = root(uri);
+ var packageName = Parser.mostName(qualifiedName);
+ var className = Parser.lastName(qualifiedName);
+ for (var i : root.getImports()) {
+ var importName = i.getQualifiedIdentifier().toString();
+ var importPackage = Parser.mostName(importName);
+ var importClass = Parser.lastName(importName);
+ if (importClass.equals("*") && importPackage.equals(packageName)) return true;
+ if (importClass.equals(className) && importPackage.equals(packageName)) return true;
+ }
+ return false;
+ }
+
+ private Set<TypeMirror> supersWithSelf(TypeMirror t) {
+ var types = new HashSet<TypeMirror>();
+ collectSupers(t, types);
+ // Object type is not included by default
+ // We need to add it to get members like .equals(other) and .hashCode()
+ types.add(borrow.task.getElements().getTypeElement("java.lang.Object").asType());
+ return types;
+ }
+
+ private void collectSupers(TypeMirror t, Set<TypeMirror> supers) {
+ supers.add(t);
+ for (var s : types.directSupertypes(t)) {
+ collectSupers(s, supers);
+ }
+ }
+
+ private boolean hasMembers(TypeMirror type) {
+ switch (type.getKind()) {
+ case ARRAY:
+ case DECLARED:
+ case ERROR:
+ case TYPEVAR:
+ case WILDCARD:
+ case UNION:
+ case INTERSECTION:
+ return true;
+ case BOOLEAN:
+ case BYTE:
+ case SHORT:
+ case INT:
+ case LONG:
+ case CHAR:
+ case FLOAT:
+ case DOUBLE:
+ case VOID:
+ case NONE:
+ case NULL:
+ case PACKAGE:
+ case EXECUTABLE:
+ case OTHER:
+ default:
+ return false;
+ }
+ }
+
+ /** Find all identifiers in scope at line:character */
+ List<Element> scopeMembers(URI uri, int line, int character, String partialName) {
+ var path = findPath(uri, line, character);
+ var types = borrow.task.getTypes();
+ var start = trees.getScope(path);
+
+ class Walk {
+ List<Element> result = new ArrayList<>();
+
+ boolean isStatic(Scope s) {
+ var method = s.getEnclosingMethod();
+ if (method != null) {
+ return method.getModifiers().contains(Modifier.STATIC);
+ } else return false;
+ }
+
+ boolean isStatic(Element e) {
+ return e.getModifiers().contains(Modifier.STATIC);
+ }
+
+ boolean isThisOrSuper(Element e) {
+ var name = e.getSimpleName();
+ return name.contentEquals("this") || name.contentEquals("super");
+ }
+
+ // Place each member of `this` or `super` directly into `results`
+ void unwrapThisSuper(VariableElement ve) {
+ var thisType = ve.asType();
+ // `this` and `super` should always be instances of DeclaredType, which we'll use to check accessibility
+ if (!(thisType instanceof DeclaredType)) {
+ LOG.warning(String.format("%s is not a DeclaredType", thisType));
+ return;
+ }
+ var thisDeclaredType = (DeclaredType) thisType;
+ var thisElement = types.asElement(thisDeclaredType);
+ for (var thisMember : thisElement.getEnclosedElements()) {
+ if (isStatic(start) && !isStatic(thisMember)) continue;
+ if (thisMember.getSimpleName().contentEquals("<init>")) continue;
+ if (!matchesPartialName(thisMember.getSimpleName(), partialName)) continue;
+
+ // Check if member is accessible from original scope
+ if (trees.isAccessible(start, thisMember, thisDeclaredType)) {
+ result.add(thisMember);
+ }
+ }
+ }
+
+ // Place each member of `s` into results, and unwrap `this` and `super`
+ void walkLocals(Scope s) {
+ try {
+ for (var e : s.getLocalElements()) {
+ if (matchesPartialName(e.getSimpleName(), partialName)) {
+ if (e instanceof TypeElement) {
+ var te = (TypeElement) e;
+ if (trees.isAccessible(start, te)) result.add(te);
+ } else if (isThisOrSuper(e)) {
+ if (!isStatic(s)) result.add(e);
+ } else {
+ result.add(e);
+ }
+ }
+ if (isThisOrSuper(e)) {
+ unwrapThisSuper((VariableElement) e);
+ }
+ if (tooManyItems(result.size())) return;
+ }
+ } catch (Exception e) {
+ LOG.log(Level.WARNING, "error walking locals in scope", e);
+ }
+ }
+
+ // Walk each enclosing scope, placing its members into `results`
+ List<Element> walkScopes() {
+ var scopes = new ArrayList<Scope>();
+ for (var s = start; s != null; s = s.getEnclosingScope()) {
+ scopes.add(s);
+ }
+ // Scopes may be contained in an enclosing scope.
+ // The outermost scope contains those elements available via "star import" declarations;
+ // the scope within that contains the top level elements of the compilation unit, including any named
+ // imports.
+ // https://parent.docs.oracle.com/en/java/javase/11/docs/api/jdk.compiler/com/sun/source/tree/Scope.html
+ for (var i = 0; i < scopes.size() - 2; i++) {
+ var s = scopes.get(i);
+ walkLocals(s);
+ // Return early?
+ if (tooManyItems(result.size())) {
+ return result;
+ }
+ }
+
+ return result;
+ }
+ }
+ return new Walk().walkScopes();
+ }
+
+ private boolean tooManyItems(int count) {
+ var test = count >= MAX_COMPLETION_ITEMS;
+ if (test) LOG.warning(String.format("...# of items %d reached max %s", count, MAX_COMPLETION_ITEMS));
+ return test;
+ }
+
+ private Set<String> subPackages(String parentPackage) {
+ var result = new HashSet<String>();
+ Consumer<String> checkClassName =
+ name -> {
+ var packageName = Parser.mostName(name);
+ if (packageName.startsWith(parentPackage) && packageName.length() > parentPackage.length()) {
+ var start = parentPackage.length() + 1;
+ var end = packageName.indexOf('.', start);
+ if (end == -1) end = packageName.length();
+ var prefix = packageName.substring(0, end);
+ result.add(prefix);
+ }
+ };
+ for (var name : parent.jdkClasses) checkClassName.accept(name);
+ for (var name : parent.classPathClasses) checkClassName.accept(name);
+ return result;
+ }
+
+ private static void addKeywords(String[] keywords, String partialName, List<Completion> result) {
+ for (var k : keywords) {
+ if (matchesPartialName(k, partialName)) {
+ result.add(Completion.ofKeyword(k));
+ }
+ }
+ }
+
+ private void completeScopeIdentifiers(
+ URI uri, int line, int character, String partialName, List<Completion> result) {
+ var root = root(uri);
+ // Add locals
+ var locals = scopeMembers(uri, line, character, partialName);
+ for (var m : locals) {
+ result.add(Completion.ofElement(m));
+ }
+ LOG.info(String.format("...found %d locals", locals.size()));
+
+ // Add static imports
+ var staticImports = staticImports(uri, partialName);
+ for (var m : staticImports) {
+ result.add(Completion.ofElement(m));
+ }
+ LOG.info(String.format("...found %d static imports", staticImports.size()));
+
+ // Add classes
+ var startsWithUpperCase = partialName.length() > 0 && Character.isUpperCase(partialName.charAt(0));
+ if (startsWithUpperCase) {
+ var packageName = Objects.toString(root.getPackageName(), "");
+ Predicate<String> matchesPartialName =
+ qualifiedName -> {
+ var className = Parser.lastName(qualifiedName);
+ return matchesPartialName(className, partialName);
+ };
+
+ // Check JDK
+ LOG.info("...checking JDK");
+ for (var c : parent.jdkClasses) {
+ if (tooManyItems(result.size())) return;
+ if (!matchesPartialName.test(c)) continue;
+ if (isSamePackage(c, packageName) || isPublicClassFile(c)) {
+ result.add(Completion.ofClassName(c, isImported(uri, c)));
+ }
+ }
+
+ // Check classpath
+ LOG.info("...checking classpath");
+ var classPathNames = new HashSet<String>();
+ for (var c : parent.classPathClasses) {
+ if (tooManyItems(result.size())) return;
+ if (!matchesPartialName.test(c)) continue;
+ if (isSamePackage(c, packageName) || isPublicClassFile(c)) {
+ result.add(Completion.ofClassName(c, isImported(uri, c)));
+ classPathNames.add(c);
+ }
+ }
+
+ // Check sourcepath
+ LOG.info("...checking source path");
+ for (var file : FileStore.all()) {
+ if (tooManyItems(result.size())) return;
+ // If file is in the same package, any class defined in the file is accessible
+ var otherPackageName = FileStore.packageName(file);
+ var samePackage = otherPackageName.equals(packageName) || otherPackageName.isEmpty();
+ // If file is in a different package, only a public class with the same name as the file is accessible
+ var maybePublic = matchesPartialName(file.getFileName().toString(), partialName);
+ if (samePackage || maybePublic) {
+ result.addAll(accessibleClasses(uri, file, partialName, packageName, classPathNames));
+ }
+ }
+ }
+ }
+
+ private boolean isSamePackage(String className, String fromPackage) {
+ return Parser.mostName(className).equals(fromPackage);
+ }
+
+ private boolean isPublicClassFile(String className) {
+ try {
+ var platform =
+ parent.fileManager.getJavaFileForInput(
+ StandardLocation.PLATFORM_CLASS_PATH, className, JavaFileObject.Kind.CLASS);
+ if (platform != null) return isPublic(platform);
+ var classpath =
+ parent.fileManager.getJavaFileForInput(
+ StandardLocation.CLASS_PATH, className, JavaFileObject.Kind.CLASS);
+ if (classpath != null) return isPublic(classpath);
+ return false;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private boolean isPublic(JavaFileObject classFile) {
+ try (var in = classFile.openInputStream()) {
+ var header = ClassHeader.of(in);
+ return header.isPublic;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List<Completion> accessibleClasses(
+ URI fromUri, Path toFile, String partialName, String fromPackage, Set<String> skip) {
+ var parse = Parser.parse(toFile);
+ var toPackage = Objects.toString(parse.getPackageName(), "");
+ var samePackage = fromPackage.equals(toPackage) || toPackage.isEmpty();
+ var result = new ArrayList<Completion>();
+ for (var t : parse.getTypeDecls()) {
+ if (!(t instanceof ClassTree)) continue;
+ var cls = (ClassTree) t;
+ // If class is not accessible, skip it
+ var isPublic = cls.getModifiers().getFlags().contains(Modifier.PUBLIC);
+ if (!samePackage && !isPublic) continue;
+ // If class doesn't match partialName, skip it
+ var name = cls.getSimpleName().toString();
+ if (!matchesPartialName(name, partialName)) continue;
+ if (parse.getPackageName() != null) {
+ name = parse.getPackageName() + "." + name;
+ }
+ // If class was already autocompleted using the classpath, skip it
+ if (skip.contains(name)) continue;
+ // Otherwise, add this name!
+ result.add(Completion.ofClassName(name, isImported(fromUri, name)));
+ }
+ return result;
+ }
+
+ private List<Element> staticImports(URI uri, String partialName) {
+ var root = root(uri);
+ var result = new ArrayList<Element>();
+ for (var i : root.getImports()) {
+ if (!i.isStatic()) continue;
+ var id = (MemberSelectTree) i.getQualifiedIdentifier();
+ var path = trees.getPath(root, id.getExpression());
+ var el = (TypeElement) trees.getElement(path);
+ if (id.getIdentifier().contentEquals("*")) {
+ for (var member : el.getEnclosedElements()) {
+ if (matchesPartialName(member.getSimpleName(), partialName)
+ && member.getModifiers().contains(Modifier.STATIC)) {
+ result.add(member);
+ if (tooManyItems(result.size())) return result;
+ }
+ }
+ } else {
+ for (var member : el.getEnclosedElements()) {
+ if (matchesPartialName(member.getSimpleName(), partialName)
+ && member.getModifiers().contains(Modifier.STATIC)) {
+ result.add(member);
+ if (tooManyItems(result.size())) return result;
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /** Find the smallest tree that includes the cursor */
+ TreePath findPath(URI uri, int line, int character) {
+ var root = root(uri);
+ var trees = Trees.instance(borrow.task);
+ var pos = trees.getSourcePositions();
+ var cursor = root.getLineMap().getPosition(line, character);
+
+ // Search for the smallest element that encompasses line:column
+ class FindSmallest extends TreePathScanner<Void, Void> {
+ TreePath found = null;
+
+ boolean containsCursor(Tree tree) {
+ long start = pos.getStartPosition(root, tree), end = pos.getEndPosition(root, tree);
+ // If element has no position, give up
+ if (start == -1 || end == -1) return false;
+ // int x = 1, y = 2, ... requires special handling
+ if (tree instanceof VariableTree) {
+ var v = (VariableTree) tree;
+ // Get contents of source
+ String source;
+ try {
+ source = root.getSourceFile().getCharContent(true).toString();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ // Find name in contents
+ var name = v.getName().toString();
+ start = source.indexOf(name, (int) start);
+ if (start == -1) {
+ LOG.warning(String.format("Can't find name `%s` in variable declaration `%s`", name, v));
+ return false;
+ }
+ end = start + name.length();
+ }
+ // Check if `tree` contains line:column
+ return start <= cursor && cursor <= end;
+ }
+
+ @Override
+ public Void scan(Tree tree, Void nothing) {
+ // This is pre-order traversal, so the deepest element will be the last one remaining in `found`
+ if (containsCursor(tree)) {
+ found = new TreePath(getCurrentPath(), tree);
+ }
+ super.scan(tree, nothing);
+ return null;
+ }
+
+ @Override
+ public Void visitErroneous(ErroneousTree node, Void nothing) {
+ for (var t : node.getErrorTrees()) {
+ scan(t, nothing);
+ }
+ return null;
+ }
+ }
+ var find = new FindSmallest();
+ find.scan(root, null);
+ if (find.found == null) {
+ var message = String.format("No TreePath to %s %d:%d", uri, line, character);
+ throw new RuntimeException(message);
+ }
+ return find.found;
+ }
+
private static final Logger LOG = Logger.getLogger("main");
}