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