summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorge Fraser <george@fivetran.com>2019-04-01 23:49:30 -0700
committerGeorge Fraser <george@fivetran.com>2019-04-01 23:49:30 -0700
commit5ce53ab15a2cab08924f0dd4879c8f9c1936a882 (patch)
tree543a49cc5d76d8eb4d2bb2419f65ad9ff59f14e0
parent2aac2958d7d0c9c5ebf3cb80f6354c91167756d0 (diff)
downloadjava-language-server-5ce53ab15a2cab08924f0dd4879c8f9c1936a882.zip
Combine CompileFocus into CompileBatch
-rw-r--r--TODOS.md1
-rw-r--r--src/main/java/org/javacs/CompileBatch.java801
-rw-r--r--src/main/java/org/javacs/CompileFocus.java863
-rw-r--r--src/main/java/org/javacs/JavaCompilerService.java6
-rw-r--r--src/main/java/org/javacs/JavaLanguageServer.java20
-rw-r--r--src/test/java/org/javacs/JavaCompilerServiceTest.java28
6 files changed, 829 insertions, 890 deletions
diff --git a/TODOS.md b/TODOS.md
index 3272084..409d76b 100644
--- a/TODOS.md
+++ b/TODOS.md
@@ -11,6 +11,7 @@
- Go-to-subclasses
- Test coverage codelens
- Go-to-implementation for overridden methods
+- `Thing#close()` shows 0 references for `try (thing)`
## Bugs
- Imports MyEnum.* unnecessarily
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");
}
diff --git a/src/main/java/org/javacs/CompileFocus.java b/src/main/java/org/javacs/CompileFocus.java
deleted file mode 100644
index c1df941..0000000
--- a/src/main/java/org/javacs/CompileFocus.java
+++ /dev/null
@@ -1,863 +0,0 @@
-package org.javacs;
-
-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.*;
-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.TypeMirror;
-import javax.lang.model.util.Types;
-import javax.tools.JavaFileObject;
-import javax.tools.StandardLocation;
-
-public class CompileFocus implements AutoCloseable {
- public static final int MAX_COMPLETION_ITEMS = 50;
-
- private final JavaCompilerService parent;
- private final URI file;
- private final String contents;
- private final int line, character;
- private final TaskPool.Borrow borrow;
- private final Trees trees;
- private final Types types;
- private final CompilationUnitTree root;
- private final TreePath path;
-
- CompileFocus(JavaCompilerService parent, URI file, int line, int character) {
- this.parent = parent;
- this.file = file;
- this.contents = Pruner.prune(file, line, character);
- this.line = line;
- this.character = character;
- this.borrow = singleFileTask(parent, file, this.contents);
- this.trees = Trees.instance(borrow.task);
- this.types = borrow.task.getTypes();
-
- var profiler = new Profiler();
- borrow.task.addTaskListener(profiler);
- try {
- this.root = borrow.task.parse().iterator().next();
- // The results of task.analyze() are unreliable when errors are present
- // You can get at `Element` values using `Trees`
- borrow.task.analyze();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- profiler.print();
- this.path = findPath(borrow.task, root, line, character);
- }
-
- @Override
- public void close() {
- borrow.close();
- }
-
- // TODO this is highlighting incorrectly
- /** Create a task that compiles a single file */
- static TaskPool.Borrow singleFileTask(JavaCompilerService parent, URI file, String contents) {
- parent.diags.clear();
- return parent.compiler.getTask(
- null,
- parent.fileManager,
- parent.diags::add,
- JavaCompilerService.options(parent.classPath),
- Collections.emptyList(),
- List.of(new SourceFileObject(file, contents)));
- }
-
- /** Find the smallest element that includes the cursor */
- public Element element() {
- return trees.getElement(path);
- }
-
- public Optional<TreePath> path(Element e) {
- return Optional.ofNullable(trees.getPath(e));
- }
-
- /** Find all overloads for the smallest method call that includes the cursor */
- public Optional<MethodInvocation> methodInvocation() {
- LOG.info(String.format("Find method invocation around %s(%d,%d)...", file, line, character));
-
- for (var path = this.path; 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(this.path.getLeaf());
- LOG.info(String.format("...active parameter `%s` is %d", this.path.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(this.path.getLeaf());
- LOG.info(String.format("...active parameter `%s` is %d", this.path.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(boolean insideClass, boolean insideMethod, String partialName) {
- LOG.info(String.format("Completing identifiers starting with `%s`...", partialName));
-
- 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(file));
- 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(file).getFileName().toString();
- name = name.substring(0, name.length() - ".java".length());
- result.add(Completion.ofSnippet("class " + name, "class " + name + " {\n $0\n}"));
- }
- }
- // Add identifiers
- completeScopeIdentifiers(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(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()) {
- 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(partialName, result);
- return result;
- }
-
- /** Find all case options in the switch expression surrounding line:character */
- public List<Completion> completeCases() {
- LOG.info(String.format("Complete enum constants following `%s`...", path.getLeaf()));
-
- // Find surrounding switch
- var path = this.path;
- 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(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(boolean isReference) {
- 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() {
- var path = this.path;
- 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() {
- var thisType = enclosingClass();
- 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(String qualifiedName) {
- 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(String partialName) {
- 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(String partialName, List<Completion> result) {
- // Add locals
- var locals = scopeMembers(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(file, 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(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(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(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(Path file, String partialName, String fromPackage, Set<String> skip) {
- var parse = Parser.parse(file);
- 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(name)));
- }
- return result;
- }
-
- private List<Element> staticImports(URI file, String partialName) {
- 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 */
- static TreePath findPath(JavacTask task, CompilationUnitTree root, int line, int character) {
- var trees = Trees.instance(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 uri = root.getSourceFile().toUri();
- 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");
-}
diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java
index 871fefb..99fba71 100644
--- a/src/main/java/org/javacs/JavaCompilerService.java
+++ b/src/main/java/org/javacs/JavaCompilerService.java
@@ -79,8 +79,10 @@ public class JavaCompilerService {
return new ParseFile(this, file);
}
- public CompileFocus compileFocus(URI file, int line, int character) {
- return new CompileFocus(this, file, line, character);
+ public CompileBatch compileFocus(URI uri, int line, int character) {
+ var contents = Pruner.prune(uri, line, character);
+ var file = new SourceFileObject(uri, contents);
+ return compileBatch(List.of(file));
}
public CompileBatch compileBatch(Collection<URI> uris) {
diff --git a/src/main/java/org/javacs/JavaLanguageServer.java b/src/main/java/org/javacs/JavaLanguageServer.java
index c66b61b..5932e21 100644
--- a/src/main/java/org/javacs/JavaLanguageServer.java
+++ b/src/main/java/org/javacs/JavaLanguageServer.java
@@ -340,7 +340,7 @@ class JavaLanguageServer extends LanguageServer {
// TODO don't complete inside of comments
if (!maybeCtx.isPresent()) {
var items = new ArrayList<CompletionItem>();
- for (var name : CompileFocus.TOP_LEVEL_KEYWORDS) {
+ for (var name : CompileBatch.TOP_LEVEL_KEYWORDS) {
var i = new CompletionItem();
i.label = name;
i.kind = CompletionItemKind.Keyword;
@@ -357,23 +357,25 @@ class JavaLanguageServer extends LanguageServer {
// Do a specific type of completion
switch (ctx.kind) {
case MemberSelect:
- cs = focus.completeMembers(false);
+ cs = focus.completeMembers(uri, ctx.line, ctx.character, false);
isIncomplete = false;
break;
case MemberReference:
- cs = focus.completeMembers(true);
+ cs = focus.completeMembers(uri, ctx.line, ctx.character, true);
isIncomplete = false;
break;
case Identifier:
- cs = focus.completeIdentifiers(ctx.inClass, ctx.inMethod, ctx.partialName);
- isIncomplete = cs.size() >= CompileFocus.MAX_COMPLETION_ITEMS;
+ cs =
+ focus.completeIdentifiers(
+ uri, ctx.line, ctx.character, ctx.inClass, ctx.inMethod, ctx.partialName);
+ isIncomplete = cs.size() >= CompileBatch.MAX_COMPLETION_ITEMS;
break;
case Annotation:
- cs = focus.completeAnnotations(ctx.partialName);
- isIncomplete = cs.size() >= CompileFocus.MAX_COMPLETION_ITEMS;
+ cs = focus.completeAnnotations(uri, ctx.line, ctx.character, ctx.partialName);
+ isIncomplete = cs.size() >= CompileBatch.MAX_COMPLETION_ITEMS;
break;
case Case:
- cs = focus.completeCases();
+ cs = focus.completeCases(uri, ctx.line, ctx.character);
isIncomplete = false;
break;
default:
@@ -747,7 +749,7 @@ class JavaLanguageServer extends LanguageServer {
var line = position.position.line + 1;
var column = position.position.character + 1;
try (var focus = compiler.compileFocus(uri, line, column)) {
- var help = focus.methodInvocation().map(this::asSignatureHelp);
+ var help = focus.methodInvocation(uri, line, column).map(this::asSignatureHelp);
return help;
}
}
diff --git a/src/test/java/org/javacs/JavaCompilerServiceTest.java b/src/test/java/org/javacs/JavaCompilerServiceTest.java
index 0e5fd4d..ec41d26 100644
--- a/src/test/java/org/javacs/JavaCompilerServiceTest.java
+++ b/src/test/java/org/javacs/JavaCompilerServiceTest.java
@@ -60,7 +60,7 @@ public class JavaCompilerServiceTest {
@Test
public void element() {
var uri = resourceUri("HelloWorld.java");
- var found = compiler.compileFocus(uri, 3, 24).element();
+ var found = compiler.compileFocus(uri, 3, 24).element(uri, 3, 24).get();
assertThat(found.getSimpleName(), hasToString(containsString("println")));
}
@@ -68,7 +68,7 @@ public class JavaCompilerServiceTest {
@Test
public void elementWithError() {
var uri = resourceUri("CompleteMembers.java");
- var found = compiler.compileFocus(uri, 3, 12).element();
+ var found = compiler.compileFocus(uri, 3, 12).element(uri, 3, 12);
assertThat(found, notNullValue());
}
@@ -93,7 +93,7 @@ public class JavaCompilerServiceTest {
public void identifiers() {
var uri = resourceUri("CompleteIdentifiers.java");
var focus = compiler.compileFocus(uri, 13, 21);
- var found = focus.scopeMembers("complete");
+ var found = focus.scopeMembers(uri, 13, 21, "complete");
var names = elementNames(found);
assertThat(names, hasItem("completeLocal"));
assertThat(names, hasItem("completeParam"));
@@ -110,7 +110,7 @@ public class JavaCompilerServiceTest {
public void identifiersInMiddle() {
var uri = resourceUri("CompleteInMiddle.java");
var focus = compiler.compileFocus(uri, 13, 21);
- var found = focus.scopeMembers("complete");
+ var found = focus.scopeMembers(uri, 13, 21, "complete");
var names = elementNames(found);
assertThat(names, hasItem("completeLocal"));
assertThat(names, hasItem("completeParam"));
@@ -128,7 +128,7 @@ public class JavaCompilerServiceTest {
var uri = resourceUri("CompleteIdentifiers.java");
var ctx = compiler.parseFile(uri).completionContext(13, 21).get();
var focus = compiler.compileFocus(uri, ctx.line, ctx.character);
- var found = focus.completeIdentifiers(ctx.inClass, ctx.inMethod, ctx.partialName);
+ var found = focus.completeIdentifiers(uri, ctx.line, ctx.character, ctx.inClass, ctx.inMethod, ctx.partialName);
var names = completionNames(found);
assertThat(names, hasItem("completeLocal"));
assertThat(names, hasItem("completeParam"));
@@ -145,7 +145,7 @@ public class JavaCompilerServiceTest {
public void members() {
var uri = resourceUri("CompleteMembers.java");
var focus = compiler.compileFocus(uri, 3, 14);
- var found = focus.completeMembers(false);
+ var found = focus.completeMembers(uri, 3, 14, false);
var names = completionNames(found);
assertThat(names, hasItem("subMethod"));
assertThat(names, hasItem("superMethod"));
@@ -157,7 +157,7 @@ public class JavaCompilerServiceTest {
var uri = resourceUri("CompleteMembers.java");
var ctx = compiler.parseFile(uri).completionContext(3, 15).get();
var focus = compiler.compileFocus(uri, ctx.line, ctx.character);
- var found = focus.completeMembers(false);
+ var found = focus.completeMembers(uri, ctx.line, ctx.character, false);
var names = completionNames(found);
assertThat(names, hasItem("subMethod"));
assertThat(names, hasItem("superMethod"));
@@ -169,7 +169,7 @@ public class JavaCompilerServiceTest {
var uri = resourceUri("CompleteExpression.java");
var ctx = compiler.parseFile(uri).completionContext(3, 37).get();
var focus = compiler.compileFocus(uri, ctx.line, ctx.character);
- var found = focus.completeMembers(false);
+ var found = focus.completeMembers(uri, ctx.line, ctx.character, false);
var names = completionNames(found);
assertThat(names, hasItem("instanceMethod"));
assertThat(names, not(hasItem("create")));
@@ -181,7 +181,7 @@ public class JavaCompilerServiceTest {
var uri = resourceUri("CompleteClass.java");
var ctx = compiler.parseFile(uri).completionContext(3, 23).get();
var focus = compiler.compileFocus(uri, ctx.line, ctx.character);
- var found = focus.completeMembers(false);
+ var found = focus.completeMembers(uri, ctx.line, ctx.character, false);
var names = completionNames(found);
assertThat(names, hasItems("staticMethod", "staticField"));
assertThat(names, hasItems("class"));
@@ -194,7 +194,7 @@ public class JavaCompilerServiceTest {
var uri = resourceUri("CompleteImports.java");
var ctx = compiler.parseFile(uri).completionContext(1, 18).get();
var focus = compiler.compileFocus(uri, ctx.line, ctx.character);
- var found = focus.completeMembers(false);
+ var found = focus.completeMembers(uri, ctx.line, ctx.character, false);
var names = completionNames(found);
assertThat(names, hasItem("List"));
assertThat(names, hasItem("concurrent"));
@@ -203,7 +203,7 @@ public class JavaCompilerServiceTest {
@Test
public void overloads() {
var uri = resourceUri("Overloads.java");
- var found = compiler.compileFocus(uri, 3, 15).methodInvocation().get();
+ var found = compiler.compileFocus(uri, 3, 15).methodInvocation(uri, 3, 15).get();
var strings = found.overloads.stream().map(Object::toString).collect(Collectors.toList());
assertThat(strings, hasItem(containsString("print(int)")));
@@ -255,7 +255,7 @@ public class JavaCompilerServiceTest {
@Test
public void localDoc() {
var uri = resourceUri("LocalMethodDoc.java");
- var invocation = compiler.compileFocus(uri, 3, 21).methodInvocation().get();
+ var invocation = compiler.compileFocus(uri, 3, 21).methodInvocation(uri, 3, 21).get();
var method = invocation.activeMethod.get();
var ptr = new Ptr(method);
var file = compiler.docs().find(ptr).get();
@@ -274,7 +274,7 @@ public class JavaCompilerServiceTest {
@Test
public void matchesPartialName() {
- assertTrue(CompileFocus.matchesPartialName("foobar", "foo"));
- assertFalse(CompileFocus.matchesPartialName("foo", "foobar"));
+ assertTrue(CompileBatch.matchesPartialName("foobar", "foo"));
+ assertFalse(CompileBatch.matchesPartialName("foo", "foobar"));
}
}