diff options
author | George Fraser <george@fivetran.com> | 2019-04-01 23:49:30 -0700 |
---|---|---|
committer | George Fraser <george@fivetran.com> | 2019-04-01 23:49:30 -0700 |
commit | 5ce53ab15a2cab08924f0dd4879c8f9c1936a882 (patch) | |
tree | 543a49cc5d76d8eb4d2bb2419f65ad9ff59f14e0 | |
parent | 2aac2958d7d0c9c5ebf3cb80f6354c91167756d0 (diff) | |
download | java-language-server-5ce53ab15a2cab08924f0dd4879c8f9c1936a882.zip |
Combine CompileFocus into CompileBatch
-rw-r--r-- | TODOS.md | 1 | ||||
-rw-r--r-- | src/main/java/org/javacs/CompileBatch.java | 801 | ||||
-rw-r--r-- | src/main/java/org/javacs/CompileFocus.java | 863 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaCompilerService.java | 6 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaLanguageServer.java | 20 | ||||
-rw-r--r-- | src/test/java/org/javacs/JavaCompilerServiceTest.java | 28 |
6 files changed, 829 insertions, 890 deletions
@@ -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")); } } |