diff options
Diffstat (limited to 'src')
135 files changed, 4484 insertions, 8721 deletions
diff --git a/src/main/java/org/javacs/Artifact.java b/src/main/java/org/javacs/Artifact.java index 8d483c0..f0eeb7f 100644 --- a/src/main/java/org/javacs/Artifact.java +++ b/src/main/java/org/javacs/Artifact.java @@ -23,7 +23,7 @@ class Artifact { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Artifact artifact = (Artifact) o; + var artifact = (Artifact) o; return Objects.equals(groupId, artifact.groupId) && Objects.equals(artifactId, artifact.artifactId) && Objects.equals(version, artifact.version); @@ -33,4 +33,9 @@ class Artifact { public int hashCode() { return Objects.hash(groupId, artifactId, version); } + + @Override + public String toString() { + return String.format("%s:%s:%s", groupId, artifactId, version); + } } diff --git a/src/main/java/org/javacs/ChildFirstClassLoader.java b/src/main/java/org/javacs/ChildFirstClassLoader.java deleted file mode 100644 index 5818903..0000000 --- a/src/main/java/org/javacs/ChildFirstClassLoader.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.javacs; - -import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.logging.Logger; - -import org.javacs.Urls; - -class ChildFirstClassLoader extends URLClassLoader { - private final String[] packages; - private static final Logger LOG = Logger.getLogger("main"); - - private boolean loadFromChild(String className) { - for (String p : packages) { - if (className.startsWith(p)) return true; - } - - return false; - } - - static URL[] parseClassPath(String classPath) { - return Arrays.stream(classPath.split(File.pathSeparator)) - .map(Urls::pathToUrl) - .toArray(URL[]::new); - } - - static ChildFirstClassLoader fromClassPath( - String classPath, String[] packages, ClassLoader parent) { - return new ChildFirstClassLoader(parseClassPath(classPath), packages, parent); - } - - private ChildFirstClassLoader(URL[] urls, String[] packages, ClassLoader parent) { - super(urls, parent); - this.packages = packages; - } - - @Override - protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { - boolean fromChild = loadFromChild(name); - Class<?> c = findLoadedClass(name); - - if (c == null && fromChild) { - try { - c = findClass(name); - - LOG.info("Loaded " + c + " from child class loader"); - - return c; - } catch (ClassNotFoundException e) { - LOG.warning("Couldn't find " + name + " in child class loader"); - } - } - - if (c == null) c = super.loadClass(name, resolve); - - if (c != null && resolve) resolveClass(c); - - return c; - } -} diff --git a/src/main/java/org/javacs/ClassPathIndex.java b/src/main/java/org/javacs/ClassPathIndex.java deleted file mode 100644 index 89976b3..0000000 --- a/src/main/java/org/javacs/ClassPathIndex.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.javacs; - -import com.google.common.base.StandardSystemProperty; -import com.google.common.reflect.ClassPath; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Modifier; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Index the classpath *without* using the java compiler API. The classpath can contain problematic - * types, for example references to classes that *aren't* present. So we use reflection to find - * class names and constructors on the classpath. - * - * <p>The isn't the only way we inspect the classpath---when completing members, for example, we use - * the Javac API. This path is strictly for when we have to search the *entire* classpath. - */ -class ClassPathIndex { - - private final List<ClassPath.ClassInfo> topLevelClasses; - - ClassPathIndex(Set<Path> classPath) { - this.topLevelClasses = - classPath(classLoader(classPath)) - .getTopLevelClasses() - .stream() - .sorted(ClassPathIndex::shortestName) - .collect(Collectors.toList()); - } - - private static int shortestName(ClassPath.ClassInfo left, ClassPath.ClassInfo right) { - return Integer.compare(left.getSimpleName().length(), right.getSimpleName().length()); - } - - private static URL toUrl(Path path) { - try { - return path.toUri().toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - /** Find all the java 8 platform .jar files */ - static URL[] java8Platform(String javaHome) { - Path rt = Paths.get(javaHome).resolve("lib").resolve("rt.jar"); - - if (Files.exists(rt)) return new URL[] {toUrl(rt)}; - else throw new RuntimeException(rt + " does not exist"); - } - - /** Find all the java 9 platform .jmod files */ - static URL[] java9Platform(String javaHome) { - Path jmods = Paths.get(javaHome).resolve("jmods"); - - try { - return Files.list(jmods) - .filter(path -> path.getFileName().toString().endsWith(".jmod")) - .map(path -> toUrl(path)) - .toArray(URL[]::new); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static boolean isJava9() { - return StandardSystemProperty.JAVA_VERSION.value().equals("9"); - } - - private static URL[] platform() { - if (isJava9()) return java9Platform(StandardSystemProperty.JAVA_HOME.value()); - else return java8Platform(StandardSystemProperty.JAVA_HOME.value()); - } - - static URLClassLoader parentClassLoader() { - URL[] bootstrap = platform(); - - return new URLClassLoader(bootstrap, null); - } - - private static ClassPath classPath(URLClassLoader classLoader) { - try { - return ClassPath.from(classLoader); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static URLClassLoader classLoader(Set<Path> classPath) { - URL[] urls = classPath.stream().flatMap(ClassPathIndex::url).toArray(URL[]::new); - URLClassLoader platform = new URLClassLoader(platform(), null); - - return new URLClassLoader(urls, platform); - } - - private static Stream<URL> url(Path path) { - try { - return Stream.of(path.toUri().toURL()); - } catch (MalformedURLException e) { - LOG.warning(e.getMessage()); - - return Stream.empty(); - } - } - - Stream<ClassPath.ClassInfo> topLevelClasses() { - return topLevelClasses.stream(); - } - - boolean isAccessibleFromPackage(ClassPath.ClassInfo info, String fromPackage) { - return info.getPackageName().equals(fromPackage) || isPublic(info); - } - - private boolean isPublic(ClassPath.ClassInfo info) { - return Modifier.isPublic(info.load().getModifiers()); - } - - boolean hasAccessibleConstructor(ClassPath.ClassInfo info, String fromPackage) { - Class<?> load = info.load(); - boolean isPublicClass = Modifier.isPublic(load.getModifiers()), - isSamePackage = fromPackage.equals(info.getPackageName()); - - for (Constructor<?> candidate : load.getDeclaredConstructors()) { - int modifiers = candidate.getModifiers(); - - if (isPublicClass && Modifier.isPublic(modifiers)) return true; - else if (isSamePackage - && !Modifier.isPrivate(modifiers) - && !Modifier.isProtected(modifiers)) return true; - } - - return false; - } - - /** Find all packages in parentPackage */ - Stream<String> packagesStartingWith(String partialPackage) { - return topLevelClasses - .stream() - .filter(c -> c.getPackageName().startsWith(partialPackage)) - .map(c -> c.getPackageName()); - } - - Stream<ClassPath.ClassInfo> topLevelClassesIn(String parentPackage, String partialClass) { - Predicate<ClassPath.ClassInfo> matches = - c -> { - return c.getPackageName().equals(parentPackage) - && Completions.containsCharactersInOrder( - c.getSimpleName(), partialClass, false); - }; - - return topLevelClasses.stream().filter(matches); - } - - Optional<ClassPath.ClassInfo> loadPackage(String prefix) { - return topLevelClasses - .stream() - .filter(c -> c.getPackageName().startsWith(prefix)) - .findAny(); - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/ClassSource.java b/src/main/java/org/javacs/ClassSource.java new file mode 100644 index 0000000..ee5ffd3 --- /dev/null +++ b/src/main/java/org/javacs/ClassSource.java @@ -0,0 +1,32 @@ +package org.javacs; + +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +interface ClassSource { + Set<String> classes(); + + Class<?> load(String className); + + static final Logger LOG = Logger.getLogger("main"); + static final Set<String> failedToLoad = new HashSet<>(); + + default boolean isPublic(String className) { + if (failedToLoad.contains(className)) return false; + try { + var c = load(className); + return Modifier.isPublic(c.getModifiers()); + } catch (Exception e) { + LOG.warning(String.format("Failed to load %s: %s", className, e.getMessage())); + failedToLoad.add(className); + return false; + } + } + + default boolean isAccessibleFromPackage(String className, String fromPackage) { + var packageName = Parser.mostName(className); + return packageName.equals(fromPackage) || isPublic(className); + } +} diff --git a/src/main/java/org/javacs/Classes.java b/src/main/java/org/javacs/Classes.java new file mode 100644 index 0000000..2ada032 --- /dev/null +++ b/src/main/java/org/javacs/Classes.java @@ -0,0 +1,184 @@ +package org.javacs; + +import com.google.common.reflect.ClassPath; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +class Classes { + + /** All exported modules in the JDK */ + static String[] JDK_MODULES = { + "java.activation", + "java.base", + "java.compiler", + "java.corba", + "java.datatransfer", + "java.desktop", + "java.instrument", + "java.jnlp", + "java.logging", + "java.management", + "java.management.rmi", + "java.naming", + "java.prefs", + "java.rmi", + "java.scripting", + "java.se", + "java.se.ee", + "java.security.jgss", + "java.security.sasl", + "java.smartcardio", + "java.sql", + "java.sql.rowset", + "java.transaction", + "java.xml", + "java.xml.bind", + "java.xml.crypto", + "java.xml.ws", + "java.xml.ws.annotation", + "javafx.base", + "javafx.controls", + "javafx.fxml", + "javafx.graphics", + "javafx.media", + "javafx.swing", + "javafx.web", + "jdk.accessibility", + "jdk.attach", + "jdk.charsets", + "jdk.compiler", + "jdk.crypto.cryptoki", + "jdk.crypto.ec", + "jdk.dynalink", + "jdk.editpad", + "jdk.hotspot.agent", + "jdk.httpserver", + "jdk.incubator.httpclient", + "jdk.jartool", + "jdk.javadoc", + "jdk.jcmd", + "jdk.jconsole", + "jdk.jdeps", + "jdk.jdi", + "jdk.jdwp.agent", + "jdk.jfr", + "jdk.jlink", + "jdk.jshell", + "jdk.jsobject", + "jdk.jstatd", + "jdk.localedata", + "jdk.management", + "jdk.management.agent", + "jdk.management.cmm", + "jdk.management.jfr", + "jdk.management.resource", + "jdk.naming.dns", + "jdk.naming.rmi", + "jdk.net", + "jdk.pack", + "jdk.packager.services", + "jdk.rmic", + "jdk.scripting.nashorn", + "jdk.sctp", + "jdk.security.auth", + "jdk.security.jgss", + "jdk.snmp", + "jdk.xml.dom", + "jdk.zipfs", + }; + + static ClassSource jdkTopLevelClasses() { + LOG.info("Searching for top-level classes in the JDK"); + + var classes = new HashSet<String>(); + var fs = FileSystems.getFileSystem(URI.create("jrt:/")); + for (var m : JDK_MODULES) { + var moduleRoot = fs.getPath(String.format("/modules/%s/", m)); + try { + Files.walk(moduleRoot) + .forEach( + classFile -> { + var relative = moduleRoot.relativize(classFile).toString(); + if (relative.endsWith(".class") && !relative.contains("$")) { + var trim = relative.substring(0, relative.length() - ".class".length()); + var qualifiedName = trim.replace(File.separatorChar, '.'); + classes.add(qualifiedName); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + LOG.info(String.format("Found %d classes in the java platform", classes.size())); + + class PlatformClassSource implements ClassSource { + public Set<String> classes() { + return Collections.unmodifiableSet(classes); + } + + public Class<?> load(String className) { + try { + return ClassLoader.getPlatformClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + return new PlatformClassSource(); + } + + static ClassSource classPathTopLevelClasses(Set<Path> classPath) { + LOG.info(String.format("Searching for top-level classes in %d classpath locations", classPath.size())); + + Function<Path, URL> toUrl = + p -> { + try { + return p.toUri().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }; + var urls = classPath.stream().map(toUrl).toArray(URL[]::new); + var classLoader = new URLClassLoader(urls, null); + ClassPath scanner; + try { + scanner = ClassPath.from(classLoader); + } catch (IOException e) { + throw new RuntimeException(e); + } + var classes = scanner.getTopLevelClasses().stream().map(info -> info.getName()).collect(Collectors.toSet()); + + LOG.info(String.format("Found %d classes in classpath", classes.size())); + + class ClassPathClassSource implements ClassSource { + public Set<String> classes() { + return Collections.unmodifiableSet(classes); + } + + public Class<?> load(String className) { + try { + return classLoader.loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + return new ClassPathClassSource(); + } + + private static final Logger LOG = Logger.getLogger("main"); +} diff --git a/src/main/java/org/javacs/CodeActions.java b/src/main/java/org/javacs/CodeActions.java deleted file mode 100644 index 6a0a4ef..0000000 --- a/src/main/java/org/javacs/CodeActions.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.javacs; - -import com.google.common.collect.ImmutableList; -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; -import java.net.URI; -import java.util.List; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.eclipse.lsp4j.CodeActionParams; -import org.eclipse.lsp4j.Command; -import org.eclipse.lsp4j.Diagnostic; - -class CodeActions { - private final JavacHolder compiler; - private final URI file; - private final Optional<String> textContent; - private final JavacTask task; - private final CompilationUnitTree source; - private final SymbolIndex index; - - CodeActions( - JavacHolder compiler, - URI file, - Optional<String> textContent, - int line, - int character, - SymbolIndex index) { - this.compiler = compiler; - this.file = file; - this.textContent = textContent; - - FocusedResult compile = compiler.compileFocused(file, textContent, line, character, false); - - this.task = compile.task; - this.source = compile.compilationUnit; - this.index = index; - } - - public List<Command> find(CodeActionParams params) { - return params.getContext() - .getDiagnostics() - .stream() - .flatMap(diagnostic -> findCodeActionsForDiagnostic(diagnostic)) - .collect(Collectors.toList()); - } - - private Stream<Command> findCodeActionsForDiagnostic(Diagnostic diagnostic) { - return codeActionsFor(diagnostic); - } - - private Stream<Command> codeActionsFor(Diagnostic diagnostic) { - if (diagnostic.getCode().equals("compiler.err.cant.resolve.location")) { - return cannotFindSymbolClassName(diagnostic.getMessage()) - .map(Stream::of) - .orElseGet(Stream::empty) - .flatMap(this::addImportActions); - } else return Stream.empty(); - } - - /** Search for symbols on the classpath and sourcepath that match name */ - private Stream<Command> addImportActions(String name) { - Stream<Command> sourcePath = - index.allTopLevelClasses() - .filter(c -> c.className.equals(name)) - .map(c -> importClassCommand(c.packageName, c.className)); - Stream<Command> classPath = - compiler.classPathIndex - .topLevelClasses() - .filter(c -> c.getSimpleName().equals(name)) - .map(c -> importClassCommand(c.getPackageName(), c.getSimpleName())); - - return Stream.concat(sourcePath, classPath); - } - - private Command importClassCommand(String packageName, String className) { - return new Command( - "Import " + packageName + "." + className, - "Java.importClass", - ImmutableList.of(file.toString(), packageName, className)); - } - - private static final Pattern CANNOT_FIND_SYMBOL = Pattern.compile("class (\\w+)"); - - public static Optional<String> cannotFindSymbolClassName(String message) { - Matcher matcher = CANNOT_FIND_SYMBOL.matcher(message); - - if (matcher.find()) return Optional.of(matcher.group(1)); - else return Optional.empty(); - } -} diff --git a/src/main/java/org/javacs/Completion.java b/src/main/java/org/javacs/Completion.java new file mode 100644 index 0000000..4135744 --- /dev/null +++ b/src/main/java/org/javacs/Completion.java @@ -0,0 +1,46 @@ +package org.javacs; + +import javax.lang.model.element.Element; +import org.javacs.Completion.PackagePart; + +/** + * Union of the different types of completion provided by JavaCompilerService. Only one of the members will be non-null. + */ +public class Completion { + public final Element element; + public final PackagePart packagePart; + public final String keyword; + public final String notImportedClass; + + private Completion(Element element, PackagePart packagePart, String keyword, String notImportedClass) { + this.element = element; + this.packagePart = packagePart; + this.keyword = keyword; + this.notImportedClass = notImportedClass; + } + + public static Completion ofElement(Element element) { + return new Completion(element, null, null, null); + } + + public static Completion ofPackagePart(String fullName, String name) { + return new Completion(null, new PackagePart(fullName, name), null, null); + } + + public static Completion ofKeyword(String keyword) { + return new Completion(null, null, keyword, null); + } + + public static Completion ofNotImportedClass(String className) { + return new Completion(null, null, null, className); + } + + public static class PackagePart { + public final String fullName, name; + + public PackagePart(String fullName, String name) { + this.fullName = fullName; + this.name = name; + } + } +} diff --git a/src/main/java/org/javacs/CompletionResult.java b/src/main/java/org/javacs/CompletionResult.java new file mode 100644 index 0000000..34338ab --- /dev/null +++ b/src/main/java/org/javacs/CompletionResult.java @@ -0,0 +1,13 @@ +package org.javacs; + +import java.util.List; + +public class CompletionResult { + public final List<Completion> items; + public final boolean isIncomplete; + + public CompletionResult(List<Completion> items, boolean isIncomplete) { + this.items = items; + this.isIncomplete = isIncomplete; + } +} diff --git a/src/main/java/org/javacs/Completions.java b/src/main/java/org/javacs/Completions.java deleted file mode 100644 index 1550a92..0000000 --- a/src/main/java/org/javacs/Completions.java +++ /dev/null @@ -1,871 +0,0 @@ -package org.javacs; - -import com.google.common.reflect.ClassPath; -import com.sun.source.tree.*; -import com.sun.source.util.JavacTask; -import com.sun.source.util.TreePath; -import com.sun.source.util.Trees; -import com.sun.tools.javac.code.Flags; -import com.sun.tools.javac.code.Symbol; -import java.util.*; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import javax.lang.model.element.*; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.util.Elements; -import org.eclipse.lsp4j.Command; -import org.eclipse.lsp4j.CompletionItem; -import org.eclipse.lsp4j.CompletionItemKind; -import org.eclipse.lsp4j.InsertTextFormat; -import org.eclipse.lsp4j.TextEdit; - -class Completions { - - static Stream<CompletionItem> at(FocusedResult compiled, SymbolIndex index, Javadocs docs) { - Function<TreePath, Completions> newCompletions = - path -> new Completions(compiled.task, compiled.classPath, index, docs, path); - return compiled.cursor.map(newCompletions).map(Completions::get).orElseGet(Stream::empty); - } - - private final JavacTask task; - private final ClassPathIndex classPath; - private final SymbolIndex sourcePath; - private final Javadocs docs; - private final Trees trees; - private final Elements elements; - private final Name thisName, superName; - private final CompilationUnitTree compilationUnit; - private final TreePath path; - private final CursorContext context; - - private Completions( - JavacTask task, - ClassPathIndex classPath, - SymbolIndex sourcePath, - Javadocs docs, - TreePath path) { - this.task = task; - this.trees = Trees.instance(task); - this.elements = task.getElements(); - this.thisName = task.getElements().getName("this"); - this.superName = task.getElements().getName("super"); - this.classPath = classPath; - this.sourcePath = sourcePath; - this.docs = docs; - this.compilationUnit = path.getCompilationUnit(); - this.path = path; - this.context = CursorContext.from(path); - } - - private Stream<CompletionItem> get() { - Tree leaf = path.getLeaf(); - Scope scope = trees.getScope(path); - - if (leaf instanceof MemberSelectTree) { - MemberSelectTree select = (MemberSelectTree) leaf; - TreePath expressionPath = new TreePath(path.getParentPath(), select.getExpression()); - - return completeMembers( - expressionPath, partialIdentifier(select.getIdentifier()), scope); - } else if (leaf instanceof MemberReferenceTree) { - MemberReferenceTree select = (MemberReferenceTree) leaf; - TreePath expressionPath = - new TreePath(path.getParentPath(), select.getQualifierExpression()); - - return completeMembers(expressionPath, partialIdentifier(select.getName()), scope); - } else if (leaf instanceof IdentifierTree) { - IdentifierTree id = (IdentifierTree) leaf; - - return completeIdentifier(partialIdentifier(id.getName()), scope); - } else return Stream.empty(); - } - - private String partialIdentifier(Name name) { - if (name.contentEquals("<error>")) return ""; - else return name.toString(); - } - - /** Suggest all available identifiers */ - private Stream<CompletionItem> completeIdentifier(String partialIdentifier, Scope from) { - switch (context) { - case Import: - return packageMembers("", partialIdentifier).flatMap(this::completionItem); - case NewClass: - { - Predicate<ExecutableElement> accessible = - init -> - trees.isAccessible( - from, - init, - (DeclaredType) init.getEnclosingElement().asType()); - Stream<CompletionItem> alreadyImported = - alreadyImportedCompletions(partialIdentifier, from) - .flatMap(this::explodeConstructors) - .filter(accessible) - .flatMap(this::completionItem); - Stream<CompletionItem> notYetImported = - notImportedConstructors(partialIdentifier, from); - - return Stream.concat(alreadyImported, notYetImported); - } - default: - { - Predicate<Element> accessible = - e -> { - if (e == null) return false; - // Class names - else if (e instanceof TypeElement) - return trees.isAccessible(from, (TypeElement) e); - else if (e.getEnclosingElement() == null) return false; - // Members of other classes - else if (e.getEnclosingElement() instanceof DeclaredType) - return trees.isAccessible( - from, e, (DeclaredType) e.getEnclosingElement()); - // Local variables - else return true; - }; - Stream<CompletionItem> alreadyImported = - alreadyImportedCompletions(partialIdentifier, from) - .filter(accessible) - .flatMap(this::completionItem); - Stream<CompletionItem> notYetImported = - notImportedClasses(partialIdentifier, from); - - return Stream.concat(alreadyImported, notYetImported); - } - } - } - - /** Suggest all accessible members of expression */ - private Stream<CompletionItem> completeMembers( - TreePath expression, String partialIdentifier, Scope from) { - switch (context) { - case NewClass: - { - Predicate<ExecutableElement> isAccessible = - init -> - trees.isAccessible( - from, - init, - (DeclaredType) init.getEnclosingElement().asType()); - return allMembers(expression, partialIdentifier, from, false) - .flatMap(this::explodeConstructors) - .filter(isAccessible) - .flatMap(this::completionItem); - } - case Import: - { - Predicate<Element> isAccessible = - member -> - !(member instanceof TypeElement) - || trees.isAccessible(from, (TypeElement) member); - return allMembers(expression, partialIdentifier, from, false) - .filter(isAccessible) - .flatMap(this::completionItem); - } - default: - { - DeclaredType type = - (DeclaredType) task.getTypes().erasure(trees.getTypeMirror(expression)); - - return allMembers( - expression, - partialIdentifier, - from, - context == CursorContext.Reference) - .filter(member -> member.getKind() != ElementKind.CONSTRUCTOR) - .filter(e -> trees.isAccessible(from, e, type)) - .flatMap(this::completionItem); - } - } - } - - private Stream<? extends Element> allMembers( - TreePath expression, String partialIdentifier, Scope from, boolean isMethodReference) { - Element element = trees.getElement(expression); - - if (element == null) return Stream.empty(); - - // com.foo.? - if (element instanceof PackageElement) { - PackageElement packageElement = (PackageElement) element; - - return packageMembers(packageElement.getQualifiedName().toString(), partialIdentifier); - } - // MyClass.? - else if (element instanceof TypeElement) { - // OuterClass.this, OuterClass.super - Stream<? extends Element> thisAndSuper = - thisScopes(from) - .stream() - .filter(scope -> scope.getEnclosingClass().equals(element)) - .flatMap(this::thisAndSuper); - // MyClass.? - Predicate<Element> isAccessible = - e -> { - return (isMethodReference && e.getKind() == ElementKind.METHOD) - || e.getModifiers().contains(Modifier.STATIC); - }; - Stream<? extends Element> members = - elements.getAllMembers((TypeElement) element).stream().filter(isAccessible); - // MyClass.class - Element dotClass = - new Symbol.VarSymbol( - Flags.PUBLIC | Flags.STATIC | Flags.FINAL, - (com.sun.tools.javac.util.Name) task.getElements().getName("class"), - (com.sun.tools.javac.code.Type) element.asType(), - (Symbol) element); - - Predicate<Element> matchesPartialIdentifier = - e -> containsCharactersInOrder(e.getSimpleName(), partialIdentifier, false); - - return Stream.concat(Stream.of(dotClass), Stream.concat(thisAndSuper, members)) - .filter(matchesPartialIdentifier); - } - // myValue.? - else { - DeclaredType type = - (DeclaredType) task.getTypes().erasure(trees.getTypeMirror(expression)); - List<? extends Element> members = - elements.getAllMembers((TypeElement) type.asElement()); - - return members.stream() - .filter(e -> !e.getModifiers().contains(Modifier.STATIC)) - .filter( - e -> - containsCharactersInOrder( - e.getSimpleName(), partialIdentifier, false)); - } - } - - private Stream<? extends Element> packageMembers( - String parentPackage, String partialIdentifier) { - // Source-path packages that match parentPackage.partialIdentifier - Stream<PackageElement> packages = subPackages(parentPackage, partialIdentifier); - Stream<TypeElement> sourcePathClasses = - sourcePathClassesInPackage(parentPackage, partialIdentifier); - Stream<TypeElement> classPathClasses = - classPath - .topLevelClassesIn(parentPackage, partialIdentifier) - .flatMap(this::loadFromClassPath); - - return Stream.concat(packages, Stream.concat(sourcePathClasses, classPathClasses)); - } - - private Stream<TypeElement> loadFromClassPath(ClassPath.ClassInfo info) { - TypeElement found = elements.getTypeElement(info.getName()); - - if (found == null) return Stream.empty(); - else return Stream.of(found); - } - - /** All sub-packages of parentPackage that match partialIdentifier */ - private Stream<PackageElement> subPackages(String parentPackage, String partialIdentifier) { - String prefix = parentPackage.isEmpty() ? "" : parentPackage + "."; - Stream<String> sourcePathMembers = - sourcePath - .allTopLevelClasses() - .map(c -> c.packageName) - .filter(packageName -> packageName.startsWith(prefix)); - Stream<String> classPathMembers = classPath.packagesStartingWith(prefix); - Set<String> next = - Stream.concat(sourcePathMembers, classPathMembers) - .map(p -> p.substring(prefix.length())) - .map(Completions::firstId) - .filter(p -> containsCharactersInOrder(p, partialIdentifier, false)) - .collect(Collectors.toSet()); - - // Load 1 member of package to force javac to recognize that it exists - for (String part : next) { - classPath.loadPackage(prefix + part).ifPresent(this::tryLoad); - } - - return next.stream() - .map(last -> elements.getPackageElement(prefix + last)) - .filter(Objects::nonNull); - } - - private boolean isAlreadyImported(String qualifiedName) { - String packageName = mostIds(qualifiedName); - - if (packageName.equals("java.lang")) return true; - - if (packageName.equals(Objects.toString(compilationUnit.getPackageName(), ""))) return true; - - for (ImportTree each : compilationUnit.getImports()) { - if (each.isStatic()) continue; - - String importName = importId(each); - - if (isStarImport(each) && mostIds(importName).equals(packageName)) return true; - else if (importName.equals(qualifiedName)) return true; - } - - return false; - } - - private static boolean isStarImport(ImportTree tree) { - String importName = importId(tree); - - return importName.endsWith(".*"); - } - - private static String importId(ImportTree tree) { - return tree.getQualifiedIdentifier().toString(); - } - - static String firstId(String qualifiedName) { - int firstDot = qualifiedName.indexOf('.'); - - if (firstDot == -1) return qualifiedName; - else return qualifiedName.substring(0, firstDot); - } - - static String mostIds(String qualifiedName) { - int lastDot = qualifiedName.lastIndexOf('.'); - - if (lastDot == -1) return ""; - else return qualifiedName.substring(0, lastDot); - } - - static String lastId(String qualifiedName) { - int lastDot = qualifiedName.lastIndexOf('.'); - - if (lastDot == -1) return qualifiedName; - else return qualifiedName.substring(lastDot + 1); - } - - private Stream<ExecutableElement> explodeConstructors(Element element) { - if (element.getKind() != ElementKind.CLASS) return Stream.empty(); - - try { - return elements.getAllMembers((TypeElement) element) - .stream() - .filter(e -> e.getKind() == ElementKind.CONSTRUCTOR) - .map(e -> (ExecutableElement) e); - } catch (Symbol.CompletionFailure failed) { - LOG.warning(failed.getMessage()); - - return Stream.empty(); - } - } - - /** Suggest constructors that haven't yet been imported, but are on the source or class path */ - private Stream<CompletionItem> notImportedConstructors(String partialIdentifier, Scope scope) { - String packageName = packageName(scope); - Stream<CompletionItem> fromSourcePath = - accessibleSourcePathClasses(partialIdentifier, scope) - .filter(c -> c.hasAccessibleConstructor(packageName)) - .map(this::completeConstructorFromSourcePath); - Stream<CompletionItem> fromClassPath = - accessibleClassPathClasses(partialIdentifier, scope) - .filter(c -> classPath.hasAccessibleConstructor(c, packageName)) - .map(c -> completeConstructorFromClassPath(c.load())); - // TODO remove class path classes that are also available from source path - - return Stream.concat(fromSourcePath, fromClassPath); - } - - private CompletionItem completeConstructorFromSourcePath(ReachableClass c) { - return completeConstructor(c.packageName, c.className, c.hasTypeParameters); - } - - private CompletionItem completeConstructorFromClassPath(Class<?> c) { - return completeConstructor( - c.getPackage().getName(), c.getSimpleName(), c.getTypeParameters().length > 0); - } - - /** Suggest classes that haven't yet been imported, but are on the source or class path */ - private Stream<CompletionItem> notImportedClasses(String partialIdentifier, Scope scope) { - Stream<String> fromSourcePath = - accessibleSourcePathClasses(partialIdentifier, scope).map(c -> c.qualifiedName()); - Stream<String> fromClassPath = - accessibleClassPathClasses(partialIdentifier, scope) - .map(c -> c.getName()) - .filter(name -> !sourcePath.isTopLevelClass(name)); - - return Stream.concat(fromSourcePath, fromClassPath) - .map(this::completeClassNameFromClassPath); - } - - private Stream<ClassPath.ClassInfo> accessibleClassPathClasses( - String partialIdentifier, Scope scope) { - String packageName = packageName(scope); - - return classPath - .topLevelClasses() - .filter(c -> containsCharactersInOrder(c.getSimpleName(), partialIdentifier, false)) - .filter(c -> classPath.isAccessibleFromPackage(c, packageName)); - } - - private Stream<ReachableClass> accessibleSourcePathClasses( - String partialIdentifier, Scope scope) { - String packageName = packageName(scope); - - return sourcePath - .accessibleTopLevelClasses(packageName) - .filter(c -> containsCharactersInOrder(c.className, partialIdentifier, false)); - } - - private String packageName(Scope scope) { - PackageElement packageEl = elements.getPackageOf(scope.getEnclosingClass()); - - return packageEl == null ? "" : packageEl.getQualifiedName().toString(); - } - - /** Suggest all completions that are visible from scope */ - private Stream<? extends Element> alreadyImportedCompletions( - String partialIdentifier, Scope scope) { - Predicate<Element> matchesName = - e -> containsCharactersInOrder(e.getSimpleName(), partialIdentifier, false); - return alreadyImportedSymbols(scope).filter(matchesName); - } - - private Stream<TypeElement> tryLoad(ClassPath.ClassInfo c) { - try { - TypeElement type = elements.getTypeElement(c.getName()); - - if (type != null) return Stream.of(type); - else return Stream.empty(); - } catch (Symbol.CompletionFailure failed) { - LOG.warning(failed.getMessage()); - - return Stream.empty(); - } - } - - private Stream<? extends Element> alreadyImportedSymbols(Scope scope) { - Collection<TypeElement> thisScopes = scopeClasses(thisScopes(scope)); - Collection<TypeElement> classScopes = classScopes(scope); - List<Scope> methodScopes = methodScopes(scope); - Stream<? extends Element> staticImports = - compilationUnit.getImports().stream().flatMap(this::staticImports); - Stream<? extends Element> elements = Stream.empty(); - - if (!isStaticMethod(scope)) elements = Stream.concat(elements, thisAndSuper(scope)); - - elements = Stream.concat(elements, methodScopes.stream().flatMap(this::locals)); - elements = Stream.concat(elements, thisScopes.stream().flatMap(this::instanceMembers)); - elements = Stream.concat(elements, classScopes.stream().flatMap(this::staticMembers)); - elements = Stream.concat(elements, staticImports); - - return elements; - } - - private Stream<TypeElement> sourcePathClassesInPackage( - String packageName, String partialClass) { - return sourcePath - .allTopLevelClasses() - .filter( - c -> - c.packageName.equals(packageName) - && containsCharactersInOrder( - c.className, partialClass, false)) - .map(ReachableClass::qualifiedName) - .flatMap(this::loadFromSourcePath); - } - - private Stream<TypeElement> loadFromSourcePath(String name) { - TypeElement found = elements.getTypeElement(name); - - if (found == null) return Stream.empty(); - else return Stream.of(found); - } - - private Stream<? extends Element> staticImports(ImportTree tree) { - if (!tree.isStatic()) return Stream.empty(); - - if (isStarImport(tree)) { - String parentName = mostIds(importId(tree)); - TypeElement parentElement = elements.getTypeElement(parentName); - - if (parentElement == null) { - LOG.warning("Can't find " + parentName); - - return Stream.empty(); - } - - return parentElement.getEnclosedElements().stream(); - } else { - String name = importId(tree); - String className = mostIds(name); - String memberName = lastId(name); - TypeElement classElement = elements.getTypeElement(className); - - if (classElement == null) { - LOG.warning("Can't find " + className); - - return Stream.empty(); - } - - for (Element each : classElement.getEnclosedElements()) { - if (each.getSimpleName().contentEquals(memberName)) return Stream.of(each); - } - - LOG.warning("Couldn't find " + memberName + " in " + className); - - return Stream.empty(); - } - } - - private List<Scope> thisScopes(Scope scope) { - List<Scope> acc = new ArrayList<>(); - - while (scope != null && scope.getEnclosingClass() != null) { - TypeElement each = scope.getEnclosingClass(); - boolean staticMethod = isStaticMethod(scope); - boolean staticClass = each.getModifiers().contains(Modifier.STATIC); - boolean anonymousClass = isAnonymousClass(each); - - // If this scope is a static method, terminate - if (staticMethod) break; - // If the user has indicated this is a static class, it's the last scope in the chain - else if (staticClass && !anonymousClass) { - acc.add(scope); - - break; - } - // If this is an inner class, add it to the chain and keep going - else { - acc.add(scope); - - scope = scope.getEnclosingScope(); - } - } - - return acc; - } - - private Collection<TypeElement> scopeClasses(Collection<Scope> scopes) { - Map<Name, TypeElement> acc = new LinkedHashMap<>(); - - for (Scope scope : scopes) { - TypeElement each = scope.getEnclosingClass(); - - acc.put(each.getQualifiedName(), each); - } - - return acc.values(); - } - - private List<Scope> methodScopes(Scope scope) { - List<Scope> acc = new ArrayList<>(); - - while (scope != null && scope.getEnclosingClass() != null) { - if (scope.getEnclosingMethod() != null) acc.add(scope); - - scope = scope.getEnclosingScope(); - } - - return acc; - } - - private Collection<TypeElement> classScopes(Scope scope) { - Map<Name, TypeElement> acc = new LinkedHashMap<>(); - - while (scope != null && scope.getEnclosingClass() != null) { - TypeElement each = scope.getEnclosingClass(); - - acc.putIfAbsent(each.getQualifiedName(), each); - - scope = scope.getEnclosingScope(); - } - - return acc.values(); - } - - private Stream<? extends Element> instanceMembers(TypeElement enclosingClass) { - return elements.getAllMembers(enclosingClass) - .stream() - .filter(each -> !each.getModifiers().contains(Modifier.STATIC)); - } - - private Stream<? extends Element> staticMembers(TypeElement enclosingClass) { - return elements.getAllMembers(enclosingClass) - .stream() - .filter(each -> each.getModifiers().contains(Modifier.STATIC)); - } - - private Stream<? extends Element> locals(Scope scope) { - return StreamSupport.stream(scope.getLocalElements().spliterator(), false) - .filter(e -> !isThisOrSuper(e)); - } - - private Stream<? extends Element> thisAndSuper(Scope scope) { - return StreamSupport.stream(scope.getLocalElements().spliterator(), false) - .filter(e -> isThisOrSuper(e)); - } - - private boolean isStaticMethod(Scope scope) { - return scope.getEnclosingMethod() != null - && scope.getEnclosingMethod().getModifiers().contains(Modifier.STATIC); - } - - private boolean isAnonymousClass(Element candidate) { - return candidate != null - && candidate instanceof TypeElement - && ((TypeElement) candidate).getNestingKind() == NestingKind.ANONYMOUS; - } - - private boolean isThisOrSuper(Element each) { - Name name = each.getSimpleName(); - - return name.equals(thisName) || name.equals(superName); - } - - private static final Command TRIGGER_SIGNATURE_HELP = - new Command("", "editor.action.triggerParameterHints"); - - /** - * Complete constructor with minimal type information. - * - * <p>This is important when we're autocompleting new ? with a class that we haven't yet - * imported. We don't yet have detailed type information or javadocs, and it's expensive to - * retrieve them. So we autocomplete a minimal constructor, and let signature-help fill in the - * details. - */ - private CompletionItem completeConstructor( - String packageName, String className, boolean hasTypeParameters) { - CompletionItem item = new CompletionItem(); - String qualifiedName = packageName.isEmpty() ? className : packageName + "." + className; - String key = String.format("%s#<init>", className); - String insertText = className; - - if (hasTypeParameters) insertText += "<>"; - - insertText += "($0)"; - - item.setKind(CompletionItemKind.Constructor); - item.setLabel(className); - item.setDetail(packageName); - item.setInsertText(insertText); - item.setInsertTextFormat(InsertTextFormat.Snippet); - item.setCommand(TRIGGER_SIGNATURE_HELP); - item.setFilterText(className); - item.setAdditionalTextEdits(addImport(qualifiedName)); - item.setSortText("3/" + className); - item.setData(key); - - return item; - } - - private CompletionItem completeClassNameFromClassPath(String qualifiedName) { - CompletionItem item = new CompletionItem(); - String packageName = mostIds(qualifiedName), simpleName = lastId(qualifiedName); - - item.setLabel(simpleName); - item.setDetail(packageName); - item.setInsertText(simpleName); - item.setAdditionalTextEdits(addImport(qualifiedName)); - item.setSortText("3/" + simpleName); - item.setData(qualifiedName); - - // TODO implement vscode resolve-completion-item - - return item; - } - - private Stream<CompletionItem> completionItem(Element e) { - try { - String name = e.getSimpleName().toString(); - - switch (e.getKind()) { - case PACKAGE: - { - PackageElement p = (PackageElement) e; - CompletionItem item = new CompletionItem(); - String id = lastId(p.getSimpleName().toString()); - - item.setKind(CompletionItemKind.Module); - item.setLabel(id); - item.setInsertText(id); - item.setSortText("1/" + id); - - return Stream.of(item); - } - case ENUM: - case INTERFACE: - case ANNOTATION_TYPE: - case CLASS: - { - TypeElement type = (TypeElement) e; - int order = isAlreadyImported(type.getQualifiedName().toString()) ? 1 : 2; - CompletionItem item = new CompletionItem(); - - item.setKind(classKind(e.getKind())); - item.setLabel(name); - item.setDetail(type.getQualifiedName().toString()); - item.setInsertText(name); - - PackageElement classPackage = elements.getPackageOf(e); - if (classPackage != null) - item.setDetail(classPackage.getQualifiedName().toString()); - - item.setAdditionalTextEdits( - addImport(((TypeElement) e).getQualifiedName().toString())); - item.setSortText(order + "/" + name); - item.setData(type.getQualifiedName().toString()); - - return Stream.of(item); - } - case TYPE_PARAMETER: - { - CompletionItem item = new CompletionItem(); - - item.setKind(CompletionItemKind.Reference); - item.setLabel(name); - item.setSortText("1/" + name); - - return Stream.of(item); - } - case ENUM_CONSTANT: - { - CompletionItem item = new CompletionItem(); - - item.setKind(CompletionItemKind.Enum); - item.setLabel(name); - item.setDetail(e.getEnclosingElement().getSimpleName().toString()); - item.setSortText("1/" + name); - - return Stream.of(item); - } - case FIELD: - { - CompletionItem item = new CompletionItem(); - boolean isField = e.getEnclosingElement().getKind() == ElementKind.CLASS; - - item.setKind( - isField - ? CompletionItemKind.Property - : CompletionItemKind.Variable); - item.setLabel(name); - item.setDetail(ShortTypePrinter.print(e.asType())); - item.setSortText(String.format("%s/%s", isField ? 1 : 0, name)); - - return Stream.of(item); - } - case PARAMETER: - case LOCAL_VARIABLE: - case EXCEPTION_PARAMETER: - { - CompletionItem item = new CompletionItem(); - - item.setKind(CompletionItemKind.Variable); - item.setLabel(name); - item.setSortText("1/" + name); - - return Stream.of(item); - } - case METHOD: - { - ExecutableElement method = (ExecutableElement) e; - CompletionItem item = new CompletionItem(); - - item.setKind(CompletionItemKind.Method); - item.setLabel(name); - item.setDetail(Hovers.methodSignature(method, true, false)); - if (context != CursorContext.Reference) item.setInsertText(name + "($0)"); - item.setInsertTextFormat(InsertTextFormat.Snippet); - item.setCommand(TRIGGER_SIGNATURE_HELP); - item.setFilterText(name); - item.setSortText("1/" + name); - item.setData(docs.methodKey(method)); - - return Stream.of(item); - } - case CONSTRUCTOR: - { - TypeElement enclosingClass = (TypeElement) e.getEnclosingElement(); - int order = - isAlreadyImported(enclosingClass.getQualifiedName().toString()) - ? 2 - : 3; - name = enclosingClass.getSimpleName().toString(); - - ExecutableElement method = (ExecutableElement) e; - CompletionItem item = new CompletionItem(); - String insertText = name; - - if (!enclosingClass.getTypeParameters().isEmpty()) insertText += "<>"; - - insertText += "($0)"; - - item.setKind(CompletionItemKind.Constructor); - item.setLabel(name); - item.setDetail(Hovers.methodSignature(method, false, false)); - item.setInsertText(insertText); - item.setInsertTextFormat(InsertTextFormat.Snippet); - item.setCommand(TRIGGER_SIGNATURE_HELP); - item.setFilterText(name); - item.setAdditionalTextEdits( - addImport(enclosingClass.getQualifiedName().toString())); - item.setSortText(order + "/" + name); - item.setData(docs.methodKey(method)); - - return Stream.of(item); - } - case STATIC_INIT: - case INSTANCE_INIT: - case OTHER: - case RESOURCE_VARIABLE: - default: - // Nothing user-enterable - // Nothing user-enterable - return Stream.empty(); - } - } catch (Symbol.CompletionFailure failed) { - LOG.warning(failed.getMessage()); - - return Stream.empty(); - } - } - - private CompletionItemKind classKind(ElementKind kind) { - switch (kind) { - case CLASS: - return CompletionItemKind.Class; - case ANNOTATION_TYPE: - case INTERFACE: - return CompletionItemKind.Interface; - case ENUM: - return CompletionItemKind.Enum; - default: - throw new RuntimeException("Expected CLASS, INTERFACE or ENUM but found " + kind); - } - } - - private List<TextEdit> addImport(String qualifiedName) { - if (!isAlreadyImported(qualifiedName) && CursorContext.from(path) != CursorContext.Import) - return new RefactorFile(task, compilationUnit) - .addImport(mostIds(qualifiedName), lastId(qualifiedName)); - else return Collections.emptyList(); - } - - public static boolean containsCharactersInOrder( - CharSequence candidate, CharSequence pattern, boolean caseSensitive) { - int iCandidate = 0, iPattern = 0; - - while (iCandidate < candidate.length() && iPattern < pattern.length()) { - char patternChar = pattern.charAt(iPattern); - char testChar = candidate.charAt(iCandidate); - - if (!caseSensitive) { - patternChar = Character.toLowerCase(patternChar); - testChar = Character.toLowerCase(testChar); - } - - if (patternChar == testChar) { - iPattern++; - iCandidate++; - } else iCandidate++; - } - - return iPattern == pattern.length(); - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/Configured.java b/src/main/java/org/javacs/Configured.java deleted file mode 100644 index 444e5ac..0000000 --- a/src/main/java/org/javacs/Configured.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.javacs; - -class Configured { - final JavacHolder compiler; - final Javadocs docs; - final SymbolIndex index; - final FindSymbols find; - - Configured(JavacHolder compiler, Javadocs docs, SymbolIndex index, FindSymbols find) { - this.compiler = compiler; - this.docs = docs; - this.index = index; - this.find = find; - } -} diff --git a/src/main/java/org/javacs/CursorContext.java b/src/main/java/org/javacs/CursorContext.java deleted file mode 100644 index d5a9ebf..0000000 --- a/src/main/java/org/javacs/CursorContext.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.javacs; - -import com.sun.source.tree.ImportTree; -import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.Tree; -import com.sun.source.util.TreePath; - -enum CursorContext { - NewClass(Tree.Kind.NEW_CLASS), - Import(Tree.Kind.IMPORT), - Reference(Tree.Kind.MEMBER_REFERENCE), - Other(null); - - final Tree.Kind kind; - - TreePath find(final TreePath path) { - if (this.kind == null) return path; - else { - TreePath search = path; - - while (search != null) { - if (this.kind == search.getLeaf().getKind()) return search; - else search = search.getParentPath(); - } - - return path; - } - } - - CursorContext(Tree.Kind kind) { - this.kind = kind; - } - - /** - * Is this identifier or member embedded in an important context, for example: - * - * <p>new OuterClass.InnerClass| import package.Class| - */ - static CursorContext from(TreePath path) { - if (path == null) return Other; - else - switch (path.getLeaf().getKind()) { - case MEMBER_REFERENCE: - return Reference; - case MEMBER_SELECT: - case IDENTIFIER: - return fromIdentifier(path.getParentPath(), path.getLeaf()); - case NEW_CLASS: - return NewClass; - case IMPORT: - return Import; - default: - return Other; - } - } - - private static CursorContext fromIdentifier(TreePath parent, Tree id) { - if (parent == null) return Other; - else - switch (parent.getLeaf().getKind()) { - case MEMBER_SELECT: - case MEMBER_REFERENCE: - case IDENTIFIER: - return fromIdentifier(parent.getParentPath(), parent.getLeaf()); - case NEW_CLASS: - { - NewClassTree leaf = (NewClassTree) parent.getLeaf(); - - if (leaf.getIdentifier() == id) return NewClass; - else return Other; - } - case IMPORT: - { - ImportTree leaf = (ImportTree) parent.getLeaf(); - - if (leaf.getQualifiedIdentifier() == id) return Import; - else return Other; - } - default: - return Other; - } - } -} diff --git a/src/main/java/org/javacs/Docs.java b/src/main/java/org/javacs/Docs.java new file mode 100644 index 0000000..2e5e5e0 --- /dev/null +++ b/src/main/java/org/javacs/Docs.java @@ -0,0 +1,221 @@ +package org.javacs; + +import com.overzealous.remark.Options; +import com.overzealous.remark.Remark; +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.tree.*; +import com.sun.source.util.DocTrees; +import com.sun.source.util.TreeScanner; +import com.sun.source.util.Trees; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.util.*; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.tools.*; + +class Docs { + + /** File manager with source-path + platform sources, which we will use to look up individual source files */ + private final StandardJavaFileManager fileManager; + + private static Path srcZip() { + try { + var fs = FileSystems.newFileSystem(Lib.SRC_ZIP, Docs.class.getClassLoader()); + return fs.getPath("/"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + Docs(Set<Path> sourcePath) { + this.fileManager = + ServiceLoader.load(JavaCompiler.class).iterator().next().getStandardFileManager(__ -> {}, null, null); + + // Compute the full source path, including src.zip for platform classes + var sourcePathFiles = sourcePath.stream().map(Path::toFile).collect(Collectors.toSet()); + + try { + fileManager.setLocation(StandardLocation.SOURCE_PATH, sourcePathFiles); + fileManager.setLocationFromPaths(StandardLocation.MODULE_SOURCE_PATH, Set.of(srcZip())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private Optional<JavaFileObject> file(String className) { + try { + var fromSourcePath = + fileManager.getJavaFileForInput( + StandardLocation.SOURCE_PATH, className, JavaFileObject.Kind.SOURCE); + if (fromSourcePath != null) return Optional.of(fromSourcePath); + for (var module : Classes.JDK_MODULES) { + var moduleLocation = fileManager.getLocationForModule(StandardLocation.MODULE_SOURCE_PATH, module); + if (moduleLocation == null) continue; + var fromModuleSourcePath = + fileManager.getJavaFileForInput(moduleLocation, className, JavaFileObject.Kind.SOURCE); + if (fromModuleSourcePath != null) return Optional.of(fromModuleSourcePath); + } + return Optional.empty(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private boolean memberNameEquals(Tree member, String name) { + if (member instanceof VariableTree) { + var variable = (VariableTree) member; + return variable.getName().contentEquals(name); + } else if (member instanceof MethodTree) { + var method = (MethodTree) member; + return method.getName().contentEquals(name); + } else return false; + } + + private Optional<DocCommentTree> findDoc(String className, String memberName) { + var file = file(className); + if (!file.isPresent()) return Optional.empty(); + var task = Parser.parseTask(file.get()); + CompilationUnitTree root; + try { + var it = task.parse().iterator(); + if (!it.hasNext()) { + LOG.warning("Found no CompilationUnitTree in " + file); + return Optional.empty(); + } + root = it.next(); + } catch (IOException e) { + throw new RuntimeException(e); + } + var docs = DocTrees.instance(task); + var trees = Trees.instance(task); + class Find extends TreeScanner<Void, Void> { + Optional<DocCommentTree> result = Optional.empty(); + + @Override + public Void visitClass(ClassTree node, Void aVoid) { + // TODO this will be wrong when inner class has same name as top-level class + if (node.getSimpleName().contentEquals(Parser.lastName(className))) { + if (memberName == null) { + var path = trees.getPath(root, node); + result = Optional.ofNullable(docs.getDocCommentTree(path)); + } else { + for (var member : node.getMembers()) { + if (memberNameEquals(member, memberName)) { + var path = trees.getPath(root, member); + result = Optional.ofNullable(docs.getDocCommentTree(path)); + } + } + } + } + return null; + } + } + var find = new Find(); + find.scan(root, null); + return find.result; + } + + Optional<DocCommentTree> memberDoc(String className, String memberName) { + Objects.requireNonNull(className); + Objects.requireNonNull(memberName); + + return findDoc(className, memberName); + } + + Optional<DocCommentTree> classDoc(String className) { + Objects.requireNonNull(className); + + return findDoc(className, null); + } + + private boolean sameMethod(MethodTree candidate, String methodName, List<String> parameterTypes) { + if (!candidate.getName().contentEquals(methodName)) return false; + var params = candidate.getParameters(); + if (params.size() != parameterTypes.size()) return false; + for (int i = 0; i < params.size(); i++) { + var expect = parameterTypes.get(i); + var expectSimple = Parser.lastName(expect); + var p = params.get(i); + var t = p.getType(); + if (!(t instanceof IdentifierTree)) { + LOG.warning( + "Parameter " + p.getName() + " of method " + candidate.getName() + " is not an IdentifierTree"); + return false; + } + var id = (IdentifierTree) t; + var simple = Parser.lastName(id.getName().toString()); + + if (!simple.equals(expectSimple)) return false; + } + return true; + } + + Optional<MethodTree> findMethod(String className, String methodName, List<String> parameterTypes) { + Objects.requireNonNull(className); + Objects.requireNonNull(methodName); + + var file = file(className); + if (!file.isPresent()) return Optional.empty(); + var task = Parser.parseTask(file.get()); + CompilationUnitTree root; + try { + root = task.parse().iterator().next(); + } catch (IOException e) { + throw new RuntimeException(e); + } + class Find extends TreeScanner<Void, Void> { + Optional<MethodTree> result = Optional.empty(); + + @Override + public Void visitClass(ClassTree node, Void aVoid) { + // TODO this will be wrong when inner class has same name as top-level class + if (node.getSimpleName().contentEquals(Parser.lastName(className))) { + for (var member : node.getMembers()) { + if (member instanceof MethodTree) { + var method = (MethodTree) member; + if (sameMethod(method, methodName, parameterTypes)) result = Optional.of(method); + } + } + } + return null; + } + } + var find = new Find(); + find.scan(root, null); + return find.result; + } + + private static final Pattern HTML_TAG = Pattern.compile("<(\\w+)>"); + + private static boolean isHtml(String text) { + var tags = HTML_TAG.matcher(text); + while (tags.find()) { + var tag = tags.group(1); + var close = String.format("</%s>", tag); + var findClose = text.indexOf(close, tags.end()); + if (findClose != -1) return true; + } + return false; + } + + // TODO is this still necessary? + /** If `commentText` looks like HTML, convert it to markdown */ + static String htmlToMarkdown(String commentText) { + if (isHtml(commentText)) { + var options = new Options(); + + options.tables = Options.Tables.CONVERT_TO_CODE_BLOCK; + options.hardwraps = true; + options.inlineLinks = true; + options.autoLinks = true; + options.reverseHtmlSmartPunctuation = true; + + return new Remark(options).convertFragment(commentText); + } else return commentText; + } + + private static final Logger LOG = Logger.getLogger("main"); +} diff --git a/src/main/java/org/javacs/EmptyRootDoc.java b/src/main/java/org/javacs/EmptyRootDoc.java deleted file mode 100644 index 36b2571..0000000 --- a/src/main/java/org/javacs/EmptyRootDoc.java +++ /dev/null @@ -1,175 +0,0 @@ -package org.javacs; - -import com.sun.javadoc.*; - -class EmptyRootDoc implements RootDoc { - static final EmptyRootDoc INSTANCE = new EmptyRootDoc(); - - private EmptyRootDoc() {} - - @Override - public String[][] options() { - return new String[0][]; - } - - @Override - public PackageDoc[] specifiedPackages() { - return new PackageDoc[0]; - } - - @Override - public ClassDoc[] specifiedClasses() { - return new ClassDoc[0]; - } - - @Override - public ClassDoc[] classes() { - return new ClassDoc[0]; - } - - @Override - public PackageDoc packageNamed(String name) { - return null; - } - - @Override - public ClassDoc classNamed(String qualifiedName) { - return null; - } - - @Override - public String commentText() { - return null; - } - - @Override - public Tag[] tags() { - return new Tag[0]; - } - - @Override - public Tag[] tags(String tagname) { - return new Tag[0]; - } - - @Override - public SeeTag[] seeTags() { - return new SeeTag[0]; - } - - @Override - public Tag[] inlineTags() { - return new Tag[0]; - } - - @Override - public Tag[] firstSentenceTags() { - return new Tag[0]; - } - - @Override - public String getRawCommentText() { - return null; - } - - @Override - public void setRawCommentText(String rawDocumentation) {} - - @Override - public String name() { - return null; - } - - @Override - public int compareTo(Object obj) { - return 0; - } - - @Override - public boolean isField() { - return false; - } - - @Override - public boolean isEnumConstant() { - return false; - } - - @Override - public boolean isConstructor() { - return false; - } - - @Override - public boolean isMethod() { - return false; - } - - @Override - public boolean isAnnotationTypeElement() { - return false; - } - - @Override - public boolean isInterface() { - return false; - } - - @Override - public boolean isException() { - return false; - } - - @Override - public boolean isError() { - return false; - } - - @Override - public boolean isEnum() { - return false; - } - - @Override - public boolean isAnnotationType() { - return false; - } - - @Override - public boolean isOrdinaryClass() { - return false; - } - - @Override - public boolean isClass() { - return false; - } - - @Override - public boolean isIncluded() { - return false; - } - - @Override - public SourcePosition position() { - return null; - } - - @Override - public void printError(String msg) {} - - @Override - public void printError(SourcePosition pos, String msg) {} - - @Override - public void printWarning(String msg) {} - - @Override - public void printWarning(SourcePosition pos, String msg) {} - - @Override - public void printNotice(String msg) {} - - @Override - public void printNotice(SourcePosition pos, String msg) {} -} diff --git a/src/main/java/org/javacs/ExistingImports.java b/src/main/java/org/javacs/ExistingImports.java new file mode 100644 index 0000000..28e133a --- /dev/null +++ b/src/main/java/org/javacs/ExistingImports.java @@ -0,0 +1,15 @@ +package org.javacs; + +import java.util.Set; + +class ExistingImports { + /** Fully-qualified names of classes that have been imported on the source path */ + final Set<String> classes; + /** Package names from star-imports like `import java.util.*` */ + final Set<String> packages; + + ExistingImports(Set<String> classes, Set<String> packages) { + this.classes = classes; + this.packages = packages; + } +} diff --git a/src/main/java/org/javacs/FindCursor.java b/src/main/java/org/javacs/FindCursor.java deleted file mode 100644 index 18464c7..0000000 --- a/src/main/java/org/javacs/FindCursor.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.javacs; - -import com.sun.source.tree.*; -import com.sun.source.util.*; -import java.util.Optional; - -class FindCursor { - - public static Optional<TreePath> find( - JavacTask task, CompilationUnitTree source, int line, int column) { - SourcePositions sourcePositions = Trees.instance(task).getSourcePositions(); - long offset = source.getLineMap().getPosition(line, column); - - class Finished extends RuntimeException { - final TreePath found; - - Finished(TreePath of) { - found = of; - } - } - - class Search extends TreePathScanner<Void, Void> { - @Override - public Void scan(Tree leaf, Void nothing) { - if (containsCursor(leaf)) { - super.scan(leaf, nothing); - - throw new Finished(new TreePath(getCurrentPath(), leaf)); - } else return null; - } - - boolean containsCursor(Tree leaf) { - long start = sourcePositions.getStartPosition(source, leaf); - long end = sourcePositions.getEndPosition(source, leaf); - - return start <= offset && offset <= end; - } - - @Override - public Void visitErroneous(ErroneousTree node, Void nothing) { - return super.scan(node.getErrorTrees(), nothing); - } - } - - try { - new Search().scan(source, null); - - return Optional.empty(); - } catch (Finished found) { - return Optional.of(found.found); - } - } -} diff --git a/src/main/java/org/javacs/FindSymbols.java b/src/main/java/org/javacs/FindSymbols.java deleted file mode 100644 index 9dd04d9..0000000 --- a/src/main/java/org/javacs/FindSymbols.java +++ /dev/null @@ -1,264 +0,0 @@ -package org.javacs; - -import com.sun.source.tree.*; -import com.sun.source.util.JavacTask; -import com.sun.source.util.TreePath; -import com.sun.source.util.TreePathScanner; -import com.sun.source.util.Trees; -import java.net.URI; -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.lang.model.element.*; -import javax.tools.Diagnostic; -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; - -class FindSymbols { - private final SymbolIndex index; - private final JavacHolder compiler; - private final Function<URI, Optional<String>> activeContent; - - FindSymbols( - SymbolIndex index, - JavacHolder compiler, - Function<URI, Optional<String>> activeContent) { - this.index = index; - this.compiler = compiler; - this.activeContent = activeContent; - } - - /** - * Find a symbol in its file. - * - * <p>It's possible that `symbol` comes from a .class file where the corresponding .java file - * was not visited during incremental compilation. In order to be sure we have access to the - * source positions, we will recompile the .java file where `symbol` was declared. - */ - Optional<Location> find(Element symbol) { - index.updateOpenFiles(); - - return findFile(symbol).flatMap(file -> findIn(symbol, file)); - } - - /** Find all references to a symbol */ - Stream<Location> references(Element symbol) { - String name = symbol.getSimpleName().toString(); - - return findReferences(index.potentialReferences(name), symbol).stream(); - } - - private void visitElements(URI source, BiConsumer<JavacTask, Element> forEach) { - Map<URI, Optional<String>> todo = - Collections.singletonMap(source, activeContent.apply(source)); - - compiler.compileBatch( - todo, - (task, compilationUnit) -> { - Trees trees = Trees.instance(task); - - new TreePathScanner<Void, Void>() { - @Override - public Void visitClass(ClassTree node, Void aVoid) { - addDeclaration(); - - return super.visitClass(node, aVoid); - } - - @Override - public Void visitMethod(MethodTree node, Void aVoid) { - addDeclaration(); - - return super.visitMethod(node, aVoid); - } - - @Override - public Void visitVariable(VariableTree node, Void aVoid) { - addDeclaration(); - - return super.visitVariable(node, aVoid); - } - - private void addDeclaration() { - Element el = trees.getElement(getCurrentPath()); - - forEach.accept(task, el); - } - }.scan(compilationUnit, null); - }); - } - - private Optional<Location> findIn(Element symbol, URI file) { - List<Location> result = new ArrayList<>(); - - visitElements( - file, - (task, found) -> { - if (sameSymbol(symbol, found)) { - findElementName(found, Trees.instance(task)).ifPresent(result::add); - } - }); - - if (!result.isEmpty()) return Optional.of(result.get(0)); - else return Optional.empty(); - } - - private List<Location> findReferences(Collection<URI> files, Element target) { - List<Location> found = new ArrayList<>(); - Map<URI, Optional<String>> todo = - files.stream().collect(Collectors.toMap(uri -> uri, activeContent)); - - compiler.compileBatch( - todo, - (task, compilationUnit) -> { - Trees trees = Trees.instance(task); - - new TreePathScanner<Void, Void>() { - @Override - public Void visitMemberSelect(MemberSelectTree node, Void aVoid) { - addReference(); - - return super.visitMemberSelect(node, aVoid); - } - - @Override - public Void visitMemberReference(MemberReferenceTree node, Void aVoid) { - addReference(); - - return super.visitMemberReference(node, aVoid); - } - - @Override - public Void visitNewClass(NewClassTree node, Void aVoid) { - addReference(); - - return super.visitNewClass(node, aVoid); - } - - @Override - public Void visitIdentifier(IdentifierTree node, Void aVoid) { - addReference(); - - return super.visitIdentifier(node, aVoid); - } - - private void addReference() { - Element symbol = trees.getElement(getCurrentPath()); - - if (sameSymbol(target, symbol)) - findPath(getCurrentPath(), trees).ifPresent(found::add); - } - }.scan(compilationUnit, null); - }); - - return found; - } - - private static Optional<Location> findPath(TreePath path, Trees trees) { - CompilationUnitTree compilationUnit = path.getCompilationUnit(); - long start = trees.getSourcePositions().getStartPosition(compilationUnit, path.getLeaf()); - long end = trees.getSourcePositions().getEndPosition(compilationUnit, path.getLeaf()); - - if (start == Diagnostic.NOPOS) return Optional.empty(); - - if (end == Diagnostic.NOPOS) end = start; - - int startLine = (int) compilationUnit.getLineMap().getLineNumber(start); - int startColumn = (int) compilationUnit.getLineMap().getColumnNumber(start); - int endLine = (int) compilationUnit.getLineMap().getLineNumber(end); - int endColumn = (int) compilationUnit.getLineMap().getColumnNumber(end); - - return Optional.of( - new Location( - compilationUnit.getSourceFile().toUri().toString(), - new Range( - new Position(startLine - 1, startColumn - 1), - new Position(endLine - 1, endColumn - 1)))); - } - - private Optional<URI> findFile(Element symbol) { - return topLevelClass(symbol).flatMap(index::findDeclaringFile); - } - - private Optional<TypeElement> topLevelClass(Element symbol) { - TypeElement result = null; - - while (symbol != null) { - if (symbol instanceof TypeElement) result = (TypeElement) symbol; - - symbol = symbol.getEnclosingElement(); - } - - return Optional.ofNullable(result); - } - - private static String qualifiedName(Element s) { - StringJoiner acc = new StringJoiner("."); - - createQualifiedName(s, acc); - - return acc.toString(); - } - - private static void createQualifiedName(Element s, StringJoiner acc) { - if (s != null) { - createQualifiedName(s.getEnclosingElement(), acc); - - if (s instanceof PackageElement) - acc.add(((PackageElement) s).getQualifiedName().toString()); - else if (s.getSimpleName().length() != 0) acc.add(s.getSimpleName().toString()); - } - } - - /** Find a more accurate position for symbol by searching for its name. */ - private static Optional<Location> findElementName(Element symbol, Trees trees) { - TreePath path = trees.getPath(symbol); - Name name = - symbol.getKind() == ElementKind.CONSTRUCTOR - ? symbol.getEnclosingElement().getSimpleName() - : symbol.getSimpleName(); - - return SymbolIndex.findTreeName(name, path, trees); - } - - private static boolean sameSymbol(Element target, Element symbol) { - return symbol != null - && target != null - && toStringEquals(symbol.getEnclosingElement(), target.getEnclosingElement()) - && toStringEquals(symbol, target); - } - - private static boolean toStringEquals(Object left, Object right) { - return Objects.equals(Objects.toString(left, ""), Objects.toString(right, "")); - } - - private static boolean shouldIndex(Element symbol) { - if (symbol == null) return false; - - ElementKind kind = symbol.getKind(); - - switch (kind) { - case ENUM: - case ANNOTATION_TYPE: - case INTERFACE: - case ENUM_CONSTANT: - case FIELD: - case METHOD: - return true; - case CLASS: - return !isAnonymous(symbol); - case CONSTRUCTOR: - // TODO also skip generated constructors - return !isAnonymous(symbol.getEnclosingElement()); - default: - return false; - } - } - - private static boolean isAnonymous(Element symbol) { - return symbol.getSimpleName().toString().isEmpty(); - } -} diff --git a/src/main/java/org/javacs/FixImports.java b/src/main/java/org/javacs/FixImports.java new file mode 100644 index 0000000..751c756 --- /dev/null +++ b/src/main/java/org/javacs/FixImports.java @@ -0,0 +1,17 @@ +package org.javacs; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.util.SourcePositions; +import java.util.Set; + +class FixImports { + final CompilationUnitTree parsed; + final SourcePositions sourcePositions; + final Set<String> fixedImports; + + FixImports(CompilationUnitTree parsed, SourcePositions sourcePositions, Set<String> fixedImports) { + this.parsed = parsed; + this.sourcePositions = sourcePositions; + this.fixedImports = fixedImports; + } +} diff --git a/src/main/java/org/javacs/FocusedResult.java b/src/main/java/org/javacs/FocusedResult.java deleted file mode 100644 index 53c9d2d..0000000 --- a/src/main/java/org/javacs/FocusedResult.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.javacs; - -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; -import com.sun.source.util.TreePath; -import java.util.Optional; - -class FocusedResult { - final CompilationUnitTree compilationUnit; - final Optional<TreePath> cursor; - final JavacTask task; - final ClassPathIndex classPath; - - FocusedResult( - CompilationUnitTree compilationUnit, - Optional<TreePath> cursor, - JavacTask task, - ClassPathIndex classPath) { - this.compilationUnit = compilationUnit; - this.cursor = cursor; - this.task = task; - this.classPath = classPath; - } -} diff --git a/src/main/java/org/javacs/Hovers.java b/src/main/java/org/javacs/Hovers.java deleted file mode 100644 index 2909d37..0000000 --- a/src/main/java/org/javacs/Hovers.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.javacs; - -import com.sun.javadoc.MethodDoc; -import com.sun.javadoc.ProgramElementDoc; -import com.sun.tools.javac.code.Type; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.StringJoiner; -import java.util.stream.Collectors; -import javax.lang.model.element.*; -import javax.lang.model.type.TypeMirror; -import org.eclipse.lsp4j.Hover; -import org.eclipse.lsp4j.jsonrpc.messages.Either; - -public class Hovers { - - public static Hover hoverText(Element el, Javadocs docs) { - Optional<String> doc = docs.doc(el).map(Hovers::commentText).map(Javadocs::htmlToMarkdown); - String sig = signature(el); - String result = - doc.map(text -> String.format("```java\n%s\n```\n%s", sig, text)).orElse(sig); - - return new Hover(Collections.singletonList(Either.forLeft(result)), null); - } - - private static String commentText(ProgramElementDoc doc) { - if (doc instanceof MethodDoc) { - MethodDoc method = (MethodDoc) doc; - - return Javadocs.commentText(method).orElse(""); - } else return doc.commentText(); - } - - private static String signature(Element el) { - if (el.getKind() == ElementKind.CONSTRUCTOR) { - ExecutableElement method = (ExecutableElement) el; - TypeElement enclosingClass = (TypeElement) method.getEnclosingElement(); - - return enclosingClass.getQualifiedName() + "(" + params(method.getParameters()) + ")"; - } - if (el instanceof ExecutableElement) { - ExecutableElement method = (ExecutableElement) el; - - return method.getReturnType() - + " " - + method.getSimpleName() - + "(" - + params(method.getParameters()) - + ")"; - } else if (el instanceof TypeElement) { - TypeElement type = (TypeElement) el; - - return type.getQualifiedName().toString(); - } else return el.asType().toString(); - } - - private static String params(List<? extends VariableElement> params) { - return params.stream().map(p -> p.asType().toString()).collect(Collectors.joining(", ")); - } - - public static String methodSignature( - ExecutableElement e, boolean showReturn, boolean showMethodName) { - String name = - e.getKind() == ElementKind.CONSTRUCTOR - ? constructorName(e) - : e.getSimpleName().toString(); - boolean varargs = e.isVarArgs(); - StringJoiner params = new StringJoiner(", "); - - List<? extends VariableElement> parameters = e.getParameters(); - for (int i = 0; i < parameters.size(); i++) { - VariableElement p = parameters.get(i); - String pName = shortName(p, varargs && i == parameters.size() - 1); - - params.add(pName); - } - - String signature = ""; - - if (showReturn) signature += ShortTypePrinter.print(e.getReturnType()) + " "; - - if (showMethodName) signature += name; - - signature += "(" + params + ")"; - - if (!e.getThrownTypes().isEmpty()) { - StringJoiner thrown = new StringJoiner(", "); - - for (TypeMirror t : e.getThrownTypes()) thrown.add(ShortTypePrinter.print(t)); - - signature += " throws " + thrown; - } - - return signature; - } - - public static String shortName(VariableElement p, boolean varargs) { - TypeMirror type = p.asType(); - - if (varargs) { - Type.ArrayType array = (Type.ArrayType) type; - - type = array.getComponentType(); - } - - String acc = shortTypeName(type); - String name = p.getSimpleName().toString(); - - if (varargs) acc += "..."; - - if (!name.matches("arg\\d+")) acc += " " + name; - - return acc; - } - - private static String shortTypeName(TypeMirror type) { - return ShortTypePrinter.print(type); - } - - private static String constructorName(ExecutableElement e) { - return e.getEnclosingElement().getSimpleName().toString(); - } -} diff --git a/src/main/java/org/javacs/IncrementalFileManager.java b/src/main/java/org/javacs/IncrementalFileManager.java deleted file mode 100644 index 9334632..0000000 --- a/src/main/java/org/javacs/IncrementalFileManager.java +++ /dev/null @@ -1,319 +0,0 @@ -package org.javacs; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.google.common.collect.Sets; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.tree.Tree; -import com.sun.source.util.JavacTask; -import com.sun.tools.javac.api.JavacTool; -import java.io.IOException; -import java.net.URI; -import java.time.Instant; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Logger; -import javax.lang.model.element.TypeElement; -import javax.tools.*; -import javax.tools.JavaFileObject.Kind; -import org.javacs.pubapi.*; - -/** - * An implementation of JavaFileManager that removes any .java source files where there is an - * up-to-date .class file - */ -class IncrementalFileManager extends ForwardingJavaFileManager<JavaFileManager> { - private final Set<URI> warnedHidden = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private final JavacTool javac = JavacTool.create(); - private final JavaFileManager classOnlyFileManager; - - IncrementalFileManager(JavaFileManager delegate) { - super(delegate); - - classOnlyFileManager = - new ForwardingJavaFileManager<JavaFileManager>(delegate) { - @Override - public Iterable<JavaFileObject> list( - Location location, String packageName, Set<Kind> kinds, boolean recurse) - throws IOException { - if (location == StandardLocation.SOURCE_PATH) - kinds = Sets.filter(kinds, k -> k != Kind.SOURCE); - - return super.list(location, packageName, kinds, recurse); - } - - @Override - public JavaFileObject getJavaFileForInput( - Location location, String className, JavaFileObject.Kind kind) - throws IOException { - if (kind == Kind.SOURCE) return null; - else return super.getJavaFileForInput(location, className, kind); - } - - @Override - public FileObject getFileForInput( - Location location, String packageName, String relativeName) - throws IOException { - if (location == StandardLocation.SOURCE_PATH) return null; - else return super.getFileForInput(location, packageName, relativeName); - } - }; - } - - @Override - public Iterable<JavaFileObject> list( - Location location, String packageName, Set<Kind> kinds, boolean recurse) - throws IOException { - Iterable<JavaFileObject> list = super.list(location, packageName, kinds, recurse); - - if (location == StandardLocation.SOURCE_PATH) - return Iterables.filter(list, source -> !hasUpToDateClassFiles(packageName, source)); - else return list; - } - - @Override - public JavaFileObject getJavaFileForInput( - Location location, String className, JavaFileObject.Kind kind) throws IOException { - if (location == StandardLocation.SOURCE_PATH && hasUpToDateClassFile(className)) - return null; - else return super.getJavaFileForInput(location, className, kind); - } - - @Override - public FileObject getFileForInput(Location location, String packageName, String relativeName) - throws IOException { - String className = packageName.isEmpty() ? relativeName : packageName + "." + relativeName; - - if (location == StandardLocation.SOURCE_PATH && hasUpToDateClassFile(className)) - return null; - else return super.getFileForInput(location, packageName, relativeName); - } - - private boolean hasUpToDateClassFiles(String packageName, JavaFileObject sourceFile) { - Optional<JavaFileObject> outputFile = primaryClassFile(packageName, sourceFile); - boolean hidden = - outputFile.isPresent() - && outputFile.get().getLastModified() - >= sourceFile.getLastModified() - || hasUpToDateSignatures(packageName, sourceFile); - - if (hidden && !warnedHidden.contains(sourceFile.toUri())) { - LOG.warning("Hiding " + sourceFile.toUri() + " in favor of " + outputFile.orElse(null)); - - warnedHidden.add(sourceFile.toUri()); - } - - return hidden; - } - - /** - * Cache of whether a particular source file had up-to-date class files at a particular time. - * - * <p>We're going to assume that if there were up-to-date class files, and the lastModified time - * of the source file has not changed, there are still up-to-date class files. - */ - private Map<CheckedSignature, Boolean> upToDate = new HashMap<>(); - - private static class CheckedSignature { - final URI sourceUri; - final Instant lastModified; - - public CheckedSignature(URI sourceUri, Instant lastModified) { - this.sourceUri = sourceUri; - this.lastModified = lastModified; - } - - @Override - public boolean equals(Object maybe) { - if (maybe instanceof CheckedSignature) { - CheckedSignature that = (CheckedSignature) maybe; - - return this.sourceUri.equals(that.sourceUri) - && this.lastModified.equals(that.lastModified); - } else return false; - } - - @Override - public int hashCode() { - return Objects.hash(sourceUri, lastModified); - } - } - - private boolean hasUpToDateSignatures(String packageName, JavaFileObject sourceFile) { - CheckedSignature key = - new CheckedSignature( - sourceFile.toUri(), Instant.ofEpochMilli(sourceFile.getLastModified())); - - return upToDate.computeIfAbsent(key, __ -> computeUpToDate(packageName, sourceFile)); - } - - private boolean computeUpToDate(String packageName, JavaFileObject sourceFile) { - try { - JavacTask task = - javac.getTask( - null, - classOnlyFileManager, - __ -> {}, - ImmutableList.of(), - null, - ImmutableList.of(sourceFile)); - CompilationUnitTree tree = task.parse().iterator().next(); - - for (Tree each : tree.getTypeDecls()) { - if (each instanceof ClassTree) { - ClassTree sourceClass = (ClassTree) each; - String qualifiedName = - packageName.isEmpty() - ? sourceClass.getSimpleName().toString() - : packageName + "." + sourceClass.getSimpleName(); - - if (!hasUpToDateSignature(qualifiedName)) { - JavaFileObject classFile = - super.getJavaFileForInput( - StandardLocation.CLASS_PATH, - qualifiedName, - JavaFileObject.Kind.CLASS); - - if (classFile != null) { - LOG.warning( - String.format( - "%s has a different signature than %s", - sourceFile.toUri(), classFile)); - } - - return false; - } - } - } - - LOG.info( - String.format( - "%s appears to be out-of-date but its class files have a matching public API", - sourceFile.toUri())); - - return true; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private Optional<JavaFileObject> primaryClassFile( - String packageName, JavaFileObject sourceFile) { - String primaryClassName = primaryClassSimpleName(sourceFile); - String qualifiedName = - packageName.isEmpty() ? primaryClassName : packageName + "." + primaryClassName; - - try { - return Optional.ofNullable( - super.getJavaFileForInput( - StandardLocation.CLASS_PATH, qualifiedName, JavaFileObject.Kind.CLASS)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private String primaryClassSimpleName(JavaFileObject sourceFile) { - String[] filePath = sourceFile.toUri().getPath().split("/"); - - assert filePath.length > 0 : sourceFile + " has not path"; - - String fileName = filePath[filePath.length - 1]; - - assert fileName.endsWith(".java") : sourceFile + " does not end with .java"; - - return fileName.substring(0, fileName.length() - ".java".length()); - } - - private boolean hasUpToDateClassFile(String qualifiedName) { - try { - JavaFileObject - sourceFile = - super.getJavaFileForInput( - StandardLocation.SOURCE_PATH, - qualifiedName, - JavaFileObject.Kind.SOURCE), - outputFile = - super.getJavaFileForInput( - StandardLocation.CLASS_PATH, - qualifiedName, - JavaFileObject.Kind.CLASS); - long sourceModified = sourceFile == null ? 0 : sourceFile.getLastModified(), - outputModified = outputFile == null ? 0 : outputFile.getLastModified(); - boolean hidden = - outputModified >= sourceModified || hasUpToDateSignature(qualifiedName); - - // TODO remove - // if (!hidden) { - // LOG.warning("Source and class signatures do not match..."); - // LOG.warning("\t" + sourceSignature(qualifiedName)); - // LOG.warning("\t" + classSignature(qualifiedName)); - // } - - if (hidden - && sourceFile != null - && outputFile != null - && !warnedHidden.contains(sourceFile.toUri())) { - LOG.warning("Hiding " + sourceFile.toUri() + " in favor of " + outputFile.toUri()); - - warnedHidden.add(sourceFile.toUri()); - } - - if (!hidden && warnedHidden.contains(sourceFile.toUri())) - warnedHidden.remove(sourceFile.toUri()); - - return hidden; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private boolean hasUpToDateSignature(String qualifiedName) { - return sourceSignature(qualifiedName).equals(classSignature(qualifiedName)); - } - - /** Signatures of all non-private methods and fields in a .java file */ - Optional<PubApi> sourceSignature(String qualifiedName) { - JavacTask task = - javac.getTask( - null, fileManager, __ -> {}, ImmutableList.of(), null, ImmutableList.of()); - TypeElement element = task.getElements().getTypeElement(qualifiedName); - - return signature(element); - } - - /** Signatures of all non-private methods and fields in a .class file */ - Optional<PubApi> classSignature(String qualifiedName) { - JavacTask task = - javac.getTask( - null, - classOnlyFileManager, - __ -> {}, - ImmutableList.of(), - null, - ImmutableList.of()); - TypeElement element = task.getElements().getTypeElement(qualifiedName); - - return signature(element); - } - - private static Optional<PubApi> signature(TypeElement element) { - if (element == null) return Optional.empty(); - else { - PubapiVisitor visit = new PubapiVisitor(); - - visit.scan(element); - - PubApi api = visit.getCollectedPubApi(); - - return Optional.of(api); - } - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/InferConfig.java b/src/main/java/org/javacs/InferConfig.java index 3636ca6..341eb0f 100644 --- a/src/main/java/org/javacs/InferConfig.java +++ b/src/main/java/org/javacs/InferConfig.java @@ -1,15 +1,16 @@ package org.javacs; -import com.google.common.base.Joiner; import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; -import java.nio.file.*; -import java.time.Instant; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -20,129 +21,83 @@ import java.util.stream.Collectors; import java.util.stream.Stream; class InferConfig { + private static final Logger LOG = Logger.getLogger("main"); + /** Root of the workspace that is currently open in VSCode */ private final Path workspaceRoot; - /** User-specified external dependencies, configured with java.externalDependencies */ - private final Collection<Artifact> externalDependencies; - /** User-specified class path, configured with java.classPath */ - private final List<Path> classPath; /** Location of the maven repository, usually ~/.m2 */ private final Path mavenHome; - /** Location of the gradle cache, usually ~/.gradle */ - private final Path gradleHome; - - InferConfig( - Path workspaceRoot, - Collection<Artifact> externalDependencies, - List<Path> classPath, - Path mavenHome, - Path gradleHome) { + + InferConfig(Path workspaceRoot, Path mavenHome) { this.workspaceRoot = workspaceRoot; - this.externalDependencies = externalDependencies; - this.classPath = classPath; this.mavenHome = mavenHome; - this.gradleHome = gradleHome; } - // TODO move to JavaLanguageServer - static Stream<Path> allJavaFiles(Path dir) { - PathMatcher match = FileSystems.getDefault().getPathMatcher("glob:*.java"); - - try { - // TODO instead of looking at EVERY file, once you see a few files with the same source directory, - // ignore all subsequent files in the directory - return Files.walk(dir).filter(java -> match.matches(java.getFileName())); - } catch (IOException e) { - throw new RuntimeException(e); - } + InferConfig(Path workspaceRoot) { + this(workspaceRoot, defaultMavenHome()); } - static Instant buildFilesModified(Path workspaceRoot) { - Instant workspaceModified = fileModified(workspaceRoot.resolve("WORKSPACE")), - pomModified = fileModified(workspaceRoot.resolve("pom.xml")); - - return maxTime(workspaceModified, pomModified); + private static Path defaultMavenHome() { + return Paths.get(System.getProperty("user.home")).resolve(".m2"); } - private static Instant fileModified(Path file) { - if (Files.exists(file)) { - try { - return Files.getLastModifiedTime(file).toInstant(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else return Instant.EPOCH; - } - - private static Instant maxTime(Instant... of) { - Instant result = Instant.EPOCH; - - for (Instant each : of) { - if (each.isAfter(result)) result = each; - } - + Set<Path> classPath() { + var result = new HashSet<Path>(); + result.addAll(buildClassPath()); + result.addAll(workspaceClassPath()); return result; } - /** - * Find .jar files for external dependencies, for examples settings `externalDependencies` in - * local maven / gradle repository. - */ + /** Find .jar files for external dependencies, for examples maven dependencies in ~/.m2 or jars in bazel-genfiles */ Set<Path> buildClassPath() { - // Settings `externalDependencies` - Stream<Path> result = - allExternalDependencies() - .stream() - .flatMap(artifact -> stream(findAnyJar(artifact, false))); - - // Settings `classPath` - Stream<Path> classPathJars = - classPath.stream().flatMap(jar -> stream(findWorkspaceJar(jar))); + var result = new HashSet<Path>(); - result = Stream.concat(result, classPathJars); + // Maven + var as = mvnDependencies(); + if (!as.isEmpty()) { + LOG.info("Looking for artifacts:"); + for (var a : as) { + System.err.println(" " + a); + } + } + for (var a : as) { + var found = findMavenJar(a, false); + if (found.isPresent()) result.add(found.get()); + else LOG.warning(String.format("Couldn't find jar for %s in %s", a, mavenHome)); + } // Bazel if (Files.exists(workspaceRoot.resolve("WORKSPACE"))) { - Path bazelGenFiles = workspaceRoot.resolve("bazel-genfiles"); + var bazelGenFiles = workspaceRoot.resolve("bazel-genfiles"); if (Files.exists(bazelGenFiles) && Files.isSymbolicLink(bazelGenFiles)) { - result = Stream.concat(result, bazelJars(bazelGenFiles)); + LOG.info("Looking for bazel generated files in " + bazelGenFiles); + var jars = bazelJars(bazelGenFiles); + LOG.info(String.format("Found %d generated-files directories", jars.size())); + result.addAll(jars); } } - return result.collect(Collectors.toSet()); - } - - private Optional<Path> findWorkspaceJar(Path jar) { - jar = workspaceRoot.resolve(jar); - - if (Files.exists(jar)) return Optional.of(jar); - else { - LOG.warning("No such file " + jar); - - return Optional.empty(); - } + return result; } /** - * Find directories that contain java .class files in the workspace, for example files generated - * by maven in target/classes + * Find directories that contain java .class files in the workspace, for example files generated by maven in + * target/classes */ Set<Path> workspaceClassPath() { // Bazel if (Files.exists(workspaceRoot.resolve("WORKSPACE"))) { - Path bazelBin = workspaceRoot.resolve("bazel-bin"); + var bazelBin = workspaceRoot.resolve("bazel-bin"); if (Files.exists(bazelBin) && Files.isSymbolicLink(bazelBin)) { - return bazelOutputDirectories(bazelBin).collect(Collectors.toSet()); + return bazelOutputDirectories(bazelBin); } } // Maven try { - return Files.walk(workspaceRoot) - .flatMap(this::outputDirectory) - .collect(Collectors.toSet()); + return Files.walk(workspaceRoot).flatMap(this::outputDirectory).collect(Collectors.toSet()); } catch (IOException e) { throw new RuntimeException(e); } @@ -151,7 +106,7 @@ class InferConfig { /** Recognize build root files like pom.xml and return compiler output directories */ private Stream<Path> outputDirectory(Path file) { if (file.getFileName().toString().equals("pom.xml")) { - Path target = file.resolveSibling("target"); + var target = file.resolveSibling("target"); if (Files.exists(target) && Files.isDirectory(target)) { return Stream.of(target.resolve("classes"), target.resolve("test-classes")); @@ -163,30 +118,56 @@ class InferConfig { return Stream.empty(); } + private void findBazelJavac(File bazelRoot, File workspaceRoot, Set<Path> acc) { + // If _javac directory exists, search it for dirs with names like lib*_classes + var javac = new File(bazelRoot, "_javac"); + if (javac.exists()) { + var match = FileSystems.getDefault().getPathMatcher("glob:**/lib*_classes"); + try { + Files.walk(javac.toPath()).filter(match::matches).filter(Files::isDirectory).forEach(acc::add); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + // Recurse into all directories that mirror the structure of the workspace + if (bazelRoot.isDirectory()) { + var children = bazelRoot.list((__, name) -> new File(workspaceRoot, name).exists()); + for (var child : children) { + var bazelChild = new File(bazelRoot, child); + var workspaceChild = new File(workspaceRoot, child); + findBazelJavac(bazelChild, workspaceChild, acc); + } + } + } + /** * Search bazel-bin for per-module output directories matching the pattern: * * <p>bazel-bin/path/to/module/_javac/rule/lib*_classes */ - private Stream<Path> bazelOutputDirectories(Path bazelBin) { + private Set<Path> bazelOutputDirectories(Path bazelBin) { try { - Path target = Files.readSymbolicLink(bazelBin); - PathMatcher match = - FileSystems.getDefault().getPathMatcher("glob:**/_javac/*/lib*_classes"); + var bazelBinTarget = Files.readSymbolicLink(bazelBin); + LOG.info("Searching for bazel output directories in " + bazelBinTarget); + + var dirs = new HashSet<Path>(); + findBazelJavac(bazelBinTarget.toFile(), workspaceRoot.toFile(), dirs); + LOG.info(String.format("Found %d bazel output directories", dirs.size())); - return Files.walk(target).filter(match::matches).filter(Files::isDirectory); + return dirs; } catch (IOException e) { throw new RuntimeException(e); } } /** Search bazel-genfiles for jars */ - private Stream<Path> bazelJars(Path bazelGenFiles) { + private Set<Path> bazelJars(Path bazelGenFiles) { try { - Path target = Files.readSymbolicLink(bazelGenFiles); + var target = Files.readSymbolicLink(bazelGenFiles); return Files.walk(target) - .filter(file -> file.getFileName().toString().endsWith(".jar")); + .filter(file -> file.getFileName().toString().endsWith(".jar")) + .collect(Collectors.toSet()); } catch (IOException e) { throw new RuntimeException(e); } @@ -194,21 +175,17 @@ class InferConfig { /** Find source .jar files for `externalDependencies` in local maven / gradle repository. */ Set<Path> buildDocPath() { - return allExternalDependencies() - .stream() - .flatMap(artifact -> stream(findAnyJar(artifact, true))) - .collect(Collectors.toSet()); - } - - private Optional<Path> findAnyJar(Artifact artifact, boolean source) { - Optional<Path> maven = findMavenJar(artifact, source); - - if (maven.isPresent()) return maven; - else return findGradleJar(artifact, source); + var result = new HashSet<Path>(); + // Maven + for (var a : mvnDependencies()) { + findMavenJar(a, true).ifPresent(result::add); + } + // TODO Bazel + return result; } - private Optional<Path> findMavenJar(Artifact artifact, boolean source) { - Path jar = + Optional<Path> findMavenJar(Artifact artifact, boolean source) { + var jar = mavenHome .resolve("repository") .resolve(artifact.groupId.replace('.', File.separatorChar)) @@ -220,75 +197,22 @@ class InferConfig { else return Optional.empty(); } - private Optional<Path> findGradleJar(Artifact artifact, boolean source) { - // Search for caches/modules-*/files-*/groupId/artifactId/version/*/artifactId-version[-sources].jar - Path base = gradleHome.resolve("caches"); - String pattern = - "glob:" - + Joiner.on(File.separatorChar) - .join( - base.toString(), - "modules-*", - "files-*", - artifact.groupId, - artifact.artifactId, - artifact.version, - "*", - fileName(artifact, source)); - PathMatcher match = FileSystems.getDefault().getPathMatcher(pattern); - - try { - return Files.walk(base, 7).filter(match::matches).findFirst(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - private String fileName(Artifact artifact, boolean source) { return artifact.artifactId + '-' + artifact.version + (source ? "-sources" : "") + ".jar"; } - private boolean isSourceJar(Path jar) { - return jar.getFileName().toString().endsWith("-sources.jar"); - } - - private int compareMavenVersions(Path leftJar, Path rightJar) { - return compareVersions(mavenVersion(leftJar), mavenVersion(rightJar)); - } - - private String[] mavenVersion(Path jar) { - return jar.getParent().getFileName().toString().split("\\."); - } - - private int compareVersions(String[] left, String[] right) { - int n = Math.min(left.length, right.length); - - for (int i = 0; i < n; i++) { - int each = left[i].compareTo(right[i]); - - if (each != 0) return each; - } - - return 0; - } - - private static <T> Stream<T> stream(Optional<T> option) { - return option.map(Stream::of).orElseGet(Stream::empty); - } - static List<Artifact> dependencyList(Path pomXml) { Objects.requireNonNull(pomXml, "pom.xml path is null"); try { // Tell maven to output deps to a temporary file - Path outputFile = Files.createTempFile("deps", ".txt"); + var outputFile = Files.createTempFile("deps", ".txt"); - String cmd = + var cmd = String.format( - "%s dependency:list -DincludeScope=test -DoutputFile=%s", - getMvnCommand(), outputFile); - File workingDirectory = pomXml.toAbsolutePath().getParent().toFile(); - int result = Runtime.getRuntime().exec(cmd, null, workingDirectory).waitFor(); + "%s dependency:list -DincludeScope=test -DoutputFile=%s", getMvnCommand(), outputFile); + var workingDirectory = pomXml.toAbsolutePath().getParent().toFile(); + var result = Runtime.getRuntime().exec(cmd, null, workingDirectory).waitFor(); if (result != 0) throw new RuntimeException("`" + cmd + "` returned " + result); @@ -299,9 +223,9 @@ class InferConfig { } private static List<Artifact> readDependencyList(Path outputFile) { - Pattern artifact = Pattern.compile(".*:.*:.*:.*:.*"); + var artifact = Pattern.compile(".*:.*:.*:.*:.*"); - try (InputStream in = Files.newInputStream(outputFile)) { + try (var in = Files.newInputStream(outputFile)) { return new BufferedReader(new InputStreamReader(in)) .lines() .map(String::trim) @@ -313,14 +237,9 @@ class InferConfig { } } - /** - * Get external dependencies from this.externalDependencies if available, or try to infer them. - */ - private Collection<Artifact> allExternalDependencies() { - if (!externalDependencies.isEmpty()) return externalDependencies; - - // If user does not specify java.externalDependencies, look for pom.xml - Path pomXml = workspaceRoot.resolve("pom.xml"); + /** Get external dependencies from this.externalDependencies if available, or try to infer them. */ + private Collection<Artifact> mvnDependencies() { + var pomXml = workspaceRoot.resolve("pom.xml"); if (Files.exists(pomXml)) return dependencyList(pomXml); @@ -328,7 +247,7 @@ class InferConfig { } static String getMvnCommand() { - String mvnCommand = "mvn"; + var mvnCommand = "mvn"; if (File.separatorChar == '\\') { mvnCommand = findExecutableOnPath("mvn.cmd"); if (mvnCommand == null) { @@ -339,14 +258,12 @@ class InferConfig { } private static String findExecutableOnPath(String name) { - for (String dirname : System.getenv("PATH").split(File.pathSeparator)) { - File file = new File(dirname, name); + for (var dirname : System.getenv("PATH").split(File.pathSeparator)) { + var file = new File(dirname, name); if (file.isFile() && file.canExecute()) { return file.getAbsolutePath(); } } return null; } - - private static final Logger LOG = Logger.getLogger("main"); } diff --git a/src/main/java/org/javacs/InferSourcePath.java b/src/main/java/org/javacs/InferSourcePath.java new file mode 100644 index 0000000..f261d12 --- /dev/null +++ b/src/main/java/org/javacs/InferSourcePath.java @@ -0,0 +1,77 @@ +package org.javacs; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.logging.Logger; +import java.util.stream.Stream; + +class InferSourcePath { + + static Stream<Path> allJavaFiles(Path dir) { + var match = FileSystems.getDefault().getPathMatcher("glob:*.java"); + + try { + return Files.walk(dir).filter(java -> match.matches(java.getFileName())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static Set<Path> sourcePath(Path workspaceRoot) { + LOG.info("Searching for source roots in " + workspaceRoot); + + class SourcePaths implements Consumer<Path> { + int certaintyThreshold = 10; + Map<Path, Integer> sourceRoots = new HashMap<>(); + + boolean alreadyKnown(Path java) { + for (var root : sourceRoots.keySet()) { + if (java.startsWith(root) && sourceRoots.get(root) > certaintyThreshold) return true; + } + return false; + } + + Optional<Path> infer(Path java) { + var packageName = Objects.toString(Parser.parse(java).getPackageName(), ""); + var packagePath = packageName.replace('.', File.separatorChar); + var dir = java.getParent(); + if (!dir.endsWith(packagePath)) { + LOG.warning("Java source file " + java + " is not in " + packagePath); + return Optional.empty(); + } else { + var up = Paths.get(packagePath).getNameCount(); + var truncate = dir; + for (int i = 0; i < up; i++) truncate = truncate.getParent(); + return Optional.of(truncate); + } + } + + @Override + public void accept(Path java) { + if (!alreadyKnown(java)) { + infer(java) + .ifPresent( + root -> { + var count = sourceRoots.getOrDefault(root, 0); + sourceRoots.put(root, count + 1); + }); + } + } + } + var checker = new SourcePaths(); + allJavaFiles(workspaceRoot).forEach(checker); + return checker.sourceRoots.keySet(); + } + + 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 new file mode 100644 index 0000000..cebf878 --- /dev/null +++ b/src/main/java/org/javacs/JavaCompilerService.java @@ -0,0 +1,1202 @@ +package org.javacs; + +import com.google.common.collect.Sets; +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.tree.*; +import com.sun.source.util.JavacTask; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import com.sun.source.util.TreeScanner; +import com.sun.source.util.Trees; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; + +// TODO eliminate uses of URI in favor of Path +public class JavaCompilerService { + private static final Logger LOG = Logger.getLogger("main"); + // Not modifiable! If you want to edit these, you need to create a new instance + private final Set<Path> sourcePath, classPath, docPath; + private final JavaCompiler compiler = ServiceLoader.load(JavaCompiler.class).iterator().next(); + private final Docs docs; + private final ClassSource jdkClasses = Classes.jdkTopLevelClasses(), classPathClasses; + // Diagnostics from the last compilation task + private final List<Diagnostic<? extends JavaFileObject>> diags = new ArrayList<>(); + // Use the same file manager for multiple tasks, so we don't repeatedly re-compile the same files + private final StandardJavaFileManager fileManager = + compiler.getStandardFileManager(diags::add, null, Charset.defaultCharset()); + // Cache a single compiled file + // Since the user can only edit one file at a time, this should be sufficient + private Cache cache; + + public JavaCompilerService(Set<Path> sourcePath, Set<Path> classPath, Set<Path> docPath) { + var klass = compiler.getClass(); + var path = klass.getName().replace('.', '/'); + var location = klass.getResource(String.format("/%s.class", path)); + + System.err.println("Source path:"); + for (var p : sourcePath) { + System.err.println(" " + p); + } + System.err.println("Class path:"); + for (var p : classPath) { + System.err.println(" " + p); + } + System.err.println("Doc path:"); + for (var p : docPath) { + System.err.println(" " + p); + } + // sourcePath and classPath can't actually be modified, because JavaCompiler remembers them from task to task + this.sourcePath = Collections.unmodifiableSet(sourcePath); + this.classPath = Collections.unmodifiableSet(classPath); + this.docPath = Collections.unmodifiableSet(docPath); + this.docs = new Docs(Sets.union(sourcePath, docPath)); + this.classPathClasses = Classes.classPathTopLevelClasses(classPath); + } + + 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 : jdkClasses.classes()) checkClassName.accept(name); + for (var name : classPathClasses.classes()) checkClassName.accept(name); + return result; + } + + /** Combine source path or class path entries using the system separator, for example ':' in unix */ + private static String joinPath(Collection<Path> classOrSourcePath) { + return classOrSourcePath.stream().map(p -> p.toString()).collect(Collectors.joining(File.pathSeparator)); + } + + private static List<String> options(Set<Path> sourcePath, Set<Path> classPath) { + return Arrays.asList( + "-classpath", + joinPath(classPath), + "-sourcepath", + joinPath(sourcePath), + // "-verbose", + "-proc:none", + "-g", + // You would think we could do -Xlint:all, + // but some lints trigger fatal errors in the presence of parse errors + "-Xlint:cast", + "-Xlint:deprecation", + "-Xlint:empty", + "-Xlint:fallthrough", + "-Xlint:finally", + "-Xlint:path", + "-Xlint:unchecked", + "-Xlint:varargs", + "-Xlint:static"); + } + + /** Create a task that compiles a single file */ + private JavacTask singleFileTask(URI file, String contents) { + diags.clear(); + return (JavacTask) + compiler.getTask( + null, + fileManager, + diags::add, + options(sourcePath, classPath), + Collections.emptyList(), + Collections.singletonList(new StringFileObject(contents, file))); + } + + private JavacTask batchTask(Collection<Path> paths) { + diags.clear(); + var files = paths.stream().map(Path::toFile).collect(Collectors.toList()); + return (JavacTask) + compiler.getTask( + null, + fileManager, + diags::add, + options(sourcePath, classPath), + Collections.emptyList(), + fileManager.getJavaFileObjectsFromFiles(files)); + } + + List<Diagnostic<? extends JavaFileObject>> lint(Collection<Path> files) { + var task = batchTask(files); + try { + task.parse(); + task.analyze(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return Collections.unmodifiableList(new ArrayList<>(diags)); + } + + /** Stores the compiled version of a single file */ + class Cache { + final String contents; + final URI file; + final CompilationUnitTree root; + final JavacTask task; + final int line, character; + + Cache(URI file, String contents, int line, int character) { + // If `line` is -1, recompile the entire file + if (line == -1) { + this.contents = contents; + } + // Otherwise, focus on the block surrounding line:character, + // erasing all other block bodies and everything after the cursor in its own block + else { + var p = new Pruner(file, contents); + p.prune(line, character); + this.contents = p.contents(); + } + this.file = file; + this.task = singleFileTask(file, this.contents); + try { + this.root = task.parse().iterator().next(); + // The results of task.analyze() are unreliable when errors are present + // You can get at `Element` values using `Trees` + task.analyze(); + } catch (IOException e) { + throw new RuntimeException(e); + } + this.line = line; + this.character = character; + } + } + + /** Recompile if the active file has been edited, or if the active file has changed */ + private void recompile(URI file, String contents, int line, int character) { + if (cache == null + || !cache.file.equals(file) + || !cache.contents.equals(contents) + || cache.line != line + || cache.character != character) { + cache = new Cache(file, contents, line, character); + } + } + + /** Find the smallest tree that includes the cursor */ + private TreePath path(URI file, int line, int character) { + var trees = Trees.instance(cache.task); + var pos = trees.getSourcePositions(); + var cursor = cache.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(cache.root, tree), end = pos.getEndPosition(cache.root, tree); + // If element has no position, give up + if (start == -1 || end == -1) return false; + // 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; + } + + TreePath find(Tree root) { + scan(root, null); + if (found == null) { + var message = String.format("No TreePath to %s %d:%d", file, line, character); + throw new RuntimeException(message); + } + return found; + } + } + return new FindSmallest().find(cache.root); + } + + /** Find all identifiers in scope at line:character */ + public List<Element> scopeMembers(URI file, String contents, int line, int character) { + recompile(file, contents, line, character); + + var trees = Trees.instance(cache.task); + var types = cache.task.getTypes(); + var path = path(file, line, character); + 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(VariableElement ve) { + var name = ve.getSimpleName().toString(); + return name.equals("this") || name.equals("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; + + // 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) { + for (var e : s.getLocalElements()) { + if (e instanceof TypeElement) { + var te = (TypeElement) e; + if (trees.isAccessible(start, te)) result.add(te); + } else if (e instanceof VariableElement) { + var ve = (VariableElement) e; + if (isThisOrSuper(ve)) { + unwrapThisSuper(ve); + if (!isStatic(s)) result.add(ve); + } else { + result.add(ve); + } + } else { + result.add(e); + } + } + } + + // Walk each enclosing scope, placing its members into `results` + List<Element> walkScopes() { + for (var s = start; s != null; s = s.getEnclosingScope()) { + walkLocals(s); + } + + return result; + } + } + return new Walk().walkScopes(); + } + + private List<TypeMirror> supersWithSelf(TypeMirror t) { + var elements = cache.task.getElements(); + var types = cache.task.getTypes(); + var result = new ArrayList<TypeMirror>(); + result.add(t); + // Add members of superclasses and interfaces + result.addAll(types.directSupertypes(t)); + // Object type is not included by default + // We need to add it to get members like .equals(other) and .hashCode() + // TODO this may add things twice for interfaces with no super-interfaces + result.add(elements.getTypeElement("java.lang.Object").asType()); + return result; + } + + 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 members of expression ending at line:character */ + public List<Completion> members(URI file, String contents, int line, int character, boolean isReference) { + recompile(file, contents, line, character); + + var trees = Trees.instance(cache.task); + var types = cache.task.getTypes(); + var path = path(file, line, character); + var scope = trees.getScope(path); + var element = trees.getElement(path); + + if (element instanceof PackageElement) { + var result = new ArrayList<Completion>(); + var p = (PackageElement) element; + + // 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; + + // 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; + + // 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 (hasMembers(type)) { + var result = new ArrayList<Completion>(); + var ts = supersWithSelf(type); + var alreadyAdded = new HashSet<String>(); + for (var t : ts) { + var e = types.asElement(t); + 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 (t instanceof DeclaredType) { + if (trees.isAccessible(scope, member, (DeclaredType) t)) { + result.add(Completion.ofElement(member)); + } + } + // Otherwise, accessibility rules are very complicated + // Give up and just declare that everything is accessible + else result.add(Completion.ofElement(member)); + // Remember the signature of the added method, so we don't re-add it later + alreadyAdded.add(member.toString()); + } + } + return result; + } else { + LOG.warning("Don't know how to complete members of type " + type); + return Collections.emptyList(); + } + } + } + + private static String[] TOP_LEVEL_KEYWORDS = { + "package", + "import", + "public", + "private", + "protected", + "abstract", + "class", + "interface", + }; + + private static String[] CLASS_BODY_KEYWORDS = { + "public", + "private", + "protected", + "static", + "final", + "native", + "synchronized", + "abstract", + "default", + "class", + "interface", + }; + + 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", + }; + + /** + * Complete members or identifiers at the cursor. Delegates to `members` or `scopeMembers`, depending on whether the + * expression before the cursor looks like `foo.bar` or `foo` + */ + public CompletionResult completions(URI file, String contents, int line, int character, int limitHint) { + LOG.info(String.format("Completing at %s[%d,%d]...", file.getPath(), line, character)); + // TODO why not just recompile? It's going to get triggered shortly anyway + var task = singleFileTask(file, contents); + CompilationUnitTree parse; + try { + parse = task.parse().iterator().next(); + } catch (IOException e) { + throw new RuntimeException(e); + } + var pos = Trees.instance(task).getSourcePositions(); + var lines = parse.getLineMap(); + var cursor = lines.getPosition(line, character); + + class Find extends TreeScanner<Void, Void> { + List<Completion> result = null; + boolean isIncomplete = false; + int insideClass = 0, insideMethod = 0; + + boolean containsCursor(Tree node) { + return pos.getStartPosition(parse, node) <= cursor && cursor <= pos.getEndPosition(parse, node); + } + + @Override + public Void visitClass​(ClassTree node, Void nothing) { + insideClass++; + super.visitClass(node, null); + insideClass--; + return null; + } + + @Override + public Void visitMethod​(MethodTree node, Void nothing) { + insideMethod++; + super.visitMethod(node, null); + insideMethod--; + return null; + } + + @Override + public Void visitMemberSelect(MemberSelectTree node, Void nothing) { + super.visitMemberSelect(node, nothing); + + if (containsCursor(node) && !containsCursor(node.getExpression()) && result == null) { + LOG.info("...completing members of " + node.getExpression()); + long offset = pos.getEndPosition(parse, node.getExpression()), + line = lines.getLineNumber(offset), + column = lines.getColumnNumber(offset); + result = members(file, contents, (int) line, (int) column, false); + } + return null; + } + + @Override + public Void visitMemberReference(MemberReferenceTree node, Void nothing) { + super.visitMemberReference(node, nothing); + + if (containsCursor(node) && !containsCursor(node.getQualifierExpression()) && result == null) { + LOG.info("...completing members of " + node.getQualifierExpression()); + long offset = pos.getEndPosition(parse, node.getQualifierExpression()), + line = lines.getLineNumber(offset), + column = lines.getColumnNumber(offset); + result = members(file, contents, (int) line, (int) column, true); + } + return null; + } + + @Override + public Void visitIdentifier(IdentifierTree node, Void nothing) { + super.visitIdentifier(node, nothing); + + if (containsCursor(node) && result == null) { + LOG.info("...completing identifiers"); + result = new ArrayList<>(); + // Does a candidate completion match the name in `node`? + var partialName = Objects.toString(node.getName(), ""); + // Add keywords + if (insideClass == 0) { + for (var k : TOP_LEVEL_KEYWORDS) { + if (k.startsWith(partialName)) { + result.add(Completion.ofKeyword(k)); + } + } + } + else if (insideMethod == 0) { + for (var k : CLASS_BODY_KEYWORDS) { + if (k.startsWith(partialName)) { + result.add(Completion.ofKeyword(k)); + } + } + } + else { + for (var k : METHOD_BODY_KEYWORDS) { + if (k.startsWith(partialName)) { + result.add(Completion.ofKeyword(k)); + } + } + } + var startsWithUpperCase = partialName.length() > 0 && Character.isUpperCase(partialName.charAt(0)); + var alreadyImported = new HashSet<String>(); + // Add names that have already been imported + for (var m : scopeMembers(file, contents, line, character)) { + if (m.getSimpleName().toString().startsWith(partialName)) { + result.add(Completion.ofElement(m)); + + if (m instanceof TypeElement && startsWithUpperCase) { + var t = (TypeElement) m; + alreadyImported.add(t.getQualifiedName().toString()); + } + } + } + // Add names of classes that haven't been imported + if (startsWithUpperCase) { + var packageName = Objects.toString(parse.getPackageName(), ""); + Predicate<String> matchesPartialName = + qualifiedName -> { + var className = Parser.lastName(qualifiedName); + return className.startsWith(partialName); + }; + Predicate<String> notAlreadyImported = className -> !alreadyImported.contains(className); + var fromJdk = + jdkClasses + .classes() + .stream() + .filter(matchesPartialName) + .filter(notAlreadyImported) + .filter(c -> jdkClasses.isAccessibleFromPackage(c, packageName)); + var fromClasspath = + classPathClasses + .classes() + .stream() + .filter(matchesPartialName) + .filter(notAlreadyImported) + .filter(c -> classPathClasses.isAccessibleFromPackage(c, packageName)); + Function<Path, Stream<String>> classesInDir = dir -> { + Predicate<Path> matchesFileName = + file -> file.getFileName().toString().startsWith(partialName); + Predicate<Path> isPublic = + file -> { + var fileName = file.getFileName().toString(); + if (!fileName.endsWith(".java")) return false; + var simpleName = fileName.substring(0, fileName.length() - ".java".length()); + Stream<String> lines; + try { + lines = Files.lines(file); + } catch (IOException e) { + throw new RuntimeException(e); + } + return lines.anyMatch(line -> line.matches(".*public\\s+class\\s+" + simpleName + ".*")); + }; + Function<Path, String> qualifiedName = + file -> { + var relative = dir.relativize(file).toString().replace('/', '.'); + if (!relative.endsWith(".java")) return "??? " + relative + " does not end in .java"; + return relative.substring(0, relative.length() - ".java".length()); + }; + return javaSourcesInDir(dir).filter(matchesFileName).filter(isPublic).map(qualifiedName); + }; + var fromSourcePath = + sourcePath.stream() + .flatMap(classesInDir) + .filter(notAlreadyImported); + Consumer<Stream<String>> addCompletions = qualifiedNames -> { + Iterable<String> it = qualifiedNames::iterator; + for (var name : it) { + if (result.size() >= limitHint) { + isIncomplete = true; + return; + } else { + var completion = Completion.ofNotImportedClass(name); + result.add(completion); + } + } + }; + addCompletions.accept(fromJdk); + addCompletions.accept(fromClasspath); + addCompletions.accept(fromSourcePath); + } + } + return null; + } + + @Override + public Void visitErroneous(ErroneousTree node, Void nothing) { + for (var t : node.getErrorTrees()) { + t.accept(this, null); + } + return null; + } + + CompletionResult run() { + scan(parse, null); + if (result == null) result = Collections.emptyList(); + if (isIncomplete) LOG.info(String.format("Found %d items (incomplete)", result.size())); + return new CompletionResult(result, isIncomplete); + } + } + return new Find().run(); + } + + /** Find all overloads for the smallest method call that includes the cursor */ + public Optional<MethodInvocation> methodInvocation(URI file, String contents, int line, int character) { + recompile(file, contents, line, character); + + var trees = Trees.instance(cache.task); + var start = path(file, line, character); + + for (var path = start; path != null; path = path.getParentPath()) { + if (path.getLeaf() instanceof MethodInvocationTree) { + 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); + } + } + var activeParameter = invoke.getArguments().indexOf(start.getLeaf()); + 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) { + 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); + } + } + var activeParameter = invoke.getArguments().indexOf(start.getLeaf()); + Optional<ExecutableElement> activeMethod = + method instanceof ExecutableElement + ? Optional.of((ExecutableElement) method) + : Optional.empty(); + return Optional.of(new MethodInvocation(invoke, activeMethod, activeParameter, results)); + } + } + return Optional.empty(); + } + + /** Find the smallest element that includes the cursor */ + public Element element(URI file, String contents, int line, int character) { + recompile(file, contents, line, character); + + var trees = Trees.instance(cache.task); + var path = path(file, line, character); + return trees.getElement(path); + } + + private Optional<TypeElement> topLevelDeclaration(Element e) { + var parent = e; + TypeElement result = null; + while (parent.getEnclosingElement() != null) { + if (parent instanceof TypeElement) result = (TypeElement) parent; + parent = parent.getEnclosingElement(); + } + return Optional.ofNullable(result); + } + + /** */ + private boolean containsTopLevelDeclaration(Path file, String simpleClassName) { + var find = Pattern.compile("\\b(class|interface|enum) +" + simpleClassName + "\\b"); + try (var lines = Files.newBufferedReader(file)) { + var line = lines.readLine(); + while (line != null) { + if (find.matcher(line).find()) return true; + line = lines.readLine(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return false; + } + + /** Find the file `e` was declared in */ + private Optional<Path> findDeclaringFile(TypeElement e) { + var name = e.getQualifiedName().toString(); + var lastDot = name.lastIndexOf('.'); + var packageName = lastDot == -1 ? "" : name.substring(0, lastDot); + var className = name.substring(lastDot + 1); + // First, look for a file named [ClassName].java + var packagePath = Paths.get(packageName.replace('.', File.separatorChar)); + var publicClassPath = packagePath.resolve(className + ".java"); + for (var root : sourcePath) { + var absPath = root.resolve(publicClassPath); + if (Files.exists(absPath) && containsTopLevelDeclaration(absPath, className)) { + return Optional.of(absPath); + } + } + // Then, look for a secondary declaration in all java files in the package + var isPublic = e.getModifiers().contains(Modifier.PUBLIC); + if (!isPublic) { + for (var root : sourcePath) { + var absDir = root.resolve(packagePath); + try { + var foundFile = + Files.list(absDir).filter(f -> containsTopLevelDeclaration(f, className)).findFirst(); + if (foundFile.isPresent()) return foundFile; + } catch (IOException err) { + throw new RuntimeException(err); + } + } + } + return Optional.empty(); + } + + /** Compile `file` and locate `e` in it */ + private Optional<TreePath> findIn(Element e, Path file, String contents) { + var task = singleFileTask(file.toUri(), contents); + CompilationUnitTree tree; + try { + tree = task.parse().iterator().next(); + task.analyze(); + } catch (IOException err) { + throw new RuntimeException(err); + } + var trees = Trees.instance(task); + class Find extends TreePathScanner<Void, Void> { + Optional<TreePath> found = Optional.empty(); + + boolean toStringEquals(Object left, Object right) { + return Objects.equals(Objects.toString(left, ""), Objects.toString(right, "")); + } + + /** Check if the declaration at the current path is the same symbol as `e` */ + boolean sameSymbol() { + var candidate = trees.getElement(getCurrentPath()); + // `e` is from a different compilation, so we have to compare qualified names + return toStringEquals(candidate.getEnclosingElement(), e.getEnclosingElement()) + && toStringEquals(candidate, e); + } + + void check() { + if (sameSymbol()) { + found = Optional.of(getCurrentPath()); + } + } + + @Override + public Void visitClass(ClassTree node, Void aVoid) { + check(); + return super.visitClass(node, aVoid); + } + + @Override + public Void visitMethod(MethodTree node, Void aVoid) { + check(); + return super.visitMethod(node, aVoid); + } + + @Override + public Void visitVariable(VariableTree node, Void aVoid) { + check(); + return super.visitVariable(node, aVoid); + } + + Optional<TreePath> run() { + scan(tree, null); + return found; + } + } + return new Find().run(); + } + + public Optional<TreePath> definition(URI file, int line, int character, Function<URI, String> contents) { + recompile(file, contents.apply(file), line, character); + + var trees = Trees.instance(cache.task); + var path = path(file, line, character); + LOG.info("Looking for definition for " + path.getLeaf() + "..."); + var e = trees.getElement(path); + var declaration = topLevelDeclaration(e); + LOG.info("...looking for top-level declaration " + declaration); + var declaringFile = declaration.flatMap(this::findDeclaringFile); + LOG.info("...declaration is in " + declaringFile); + return declaringFile.flatMap(f -> findIn(e, f, contents.apply(f.toUri()))); + } + + /** Look up the javadoc associated with `method` */ + public Optional<DocCommentTree> methodDoc(ExecutableElement method) { + var classElement = (TypeElement) method.getEnclosingElement(); + var className = classElement.getQualifiedName().toString(); + var methodName = method.getSimpleName().toString(); + return docs.memberDoc(className, methodName); + } + + /** Find and parse the source code associated with `method` */ + public Optional<MethodTree> methodTree(ExecutableElement method) { + var classElement = (TypeElement) method.getEnclosingElement(); + var className = classElement.getQualifiedName().toString(); + var methodName = method.getSimpleName().toString(); + var parameterTypes = method.getParameters().stream().map(p -> p.asType().toString()).collect(Collectors.toList()); + return docs.findMethod(className, methodName, parameterTypes); + } + + /** Look up the javadoc associated with `type` */ + public Optional<DocCommentTree> classDoc(TypeElement type) { + return docs.classDoc(type.getQualifiedName().toString()); + } + + public Optional<DocCommentTree> classDoc(String qualifiedName) { + return docs.classDoc(qualifiedName); + } + + private Stream<Path> javaSourcesInDir(Path dir) { + var match = FileSystems.getDefault().getPathMatcher("glob:*.java"); + + try { + // TODO instead of looking at EVERY file, once you see a few files with the same source directory, + // ignore all subsequent files in the directory + return Files.walk(dir).filter(java -> match.matches(java.getFileName())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private Stream<Path> javaSources() { + return sourcePath.stream().flatMap(dir -> javaSourcesInDir(dir)); + } + + private List<Path> potentialReferences(Element to) { + var name = to.getSimpleName().toString(); + var word = Pattern.compile("\\b\\w+\\b"); + Predicate<String> containsWord = + line -> { + Matcher m = word.matcher(line); + while (m.find()) { + if (m.group().equals(name)) return true; + } + return false; + }; + Predicate<Path> test = + file -> { + try { + return Files.readAllLines(file).stream().anyMatch(containsWord); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + return javaSources().filter(test).collect(Collectors.toList()); + } + + /** + * Represents a batch compilation of many files. The batch context is different that the incremental context, so + * methods in this class should not access `cache`. + */ + static class Batch { + final JavacTask task; + final List<CompilationUnitTree> roots; + + Batch(JavacTask task, List<CompilationUnitTree> roots) { + this.task = task; + this.roots = roots; + } + + private boolean toStringEquals(Object left, Object right) { + return Objects.equals(Objects.toString(left, ""), Objects.toString(right, "")); + } + + private boolean sameSymbol(Element target, Element symbol) { + return symbol != null + && target != null + && toStringEquals(symbol.getEnclosingElement(), target.getEnclosingElement()) + && toStringEquals(symbol, target); + } + + private List<TreePath> actualReferences(CompilationUnitTree from, Element to) { + var trees = Trees.instance(task); + + class Finder extends TreeScanner<Void, Void> { + List<TreePath> results = new ArrayList<>(); + + @Override + public Void scan(Tree leaf, Void nothing) { + if (leaf != null) { + var path = trees.getPath(from, leaf); + var found = trees.getElement(path); + + if (sameSymbol(found, to)) results.add(path); + else super.scan(leaf, nothing); + } + return null; + } + + List<TreePath> run() { + scan(from, null); + return results; + } + } + return new Finder().run(); + } + } + + private Batch compileBatch(List<Path> files) { + var task = batchTask(files); + var result = new ArrayList<CompilationUnitTree>(); + try { + for (var t : task.parse()) result.add(t); + task.analyze(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return new Batch(task, result); + } + + public List<TreePath> references(URI file, String contents, int line, int character) { + recompile(file, contents, -1, -1); + + var trees = Trees.instance(cache.task); + var path = path(file, line, character); + // It's sort of odd that this works + // `to` is part of a different batch than `batch = compileBatch(possible)`, + // so `to.equals(...thing from batch...)` shouldn't work + var to = trees.getElement(path); + var possible = potentialReferences(to); + var batch = compileBatch(possible); + var result = new ArrayList<TreePath>(); + for (var f : batch.roots) { + result.addAll(batch.actualReferences(f, to)); + } + return result; + } + + public Stream<TreePath> findSymbols(String query) { + return sourcePath.stream().flatMap(dir -> Parser.findSymbols(dir, query)); + } + + // TODO this is ugly, suggests something needs to be moved into JavaCompilerService + public Trees trees() { + return Trees.instance(cache.task); + } + + /** + * Figure out what imports this file should have. Star-imports like `import java.util.*` are converted to individual + * class imports. Missing imports are inferred by looking at imports in other source files. + */ + public FixImports fixImports(URI file, String contents) { + LOG.info("Fix imports in " + file); + // Compile a single file + var task = singleFileTask(file, contents); + CompilationUnitTree tree; + try { + tree = task.parse().iterator().next(); + task.analyze(); + } catch (IOException e) { + throw new RuntimeException(e); + } + // Check diagnostics for missing imports + var unresolved = new HashSet<String>(); + for (var d : diags) { + if (d.getCode().equals("compiler.err.cant.resolve.location") && d.getSource().toUri().equals(file)) { + long start = d.getStartPosition(), end = d.getEndPosition(); + var id = contents.substring((int) start, (int) end); + if (id.matches("[A-Z]\\w+")) { + unresolved.add(id); + } else LOG.warning(id + " doesn't look like a class"); + } else if (d.getMessage(null).contains("cannot find symbol")) { + var lines = d.getMessage(null).split("\n"); + var firstLine = lines.length > 0 ? lines[0] : ""; + LOG.warning(String.format("%s %s doesn't look like symbol-not-found", d.getCode(), firstLine)); + } + } + // Look at imports in other classes to help us guess how to fix imports + var sourcePathImports = Parser.existingImports(sourcePath); + var classes = new HashSet<String>(); + classes.addAll(jdkClasses.classes()); + classes.addAll(classPathClasses.classes()); + var fixes = Parser.resolveSymbols(unresolved, sourcePathImports, classes); + // Figure out which existing imports are actually used + var trees = Trees.instance(task); + var references = new HashSet<String>(); + class FindUsedImports extends TreePathScanner<Void, Void> { + @Override + public Void visitIdentifier(IdentifierTree node, Void nothing) { + var e = trees.getElement(getCurrentPath()); + if (e instanceof TypeElement) { + var t = (TypeElement) e; + var qualifiedName = t.getQualifiedName().toString(); + var lastDot = qualifiedName.lastIndexOf('.'); + var packageName = lastDot == -1 ? "" : qualifiedName.substring(0, lastDot); + var thisPackage = Objects.toString(tree.getPackageName(), ""); + // java.lang.* and current package are imported by default + if (!packageName.equals("java.lang") + && !packageName.equals(thisPackage) + && !packageName.equals("")) { + references.add(qualifiedName); + } + } + return null; + } + } + new FindUsedImports().scan(tree, null); + // Take the intersection of existing imports ^ existing identifiers + var qualifiedNames = new HashSet<String>(); + for (var i : tree.getImports()) { + var imported = i.getQualifiedIdentifier().toString(); + if (imported.endsWith(".*")) { + var packageName = Parser.mostName(imported); + var isUsed = references.stream().anyMatch(r -> r.startsWith(packageName)); + if (isUsed) qualifiedNames.add(imported); + else LOG.warning("There are no references to package " + imported); + } + else { + if (references.contains(imported)) qualifiedNames.add(imported); + else LOG.warning("There are no references to class " + imported); + } + } + // Add qualified names from fixes + qualifiedNames.addAll(fixes.values()); + return new FixImports(tree, trees.getSourcePositions(), qualifiedNames); + } + + public List<TestMethod> testMethods(URI file, String contents) { + LOG.info(String.format("Finding test methods in %s", file.getPath())); + + var task = singleFileTask(file, contents); + CompilationUnitTree parse; + try { + parse = task.parse().iterator().next(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + class Find extends TreeScanner<Void, Void> { + ClassTree enclosingClass = null; + List<TestMethod> found = new ArrayList<>(); + + @Override + public Void visitClass​(ClassTree node, Void nothing) { + var shadowed = enclosingClass; + enclosingClass = node; + super.visitClass(node, null); + enclosingClass = shadowed; + return null; + } + + @Override + public Void visitMethod(MethodTree node, Void aVoid) { + for (var ann : node.getModifiers().getAnnotations()) { + var type = ann.getAnnotationType(); + if (type instanceof IdentifierTree) { + var id = (IdentifierTree) type; + var name = id.getName(); + if (name.contentEquals("Test") || name.contentEquals("org.junit.Test")) { + found.add(new TestMethod(task, parse, enclosingClass, node)); + } + } + } + return super.visitMethod(node, aVoid); + } + + List<TestMethod> run() { + scan(parse, null); + return found; + } + } + + return new Find().run(); + } +} diff --git a/src/main/java/org/javacs/JavaConfigJson.java b/src/main/java/org/javacs/JavaConfigJson.java deleted file mode 100644 index a306453..0000000 --- a/src/main/java/org/javacs/JavaConfigJson.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.javacs; - -import java.nio.file.Path; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; - -public class JavaConfigJson { - public Set<Path> sourcePath, - classPath = Collections.emptySet(), - docPath = Collections.emptySet(); - public Optional<Path> classPathFile = Optional.empty(), docPathFile = Optional.empty(); - public Path outputDirectory; -} diff --git a/src/main/java/org/javacs/JavaLanguageServer.java b/src/main/java/org/javacs/JavaLanguageServer.java index ee04ba9..01e301d 100644 --- a/src/main/java/org/javacs/JavaLanguageServer.java +++ b/src/main/java/org/javacs/JavaLanguageServer.java @@ -1,141 +1,101 @@ package org.javacs; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import com.sun.source.util.TreePath; -import com.sun.source.util.Trees; -import java.io.PrintWriter; -import java.io.StringWriter; import java.net.URI; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.function.Function; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogRecord; import java.util.logging.Logger; -import javax.lang.model.element.Element; +import java.util.stream.Collectors; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; import org.eclipse.lsp4j.*; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.LanguageServer; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; +import org.eclipse.lsp4j.services.*; class JavaLanguageServer implements LanguageServer { private static final Logger LOG = Logger.getLogger("main"); - int maxItems = 50; - private final CompletableFuture<LanguageClient> client = new CompletableFuture<>(); - private final JavaTextDocumentService textDocuments = new JavaTextDocumentService(client, this); - private final JavaWorkspaceService workspace = - new JavaWorkspaceService(client, this, textDocuments); - private Path workspaceRoot = Paths.get("."); - private Configured cacheConfigured; - private JavaSettings cacheSettings; - private Path cacheWorkspaceRoot; - private Instant cacheInferConfig = Instant.EPOCH; - private Set<Path> cacheSourcePath = Collections.emptySet(); + Path workspaceRoot; + LanguageClient client; + JavaCompilerService compiler; + final JavaTextDocumentService textDocuments = new JavaTextDocumentService(this); + final JavaWorkspaceService workspace = new JavaWorkspaceService(this); - /** - * Configured java compiler + indices based on workspace settings and inferred source / class - * paths - */ - Configured configured() { - Instant inferConfig = InferConfig.buildFilesModified(workspaceRoot); - - if (cacheConfigured == null - || !Objects.equals(workspace.settings(), cacheSettings) - || !Objects.equals(workspaceRoot, cacheWorkspaceRoot) - || cacheInferConfig.isBefore(inferConfig) - || !cacheConfigured.index.sourcePath().equals(cacheSourcePath)) { - cacheConfigured = createCompiler(workspace.settings(), workspaceRoot); - cacheSettings = workspace.settings(); - cacheWorkspaceRoot = workspaceRoot; - cacheInferConfig = inferConfig; - cacheSourcePath = cacheConfigured.index.sourcePath(); - - clearDiagnostics(); + private static DiagnosticSeverity severity(Diagnostic.Kind kind) { + switch (kind) { + case ERROR: + return DiagnosticSeverity.Error; + case WARNING: + case MANDATORY_WARNING: + return DiagnosticSeverity.Warning; + case NOTE: + case OTHER: + default: + return DiagnosticSeverity.Information; } - - return cacheConfigured; } - private Configured createCompiler(JavaSettings settings, Path workspaceRoot) { - SymbolIndex index = - new SymbolIndex( - workspaceRoot, textDocuments::openFiles, textDocuments::activeContent); - Set<Path> sourcePath = index.sourcePath(); - Path userHome = Paths.get(System.getProperty("user.home")), - mavenHome = userHome.resolve(".m2"), - gradleHome = userHome.resolve(".gradle"); - List<Artifact> externalDependencies = - Lists.transform(settings.java.externalDependencies, Artifact::parse); - List<Path> settingsClassPath = Lists.transform(settings.java.classPath, Paths::get); - - InferConfig infer = - new InferConfig( - workspaceRoot, - externalDependencies, - settingsClassPath, - mavenHome, - gradleHome); - Set<Path> classPath = infer.buildClassPath(), - workspaceClassPath = infer.workspaceClassPath(), - docPath = infer.buildDocPath(); - - // If user does not specify java.externalDependencies, look for javaconfig.json - // This is for compatibility with the old behavior and should eventually be removed - if (settings.java.externalDependencies.isEmpty() - && Files.exists(workspaceRoot.resolve("javaconfig.json"))) { - LegacyConfig legacy = new LegacyConfig(workspaceRoot); - Optional<JavacConfig> found = legacy.readJavaConfig(workspaceRoot); - - classPath = found.map(c -> c.classPath).orElse(classPath); - workspaceClassPath = found.map(c -> c.workspaceClassPath).orElse(workspaceClassPath); - docPath = found.map(c -> c.docPath).orElse(docPath); + private static Position position(String content, long offset) { + int line = 0, column = 0; + for (int i = 0; i < offset; i++) { + if (content.charAt(i) == '\n') { + line++; + column = 0; + } else column++; } + return new Position(line, column); + } - LOG.info("Inferred configuration: "); - LOG.info("\tsourcePath:" + Joiner.on(' ').join(sourcePath)); - LOG.info("\tclassPath:" + Joiner.on(' ').join(classPath)); - LOG.info("\tworkspaceClassPath:" + Joiner.on(' ').join(workspaceClassPath)); - LOG.info("\tdocPath:" + Joiner.on(' ').join(docPath)); - - JavacHolder compiler = - JavacHolder.create(sourcePath, Sets.union(classPath, workspaceClassPath)); - Javadocs docs = new Javadocs(sourcePath, docPath, textDocuments::activeContent); - FindSymbols find = new FindSymbols(index, compiler, textDocuments::activeContent); - - return new Configured(compiler, docs, index, find); + void publishDiagnostics(Collection<URI> files, List<Diagnostic<? extends JavaFileObject>> javaDiagnostics) { + for (var f : files) { + List<org.eclipse.lsp4j.Diagnostic> ds = new ArrayList<>(); + for (var j : javaDiagnostics) { + var uri = j.getSource().toUri(); + if (uri.equals(f)) { + var content = textDocuments.contents(uri).content; + var start = position(content, j.getStartPosition()); + var end = position(content, j.getEndPosition()); + var sev = severity(j.getKind()); + var d = new org.eclipse.lsp4j.Diagnostic(); + d.setSeverity(sev); + d.setRange(new Range(start, end)); + d.setCode(j.getCode()); + d.setMessage(j.getMessage(null)); + ds.add(d); + } + } + client.publishDiagnostics(new PublishDiagnosticsParams(f.toString(), ds)); + } } - private void clearDiagnostics() { - InferConfig.allJavaFiles(workspaceRoot).forEach(this::clearFileDiagnostics); + void lint(Collection<URI> uris) { + var paths = + uris.stream() + .filter(uri -> uri.getScheme().equals("file")) + .map(uri -> Paths.get(uri)) + .collect(Collectors.toList()); + LOG.info("Lint " + paths.stream().map(p -> p.getFileName().toString()).collect(Collectors.joining(", "))); + publishDiagnostics(uris, compiler.lint(paths)); } - private void clearFileDiagnostics(Path file) { - client.thenAccept( - c -> - c.publishDiagnostics( - new PublishDiagnosticsParams( - file.toUri().toString(), new ArrayList<>()))); + private JavaCompilerService createCompiler() { + Objects.requireNonNull(workspaceRoot, "Can't create compiler because workspaceRoot has not been initialized"); + var infer = new InferConfig(workspaceRoot); + return new JavaCompilerService( + InferSourcePath.sourcePath(workspaceRoot), infer.classPath(), infer.buildDocPath()); } @Override public CompletableFuture<InitializeResult> initialize(InitializeParams params) { - workspaceRoot = Paths.get(params.getRootPath()).toAbsolutePath().normalize(); + this.workspaceRoot = Paths.get(URI.create(params.getRootUri())); + this.compiler = createCompiler(); - InitializeResult result = new InitializeResult(); - ServerCapabilities c = new ServerCapabilities(); + var result = new InitializeResult(); + var c = new ServerCapabilities(); c.setTextDocumentSync(TextDocumentSyncKind.Incremental); c.setDefinitionProvider(true); @@ -144,10 +104,9 @@ class JavaLanguageServer implements LanguageServer { c.setWorkspaceSymbolProvider(true); c.setReferencesProvider(true); c.setDocumentSymbolProvider(true); - c.setCodeActionProvider(true); - c.setExecuteCommandProvider( - new ExecuteCommandOptions(ImmutableList.of("Java.importClass"))); c.setSignatureHelpProvider(new SignatureHelpOptions(ImmutableList.of("(", ","))); + c.setDocumentFormattingProvider(true); + c.setCodeLensProvider(new CodeLensOptions(false)); result.setCapabilities(c); @@ -172,95 +131,7 @@ class JavaLanguageServer implements LanguageServer { return workspace; } - public Optional<Element> findSymbol(URI file, int line, int character) { - Optional<String> content = textDocuments.activeContent(file); - FocusedResult result = - configured().compiler.compileFocused(file, content, line, character, false); - Trees trees = Trees.instance(result.task); - Function<TreePath, Optional<Element>> findSymbol = - cursor -> Optional.ofNullable(trees.getElement(cursor)); - - return result.cursor.flatMap(findSymbol); - } - void installClient(LanguageClient client) { - this.client.complete(client); - - Handler sendToClient = - new Handler() { - @Override - public void publish(LogRecord record) { - String message = record.getMessage(); - - if (record.getThrown() != null) { - StringWriter trace = new StringWriter(); - - record.getThrown().printStackTrace(new PrintWriter(trace)); - message += "\n" + trace; - } - - client.logMessage( - new MessageParams( - messageType(record.getLevel().intValue()), message)); - } - - private MessageType messageType(int level) { - if (level >= Level.SEVERE.intValue()) return MessageType.Error; - else if (level >= Level.WARNING.intValue()) return MessageType.Warning; - else if (level >= Level.INFO.intValue()) return MessageType.Info; - else return MessageType.Log; - } - - @Override - public void flush() {} - - @Override - public void close() throws SecurityException {} - }; - - Logger.getLogger("").addHandler(sendToClient); - } - - static void onDiagnostic(Diagnostic<? extends JavaFileObject> diagnostic) { - Level level = level(diagnostic.getKind()); - String message = diagnostic.getMessage(null); - - LOG.log(level, message); - } - - private static Level level(Diagnostic.Kind kind) { - switch (kind) { - case ERROR: - return Level.SEVERE; - case WARNING: - case MANDATORY_WARNING: - return Level.WARNING; - case NOTE: - return Level.INFO; - case OTHER: - default: - return Level.FINE; - } - } - - /** - * Compile a .java source and emit a .class file. - * - * <p>Useful for testing that the language server works when driven by .class files. - */ - void compile(URI file) { - Objects.requireNonNull(file, "file is null"); - - configured() - .compiler - .compileBatch(Collections.singletonMap(file, textDocuments.activeContent(file))); - } - - private static String jsonStringify(Object value) { - try { - return Main.JSON.writeValueAsString(value); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + this.client = client; } } diff --git a/src/main/java/org/javacs/JavaSettings.java b/src/main/java/org/javacs/JavaSettings.java deleted file mode 100644 index c656268..0000000 --- a/src/main/java/org/javacs/JavaSettings.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.javacs; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -public class JavaSettings { - public Java java = new Java(); - - public static class Java { - public List<String> classPath = new ArrayList<>(); - public List<String> externalDependencies = new ArrayList<>(); - public Optional<String> javaHome = Optional.empty(); - } -} diff --git a/src/main/java/org/javacs/JavaTextDocumentService.java b/src/main/java/org/javacs/JavaTextDocumentService.java index bcf732a..af7e64e 100644 --- a/src/main/java/org/javacs/JavaTextDocumentService.java +++ b/src/main/java/org/javacs/JavaTextDocumentService.java @@ -1,283 +1,424 @@ package org.javacs; -import com.google.common.base.Joiner; -import com.google.common.collect.Sets; +import com.google.gson.JsonPrimitive; +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.ParamTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.TreePath; import com.sun.source.util.Trees; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.net.URI; -import java.time.Duration; -import java.time.Instant; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; import java.util.stream.Collectors; -import javax.lang.model.element.Element; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaFileObject; +import javax.lang.model.element.*; import org.eclipse.lsp4j.*; -import org.eclipse.lsp4j.jsonrpc.CompletableFutures; +import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; class JavaTextDocumentService implements TextDocumentService { - private final CompletableFuture<LanguageClient> client; private final JavaLanguageServer server; private final Map<URI, VersionedContent> activeDocuments = new HashMap<>(); - JavaTextDocumentService(CompletableFuture<LanguageClient> client, JavaLanguageServer server) { - this.client = client; + JavaTextDocumentService(JavaLanguageServer server) { this.server = server; } - /** Text of file, if it is in the active set */ - Optional<String> activeContent(URI file) { - return Optional.ofNullable(activeDocuments.get(file)).map(doc -> doc.content); - } - - /** All open files, not including things like old git-versions in a diff view */ - Set<URI> openFiles() { - return Sets.filter(activeDocuments.keySet(), uri -> uri.getScheme().equals("file")); + private CompletionItemKind completionItemKind(Element e) { + switch (e.getKind()) { + case ANNOTATION_TYPE: + return CompletionItemKind.Interface; + case CLASS: + return CompletionItemKind.Class; + case CONSTRUCTOR: + return CompletionItemKind.Constructor; + case ENUM: + return CompletionItemKind.Enum; + case ENUM_CONSTANT: + return CompletionItemKind.EnumMember; + case EXCEPTION_PARAMETER: + return CompletionItemKind.Variable; + case FIELD: + return CompletionItemKind.Field; + case STATIC_INIT: + case INSTANCE_INIT: + return CompletionItemKind.Function; + case INTERFACE: + return CompletionItemKind.Interface; + case LOCAL_VARIABLE: + return CompletionItemKind.Variable; + case METHOD: + return CompletionItemKind.Method; + case PACKAGE: + return CompletionItemKind.Module; + case PARAMETER: + return CompletionItemKind.Variable; + case RESOURCE_VARIABLE: + return CompletionItemKind.Variable; + case TYPE_PARAMETER: + return CompletionItemKind.TypeParameter; + case OTHER: + default: + return null; + } } - void doLint(Collection<URI> paths) { - LOG.info("Lint " + Joiner.on(", ").join(paths)); - - List<javax.tools.Diagnostic<? extends JavaFileObject>> errors = new ArrayList<>(); - Map<URI, Optional<String>> content = - paths.stream().collect(Collectors.toMap(f -> f, this::activeContent)); - DiagnosticCollector<JavaFileObject> compile = - server.configured().compiler.compileBatch(content); - - errors.addAll(compile.getDiagnostics()); + /** Cache of completions from the last call to `completion` */ + private final Map<String, Completion> lastCompletions = new HashMap<>(); - publishDiagnostics(paths, errors); + @Override + public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams position) { + var uri = URI.create(position.getTextDocument().getUri()); + var content = contents(uri).content; + var line = position.getPosition().getLine() + 1; + var column = position.getPosition().getCharacter() + 1; + var result = new ArrayList<CompletionItem>(); + lastCompletions.clear(); + var completions = server.compiler.completions(uri, content, line, column, 50); + for (var c : completions.items) { + var i = new CompletionItem(); + var id = UUID.randomUUID().toString(); + i.setData(id); + lastCompletions.put(id, c); + if (c.element != null) { + i.setLabel(c.element.getSimpleName().toString()); + i.setKind(completionItemKind(c.element)); + // Detailed name will be resolved later, using docs to fill in method names + if (!(c.element instanceof ExecutableElement)) i.setDetail(c.element.toString()); + } else if (c.packagePart != null) { + i.setLabel(c.packagePart.name); + i.setKind(CompletionItemKind.Module); + i.setDetail(c.packagePart.fullName); + } else if (c.keyword != null) { + i.setLabel(c.keyword); + i.setKind(CompletionItemKind.Keyword); + i.setDetail("keyword"); + } else if (c.notImportedClass != null) { + i.setLabel(Parser.lastName(c.notImportedClass)); + i.setKind(CompletionItemKind.Class); + i.setDetail(c.notImportedClass); + } else throw new RuntimeException(c + " is not valid"); + + result.add(i); + } + return CompletableFuture.completedFuture(Either.forRight(new CompletionList(completions.isIncomplete, result))); } - private void publishDiagnostics( - Collection<URI> touched, - List<javax.tools.Diagnostic<? extends JavaFileObject>> diagnostics) { - Map<URI, PublishDiagnosticsParams> files = - touched.stream() - .collect( - Collectors.toMap( - uri -> uri, - newUri -> - new PublishDiagnosticsParams( - newUri.toString(), new ArrayList<>()))); + private String resolveDocDetail(MethodTree doc) { + var args = new StringJoiner(", "); + for (var p : doc.getParameters()) { + args.add(p.getName()); + } + return String.format("%s(%s)", doc.getName(), args); + } - // Organize diagnostics by file - for (javax.tools.Diagnostic<? extends JavaFileObject> error : diagnostics) { - URI uri = error.getSource().toUri(); - PublishDiagnosticsParams publish = - files.computeIfAbsent( - uri, - newUri -> - new PublishDiagnosticsParams( - newUri.toString(), new ArrayList<>())); - Lints.convert(error).ifPresent(publish.getDiagnostics()::add); + private String resolveDefaultDetail(ExecutableElement method) { + var args = new StringJoiner(", "); + var missingParamNames = + method.getParameters().stream().allMatch(p -> p.getSimpleName().toString().matches("arg\\d+")); + for (var p : method.getParameters()) { + if (missingParamNames) args.add(ShortTypePrinter.print(p.asType())); + else args.add(p.getSimpleName().toString()); } + return String.format("%s(%s)", method.getSimpleName(), args); + } - // If there are no errors in a file, put an empty PublishDiagnosticsParams - for (URI each : touched) files.putIfAbsent(each, new PublishDiagnosticsParams()); + private String asMarkdown(List<? extends DocTree> lines) { + var join = new StringJoiner("\n"); + for (var l : lines) join.add(l.toString()); + var html = join.toString(); + return Docs.htmlToMarkdown(html); + } - files.forEach( - (file, errors) -> { - if (touched.contains(file)) { - client.join().publishDiagnostics(errors); + private String asMarkdown(DocCommentTree comment) { + var lines = comment.getFirstSentence(); + return asMarkdown(lines); + } - LOG.info( - "Published " - + errors.getDiagnostics().size() - + " errors from " - + file); - } else - LOG.info( - "Ignored " - + errors.getDiagnostics().size() - + " errors from not-open " - + file); - }); + private MarkupContent asMarkupContent(DocCommentTree comment) { + var markdown = asMarkdown(comment); + var content = new MarkupContent(); + content.setKind(MarkupKind.MARKDOWN); + content.setValue(markdown); + return content; } @Override - public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion( - TextDocumentPositionParams position) { - Instant started = Instant.now(); - URI uri = URI.create(position.getTextDocument().getUri()); - Optional<String> content = activeContent(uri); - int line = position.getPosition().getLine() + 1; - int character = position.getPosition().getCharacter() + 1; - - LOG.info(String.format("completion at %s %d:%d", uri, line, character)); - - Configured config = server.configured(); - FocusedResult result = config.compiler.compileFocused(uri, content, line, character, true); - List<CompletionItem> items = - Completions.at(result, config.index, config.docs) - .limit(server.maxItems) - .collect(Collectors.toList()); - CompletionList list = new CompletionList(items.size() == server.maxItems, items); - Duration elapsed = Duration.between(started, Instant.now()); - - if (list.isIncomplete()) - LOG.info( - String.format( - "Found %d items (incomplete) in %d ms", - items.size(), elapsed.toMillis())); - else LOG.info(String.format("Found %d items in %d ms", items.size(), elapsed.toMillis())); + public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) { + var idJson = (JsonPrimitive) unresolved.getData(); + var id = idJson.getAsString(); + var cached = lastCompletions.get(id); + if (cached == null) { + LOG.warning("CompletionItem " + id + " was not in the cache"); + return CompletableFuture.completedFuture(unresolved); + } + if (cached.element != null) { + if (cached.element instanceof ExecutableElement) { + var method = (ExecutableElement) cached.element; + var tree = server.compiler.methodTree(method); + var detail = tree.map(this::resolveDocDetail).orElse(resolveDefaultDetail(method)); + unresolved.setDetail(detail); + + var doc = server.compiler.methodDoc(method); + var markdown = doc.map(this::asMarkupContent); + markdown.ifPresent(unresolved::setDocumentation); + } else if (cached.element instanceof TypeElement) { + var type = (TypeElement) cached.element; + var doc = server.compiler.classDoc(type); + var markdown = doc.map(this::asMarkupContent); + markdown.ifPresent(unresolved::setDocumentation); + } else { + LOG.info("Don't know how to look up docs for element " + cached.element); + } + // TODO constructors, fields + } else if (cached.notImportedClass != null) { + var doc = server.compiler.classDoc(cached.notImportedClass); + var markdown = doc.map(this::asMarkupContent); + markdown.ifPresent(unresolved::setDocumentation); + } + return CompletableFuture.completedFuture(unresolved); // TODO + } - return CompletableFuture.completedFuture(Either.forRight(list)); + private String hoverTypeDeclaration(TypeElement t) { + var result = new StringBuilder(); + switch (t.getKind()) { + case ANNOTATION_TYPE: + result.append("@interface"); + break; + case INTERFACE: + result.append("interface"); + break; + case CLASS: + result.append("class"); + break; + default: + LOG.warning("Don't know what to call type element " + t); + result.append("???"); + } + result.append(" ").append(ShortTypePrinter.print(t.asType())); + var superType = ShortTypePrinter.print(t.getSuperclass()); + switch (superType) { + case "Object": + case "none": + break; + default: + result.append(" extends ").append(superType); + } + return result.toString(); } - @Override - public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) { - return CompletableFutures.computeAsync( - cancel -> { - server.configured().docs.resolveCompletionItem(unresolved); + private String hoverCode(Element e) { + if (e instanceof ExecutableElement) { + var m = (ExecutableElement) e; + return ShortTypePrinter.printMethod(m); + } else if (e instanceof VariableElement) { + var v = (VariableElement) e; + return ShortTypePrinter.print(v.asType()) + " " + v; + } else if (e instanceof TypeElement) { + var t = (TypeElement) e; + var lines = new StringJoiner("\n"); + lines.add(hoverTypeDeclaration(t) + " {"); + for (var member : t.getEnclosedElements()) { + // TODO check accessibility + if (member instanceof ExecutableElement || member instanceof VariableElement) { + lines.add(" " + hoverCode(member) + ";"); + } else if (member instanceof TypeElement) { + lines.add(" " + hoverTypeDeclaration((TypeElement) member) + " { /* removed */ }"); + } + } + lines.add("}"); + return lines.toString(); + } else return e.toString(); + } - return unresolved; - }); + private Optional<String> hoverDocs(Element e) { + if (e instanceof ExecutableElement) { + var m = (ExecutableElement) e; + return server.compiler.methodDoc(m).map(this::asMarkdown); + } else if (e instanceof TypeElement) { + var t = (TypeElement) e; + return server.compiler.classDoc(t).map(this::asMarkdown); + } else return Optional.empty(); } @Override public CompletableFuture<Hover> hover(TextDocumentPositionParams position) { - URI uri = URI.create(position.getTextDocument().getUri()); - Optional<String> content = activeContent(uri); - int line = position.getPosition().getLine() + 1; - int character = position.getPosition().getCharacter() + 1; - - LOG.info(String.format("hover at %s %d:%d", uri, line, character)); - - FocusedResult result = - server.configured().compiler.compileFocused(uri, content, line, character, false); - Hover hover = elementAtCursor(result).map(this::hoverText).orElseGet(this::emptyHover); - - return CompletableFuture.completedFuture(hover); + var uri = URI.create(position.getTextDocument().getUri()); + var content = contents(uri).content; + var line = position.getPosition().getLine() + 1; + var column = position.getPosition().getCharacter() + 1; + var e = server.compiler.element(uri, content, line, column); + if (e != null) { + List<Either<String, MarkedString>> result = new ArrayList<>(); + result.add(Either.forRight(new MarkedString("java", hoverCode(e)))); + hoverDocs(e).ifPresent(doc -> result.add(Either.forLeft(doc))); + return CompletableFuture.completedFuture(new Hover(result)); + } else return CompletableFuture.completedFuture(new Hover(Collections.emptyList())); } - private Optional<Element> elementAtCursor(FocusedResult compiled) { - return compiled.cursor.flatMap( - cursor -> { - Element el = Trees.instance(compiled.task).getElement(cursor); + private List<ParameterInformation> signatureParamsFromDocs(MethodTree method, DocCommentTree doc) { + var ps = new ArrayList<ParameterInformation>(); + var paramComments = new HashMap<String, String>(); + for (var tag : doc.getBlockTags()) { + if (tag.getKind() == DocTree.Kind.PARAM) { + var param = (ParamTree) tag; + paramComments.put(param.getName().toString(), asMarkdown(param.getDescription())); + } + } + for (var param : method.getParameters()) { + var info = new ParameterInformation(); + var name = param.getName().toString(); + info.setLabel(name); + if (paramComments.containsKey(name)) info.setDocumentation(paramComments.get(name)); + else info.setDocumentation(Objects.toString(param.getType(), "")); + ps.add(info); + } + return ps; + } - return Optional.ofNullable(el); - }); + private List<ParameterInformation> signatureParamsFromMethod(ExecutableElement e) { + var missingParamNames = ShortTypePrinter.missingParamNames(e); + var ps = new ArrayList<ParameterInformation>(); + for (var v : e.getParameters()) { + var p = new ParameterInformation(); + if (missingParamNames) p.setLabel(ShortTypePrinter.print(v.asType())); + else p.setLabel(v.getSimpleName().toString()); + ps.add(p); + } + return ps; } - private Hover hoverText(Element el) { - return Hovers.hoverText(el, server.configured().docs); + private SignatureInformation asSignatureInformation(ExecutableElement e) { + var i = new SignatureInformation(); + var ps = signatureParamsFromMethod(e); + var doc = server.compiler.methodDoc(e); + var tree = server.compiler.methodTree(e); + if (doc.isPresent() && tree.isPresent()) ps = signatureParamsFromDocs(tree.get(), doc.get()); + var args = ps.stream().map(p -> p.getLabel()).collect(Collectors.joining(", ")); + var name = e.getSimpleName().toString(); + if (name.equals("<init>")) name = e.getEnclosingElement().getSimpleName().toString(); + i.setLabel(name + "(" + args + ")"); + i.setParameters(ps); + return i; } - private Hover emptyHover() { - return new Hover(Collections.emptyList(), null); + private SignatureHelp asSignatureHelp(MethodInvocation invoke) { + // TODO use docs to get parameter names + var sigs = new ArrayList<SignatureInformation>(); + for (var e : invoke.overloads) { + sigs.add(asSignatureInformation(e)); + } + var activeSig = invoke.activeMethod.map(invoke.overloads::indexOf).orElse(0); + return new SignatureHelp(sigs, activeSig, invoke.activeParameter); } @Override public CompletableFuture<SignatureHelp> signatureHelp(TextDocumentPositionParams position) { - URI uri = URI.create(position.getTextDocument().getUri()); - Optional<String> content = activeContent(uri); - int line = position.getPosition().getLine() + 1; - int character = position.getPosition().getCharacter() + 1; - - LOG.info(String.format("signatureHelp at %s %d:%d", uri, line, character)); - - Configured config = server.configured(); - FocusedResult result = config.compiler.compileFocused(uri, content, line, character, true); - SignatureHelp help = - Signatures.help(result, line, character, config.docs).orElseGet(SignatureHelp::new); - + var uri = URI.create(position.getTextDocument().getUri()); + var content = contents(uri).content; + var line = position.getPosition().getLine() + 1; + var column = position.getPosition().getCharacter() + 1; + var help = + server.compiler + .methodInvocation(uri, content, line, column) + .map(this::asSignatureHelp) + .orElse(new SignatureHelp()); return CompletableFuture.completedFuture(help); } - @Override - public CompletableFuture<List<? extends Location>> definition( - TextDocumentPositionParams position) { - URI uri = URI.create(position.getTextDocument().getUri()); - Optional<String> content = activeContent(uri); - int line = position.getPosition().getLine() + 1; - int character = position.getPosition().getCharacter() + 1; - - LOG.info(String.format("definition at %s %d:%d", uri, line, character)); - - Configured config = server.configured(); - FocusedResult result = config.compiler.compileFocused(uri, content, line, character, false); - List<Location> locations = - References.gotoDefinition(result, config.find) - .map(Collections::singletonList) - .orElseGet(Collections::emptyList); - return CompletableFuture.completedFuture(locations); + private Location location(TreePath p) { + var trees = server.compiler.trees(); + var pos = trees.getSourcePositions(); + var cu = p.getCompilationUnit(); + var lines = cu.getLineMap(); + long start = pos.getStartPosition(cu, p.getLeaf()), end = pos.getEndPosition(cu, p.getLeaf()); + int startLine = (int) lines.getLineNumber(start) - 1, startCol = (int) lines.getColumnNumber(start) - 1; + int endLine = (int) lines.getLineNumber(end) - 1, endCol = (int) lines.getColumnNumber(end) - 1; + var dUri = cu.getSourceFile().toUri(); + return new Location( + dUri.toString(), new Range(new Position(startLine, startCol), new Position(endLine, endCol))); } @Override - public CompletableFuture<List<? extends Location>> references(ReferenceParams params) { - URI uri = URI.create(params.getTextDocument().getUri()); - Optional<String> content = activeContent(uri); - int line = params.getPosition().getLine() + 1; - int character = params.getPosition().getCharacter() + 1; - - LOG.info(String.format("references at %s %d:%d", uri, line, character)); - - Configured config = server.configured(); - FocusedResult result = config.compiler.compileFocused(uri, content, line, character, false); - List<Location> locations = - References.findReferences(result, config.find) - .limit(server.maxItems) - .collect(Collectors.toList()); + public CompletableFuture<List<? extends Location>> definition(TextDocumentPositionParams position) { + var uri = URI.create(position.getTextDocument().getUri()); + var line = position.getPosition().getLine() + 1; + var column = position.getPosition().getCharacter() + 1; + var result = new ArrayList<Location>(); + server.compiler.definition(uri, line, column, f -> contents(f).content).ifPresent(d -> result.add(location(d))); + return CompletableFuture.completedFuture(result); + } - return CompletableFuture.completedFuture(locations); + @Override + public CompletableFuture<List<? extends Location>> references(ReferenceParams position) { + var uri = URI.create(position.getTextDocument().getUri()); + var content = contents(uri).content; + var line = position.getPosition().getLine() + 1; + var column = position.getPosition().getCharacter() + 1; + var result = new ArrayList<Location>(); + for (var r : server.compiler.references(uri, content, line, column)) { + result.add(location(r)); + } + return CompletableFuture.completedFuture(result); } @Override - public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight( - TextDocumentPositionParams position) { + public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(TextDocumentPositionParams position) { return null; } @Override - public CompletableFuture<List<? extends SymbolInformation>> documentSymbol( - DocumentSymbolParams params) { - URI uri = URI.create(params.getTextDocument().getUri()); - List<SymbolInformation> symbols = - server.configured().index.allInFile(uri).collect(Collectors.toList()); - - return CompletableFuture.completedFuture(symbols); + public CompletableFuture<List<? extends SymbolInformation>> documentSymbol(DocumentSymbolParams params) { + var uri = URI.create(params.getTextDocument().getUri()); + var content = contents(uri).content; + var result = + Parser.documentSymbols(Paths.get(uri), content) + .stream() + .map(Parser::asSymbolInformation) + .collect(Collectors.toList()); + return CompletableFuture.completedFuture(result); } @Override public CompletableFuture<List<? extends Command>> codeAction(CodeActionParams params) { - // Compilation is expensive - // Don't do it unless a codeAction is actually possible - // At the moment we only generate code actions in response to diagnostics - if (params.getContext().getDiagnostics().isEmpty()) - return CompletableFuture.completedFuture(Collections.emptyList()); - - URI uri = URI.create(params.getTextDocument().getUri()); - int line = params.getRange().getStart().getLine() + 1; - int character = params.getRange().getStart().getCharacter() + 1; - - LOG.info(String.format("codeAction at %s %d:%d", uri, line, character)); - - Configured config = server.configured(); - List<Command> commands = - new CodeActions( - config.compiler, - uri, - activeContent(uri), - line, - character, - config.index) - .find(params); - - return CompletableFuture.completedFuture(commands); + return null; } @Override public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params) { - return null; + var uri = URI.create(params.getTextDocument().getUri()); + var content = contents(uri).content; + var tests = server.compiler.testMethods(uri, content); + + var result = new ArrayList<CodeLens>(); + for (var test : tests) { + var trees = Trees.instance(test.parseTask); + var pos = trees.getSourcePositions(); + var start = pos.getStartPosition(test.compilationUnit, test.method); + var end = pos.getEndPosition(test.compilationUnit, test.method); + var lines = test.compilationUnit.getLineMap(); + var startLine = (int) lines.getLineNumber(start) - 1; + var startCol = (int) lines.getColumnNumber(start) - 1; + var endLine = (int) lines.getLineNumber(end) - 1; + var endCol = (int) lines.getColumnNumber(end) - 1; + var range = new Range(new Position(startLine, startCol), new Position(endLine, endCol)); + var sourceUri = test.compilationUnit.getSourceFile().toUri(); + var className = test.enclosingClass.getSimpleName().toString(); + var methodName = test.method.getName().toString(); + var command = new Command("Run Test", "java.command.test.run", List.of(sourceUri, className, methodName)); + result.add(new CodeLens(range, command, null)); + } + // TODO run all tests in file + // TODO run all tests in package + return CompletableFuture.completedFuture(result); } @Override @@ -285,20 +426,69 @@ class JavaTextDocumentService implements TextDocumentService { return null; } + private List<TextEdit> fixImports(URI java) { + var contents = server.textDocuments.contents(java).content; + var fix = server.compiler.fixImports(java, contents); + // TODO if imports already match fixed-imports, return empty list + // TODO preserve comments and other details of existing imports + var edits = new ArrayList<TextEdit>(); + // Delete all existing imports + for (var i : fix.parsed.getImports()) { + if (!i.isStatic()) { + var offset = fix.sourcePositions.getStartPosition(fix.parsed, i); + var line = (int) fix.parsed.getLineMap().getLineNumber(offset) - 1; + var delete = new TextEdit(new Range(new Position(line, 0), new Position(line + 1, 0)), ""); + edits.add(delete); + } + } + if (fix.fixedImports.isEmpty()) return edits; + // Find a place to insert the new imports + long insertLine = -1; + var insertText = new StringBuilder(); + // If there are imports, use the start of the first import as the insert position + for (var i : fix.parsed.getImports()) { + if (!i.isStatic() && insertLine == -1) { + long offset = fix.sourcePositions.getStartPosition(fix.parsed, i); + insertLine = fix.parsed.getLineMap().getLineNumber(offset) - 1; + } + } + // If there are no imports, insert after the package declaration + if (insertLine == -1 && fix.parsed.getPackageName() != null) { + long offset = fix.sourcePositions.getEndPosition(fix.parsed, fix.parsed.getPackageName()); + insertLine = fix.parsed.getLineMap().getLineNumber(offset); + insertText.append("\n"); + } + // If there are no imports and no package, insert at the top of the file + if (insertLine == -1) { + insertLine = 0; + } + // Insert each import + fix.fixedImports + .stream() + .sorted() + .forEach( + i -> { + insertText.append("import ").append(i).append(";\n"); + }); + var insertPosition = new Position((int) insertLine, 0); + var insert = new TextEdit(new Range(insertPosition, insertPosition), insertText.toString()); + edits.add(insert); + return edits; + } + @Override public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params) { - return null; + var uri = URI.create(params.getTextDocument().getUri()); + return CompletableFuture.completedFuture(fixImports(uri)); } @Override - public CompletableFuture<List<? extends TextEdit>> rangeFormatting( - DocumentRangeFormattingParams params) { + public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams params) { return null; } @Override - public CompletableFuture<List<? extends TextEdit>> onTypeFormatting( - DocumentOnTypeFormattingParams params) { + public CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams params) { return null; } @@ -307,45 +497,45 @@ class JavaTextDocumentService implements TextDocumentService { return null; } + private boolean isJava(URI uri) { + return uri.getPath().endsWith(".java"); + } + @Override public void didOpen(DidOpenTextDocumentParams params) { - TextDocumentItem document = params.getTextDocument(); - URI uri = URI.create(document.getUri()); - - activeDocuments.put(uri, new VersionedContent(document.getText(), document.getVersion())); - - doLint(Collections.singleton(uri)); + var document = params.getTextDocument(); + var uri = URI.create(document.getUri()); + if (isJava(uri)) { + activeDocuments.put(uri, new VersionedContent(document.getText(), document.getVersion())); + server.lint(Collections.singleton(uri)); + } } @Override public void didChange(DidChangeTextDocumentParams params) { - VersionedTextDocumentIdentifier document = params.getTextDocument(); - URI uri = URI.create(document.getUri()); - VersionedContent existing = activeDocuments.get(uri); - String newText = existing.content; - - if (document.getVersion() > existing.version) { - for (TextDocumentContentChangeEvent change : params.getContentChanges()) { - if (change.getRange() == null) - activeDocuments.put( - uri, new VersionedContent(change.getText(), document.getVersion())); - else newText = patch(newText, change); - } - - activeDocuments.put(uri, new VersionedContent(newText, document.getVersion())); - } else - LOG.warning( - "Ignored change with version " - + document.getVersion() - + " <= " - + existing.version); + var document = params.getTextDocument(); + var uri = URI.create(document.getUri()); + if (isJava(uri)) { + var existing = activeDocuments.get(uri); + var newText = existing.content; + + if (document.getVersion() > existing.version) { + for (var change : params.getContentChanges()) { + if (change.getRange() == null) + activeDocuments.put(uri, new VersionedContent(change.getText(), document.getVersion())); + else newText = patch(newText, change); + } + + activeDocuments.put(uri, new VersionedContent(newText, document.getVersion())); + } else LOG.warning("Ignored change with version " + document.getVersion() + " <= " + existing.version); + } } private String patch(String sourceText, TextDocumentContentChangeEvent change) { try { - Range range = change.getRange(); - BufferedReader reader = new BufferedReader(new StringReader(sourceText)); - StringWriter writer = new StringWriter(); + var range = change.getRange(); + var reader = new BufferedReader(new StringReader(sourceText)); + var writer = new StringWriter(); // Skip unchanged lines int line = 0; @@ -379,22 +569,43 @@ class JavaTextDocumentService implements TextDocumentService { @Override public void didClose(DidCloseTextDocumentParams params) { - TextDocumentIdentifier document = params.getTextDocument(); - URI uri = URI.create(document.getUri()); - - // Remove from source cache - activeDocuments.remove(uri); - - // Clear diagnostics - client.join() - .publishDiagnostics( - new PublishDiagnosticsParams(uri.toString(), new ArrayList<>())); + var document = params.getTextDocument(); + var uri = URI.create(document.getUri()); + if (isJava(uri)) { + // Remove from source cache + activeDocuments.remove(uri); + + // Clear diagnostics + server.publishDiagnostics(Collections.singletonList(uri), Collections.emptyList()); + } } @Override public void didSave(DidSaveTextDocumentParams params) { - // Re-lint all active documents - doLint(openFiles()); + var uri = URI.create(params.getTextDocument().getUri()); + if (isJava(uri)) { + // Re-lint all active documents + server.lint(activeDocuments.keySet()); + // TODO update config when java file implies a new source root + } + // TODO update config when pom.xml changes + } + + Set<URI> activeDocuments() { + return activeDocuments.keySet(); + } + + VersionedContent contents(URI openFile) { + if (activeDocuments.containsKey(openFile)) { + return activeDocuments.get(openFile); + } else { + try { + var content = Files.readAllLines(Paths.get(openFile)).stream().collect(Collectors.joining("\n")); + return new VersionedContent(content, -1); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } private static final Logger LOG = Logger.getLogger("main"); diff --git a/src/main/java/org/javacs/JavaWorkspaceService.java b/src/main/java/org/javacs/JavaWorkspaceService.java index 7572cfc..13c5e70 100644 --- a/src/main/java/org/javacs/JavaWorkspaceService.java +++ b/src/main/java/org/javacs/JavaWorkspaceService.java @@ -1,96 +1,44 @@ package org.javacs; -import com.google.gson.JsonPrimitive; -import java.net.URI; -import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; import java.util.stream.Collectors; -import org.eclipse.lsp4j.*; -import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.WorkspaceSymbolParams; import org.eclipse.lsp4j.services.WorkspaceService; class JavaWorkspaceService implements WorkspaceService { - private final CompletableFuture<LanguageClient> client; + private static final Logger LOG = Logger.getLogger("main"); + private final JavaLanguageServer server; - private final JavaTextDocumentService textDocuments; - private JavaSettings settings = new JavaSettings(); - JavaWorkspaceService( - CompletableFuture<LanguageClient> client, - JavaLanguageServer server, - JavaTextDocumentService textDocuments) { - this.client = client; + JavaWorkspaceService(JavaLanguageServer server) { this.server = server; - this.textDocuments = textDocuments; - } - - JavaSettings settings() { - return settings; } @Override public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) { - LOG.info(params.toString()); - - switch (params.getCommand()) { - case "Java.importClass": - JsonPrimitive fileStringJson = (JsonPrimitive) params.getArguments().get(0), - packageNameJson = (JsonPrimitive) params.getArguments().get(1), - classNameJson = (JsonPrimitive) params.getArguments().get(2); - String fileString = fileStringJson.getAsString(), - packageName = packageNameJson.getAsString(), - className = classNameJson.getAsString(); - URI fileUri = URI.create(fileString); - FocusedResult compiled = - server.configured() - .compiler - .compileFocused( - fileUri, textDocuments.activeContent(fileUri), 1, 1, false); - - if (compiled.compilationUnit.getSourceFile().toUri().equals(fileUri)) { - List<TextEdit> edits = - new RefactorFile(compiled.task, compiled.compilationUnit) - .addImport(packageName, className); - - client.join() - .applyEdit( - new ApplyWorkspaceEditParams( - new WorkspaceEdit( - Collections.singletonMap(fileString, edits)))); - } - - break; - default: - LOG.warning("Don't know what to do with " + params.getCommand()); - } - - return CompletableFuture.completedFuture("Done"); + return null; } @Override - public CompletableFuture<List<? extends SymbolInformation>> symbol( - WorkspaceSymbolParams params) { - List<SymbolInformation> infos = - server.configured() - .index - .search(params.getQuery()) - .limit(server.maxItems) + public CompletableFuture<List<? extends SymbolInformation>> symbol(WorkspaceSymbolParams params) { + List<SymbolInformation> list = + server.compiler + .findSymbols(params.getQuery()) + .map(Parser::asSymbolInformation) + .limit(50) .collect(Collectors.toList()); - - return CompletableFuture.completedFuture(infos); + return CompletableFuture.completedFuture(list); } @Override - public void didChangeConfiguration(DidChangeConfigurationParams change) { - settings = Main.JSON.convertValue(change.getSettings(), JavaSettings.class); - } + public void didChangeConfiguration(DidChangeConfigurationParams change) {} @Override - public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { - textDocuments.doLint(textDocuments.openFiles()); - } - - private static final Logger LOG = Logger.getLogger("main"); + public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {} } diff --git a/src/main/java/org/javacs/JavacConfig.java b/src/main/java/org/javacs/JavacConfig.java deleted file mode 100644 index 8490b8e..0000000 --- a/src/main/java/org/javacs/JavacConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.javacs; - -import java.nio.file.Path; -import java.util.Objects; -import java.util.Set; - -public class JavacConfig { - public final Set<Path> classPath, workspaceClassPath, docPath; - - public JavacConfig(Set<Path> classPath, Set<Path> workspaceClassPath, Set<Path> docPath) { - this.classPath = classPath; - this.workspaceClassPath = workspaceClassPath; - this.docPath = docPath; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - JavacConfig that = (JavacConfig) o; - return Objects.equals(classPath, that.classPath) - && Objects.equals(workspaceClassPath, that.workspaceClassPath) - && Objects.equals(docPath, that.docPath); - } - - @Override - public int hashCode() { - return Objects.hash(classPath, workspaceClassPath, docPath); - } -} diff --git a/src/main/java/org/javacs/JavacHolder.java b/src/main/java/org/javacs/JavacHolder.java deleted file mode 100644 index 69437d0..0000000 --- a/src/main/java/org/javacs/JavacHolder.java +++ /dev/null @@ -1,349 +0,0 @@ -package org.javacs; - -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; -import com.sun.source.util.TaskEvent; -import com.sun.source.util.TaskListener; -import com.sun.source.util.TreePath; -import com.sun.tools.javac.api.JavacTaskImpl; -import com.sun.tools.javac.api.JavacTool; -import com.sun.tools.javac.file.JavacFileManager; -import com.sun.tools.javac.main.JavaCompiler; -import com.sun.tools.javac.util.Options; -import java.io.*; -import java.net.URI; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.time.Instant; -import java.util.*; -import java.util.function.BiConsumer; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import javax.lang.model.element.Element; -import javax.tools.*; - -/** - * Maintains a reference to a Java compiler, and several of its internal data structures, which we - * need to fiddle with to get incremental compilation and extract the diagnostic information we - * want. - */ -public class JavacHolder { - - public static JavacHolder create(Set<Path> sourcePath, Set<Path> classPath) { - return new JavacHolder(sourcePath, classPath); - } - - /** - * Compile a single file, without updating the index. - * - * <p>As an optimization, this function may ignore code not accessible to the cursor. - */ - public FocusedResult compileFocused( - URI file, Optional<String> textContent, int line, int column, boolean pruneStatements) { - JavaFileObject fileObject = findFile(file, textContent); - - if (pruneStatements) - fileObject = TreePruner.putSemicolonAfterCursor(fileObject, line, column); - - JavacTask task = createTask(Collections.singleton(fileObject), true); - TreePruner pruner = new TreePruner(task); - - // Record timing - EnumMap<TaskEvent.Kind, Map<URI, Profile>> profile = new EnumMap<>(TaskEvent.Kind.class); - - task.addTaskListener( - new TaskListener() { - @Override - public void started(TaskEvent e) { - if (e.getSourceFile() == null) return; - - profile.computeIfAbsent(e.getKind(), newKind -> new HashMap<>()) - .put(e.getSourceFile().toUri(), new Profile()); - } - - @Override - public void finished(TaskEvent e) { - if (e.getSourceFile() == null) return; - - if (e.getKind() == TaskEvent.Kind.PARSE) { - boolean isCursorInFile = - e.getCompilationUnit().getSourceFile().toUri().equals(file); - - if (isCursorInFile) { - pruner.removeNonCursorMethodBodies( - e.getCompilationUnit(), line, column); - - if (pruneStatements) - pruner.removeStatementsAfterCursor( - e.getCompilationUnit(), line, column); - } else { - pruner.removeAllMethodBodies(e.getCompilationUnit()); - } - } - - profile.get(e.getKind()).get(e.getSourceFile().toUri()).finished = - Optional.of(Instant.now()); - } - }); - - try { - Iterable<? extends CompilationUnitTree> parse = task.parse(); - CompilationUnitTree compilationUnit = parse.iterator().next(); - - try { - Iterable<? extends Element> analyze = task.analyze(); - } catch (AssertionError e) { - if (!catchJavacError(e)) throw e; - } - - // Log timing - profile.forEach( - (kind, timed) -> { - long elapsed = - timed.values() - .stream() - .mapToLong(p -> p.elapsed().toMillis()) - .sum(); - - if (timed.size() > 5) { - LOG.info( - String.format( - "%s\t%d ms\t%d files", - kind.name(), elapsed, timed.size())); - } else { - String names = - timed.keySet() - .stream() - .map(uri -> Paths.get(uri).getFileName().toString()) - .collect(Collectors.joining(", ")); - - LOG.info(String.format("%s\t%d ms\t%s", kind.name(), elapsed, names)); - } - }); - - Optional<TreePath> cursor = FindCursor.find(task, compilationUnit, line, column); - - return new FocusedResult(compilationUnit, cursor, task, classPathIndex); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Clear files and all their dependents, recompile, compileBatch the index, and report any - * errors. - * - * <p>If these files reference un-compiled dependencies, those dependencies will also be parsed - * and compiled. - */ - public DiagnosticCollector<JavaFileObject> compileBatch(Map<URI, Optional<String>> files) { - return compileBatch(files, (task, tree) -> {}); - } - - public DiagnosticCollector<JavaFileObject> compileBatch( - Map<URI, Optional<String>> files, BiConsumer<JavacTask, CompilationUnitTree> listener) { - return doCompile(files, listener); - } - - private static final Logger LOG = Logger.getLogger("main"); - /** Where this javac looks for library .class files */ - public final Set<Path> classPath; - /** Where this javac looks for .java source files */ - public final Set<Path> sourcePath; - - /** - * Javac tool creates a new Context every time we do createTask(...), so maintaining a reference - * to it doesn't really do anything - */ - private final JavacTool javac = JavacTool.create(); - - /* - * JavacFileManager caches classpath internally, so both fileManager and incrementalFileManager will have the same classPath - */ - - /** Direct file manager we use to obtain reference to file we are re-compiling */ - private final JavacFileManager fileManager = - javac.getStandardFileManager(this::onError, null, Charset.defaultCharset()); - - /** File manager that hides .java files that have up-to-date sources */ - private final JavaFileManager incrementalFileManager = new IncrementalFileManager(fileManager); - - /** - * javac isn't friendly to swapping out the error-reporting DiagnosticListener, so we install - * this intermediate DiagnosticListener, which forwards to errorsDelegate - */ - private void onError(Diagnostic<? extends JavaFileObject> diagnostic) { - onErrorDelegate.report(diagnostic); - } - - /** - * Error reporting initially goes nowhere. We will replace this with a function that collects - * errors so we can report all the errors associated with a file apply once. - */ - private DiagnosticListener<JavaFileObject> onErrorDelegate = diagnostic -> {}; - - /** Forward javac logging to file */ - private Writer logDelegate = createLogDelegate(); - - private static Writer createLogDelegate() { - try { - Path output = Files.createTempFile("javac", ".log"); - BufferedWriter out = Files.newBufferedWriter(output); - - LOG.info("Forwarding javac log to " + output); - - return out; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public final ClassPathIndex classPathIndex; - - private JavacHolder(Set<Path> sourcePath, Set<Path> classPath) { - this.sourcePath = Collections.unmodifiableSet(sourcePath); - this.classPath = Collections.unmodifiableSet(classPath); - this.classPathIndex = new ClassPathIndex(classPath); - } - - static List<String> options(Set<Path> sourcePath, Set<Path> classPath) { - return ImmutableList.of( - "-classpath", - Joiner.on(File.pathSeparator).join(classPath), - "-sourcepath", - Joiner.on(File.pathSeparator).join(sourcePath), - "-verbose", - // You would think we could do -Xlint:all, - // but some lints trigger fatal errors in the presence of parse errors - "-Xlint:cast", - "-Xlint:deprecation", - "-Xlint:empty", - "-Xlint:fallthrough", - "-Xlint:finally", - "-Xlint:path", - "-Xlint:unchecked", - "-Xlint:varargs", - "-Xlint:static"); - } - - private static class Profile { - Instant started = Instant.now(); - Optional<Instant> finished = Optional.empty(); - - Duration elapsed() { - return Duration.between(started, finished.orElse(started)); - } - } - - private JavacTask createTask(Collection<JavaFileObject> files, boolean incremental) { - JavacTask result = - javac.getTask( - logDelegate, - incrementalFileManager, - this::onError, - options(sourcePath, classPath), - null, - files); - JavacTaskImpl impl = (JavacTaskImpl) result; - - // Better stack traces inside javac - Options options = Options.instance(impl.getContext()); - - options.put("dev", ""); - - // Skip annotation processing - JavaCompiler compiler = JavaCompiler.instance(impl.getContext()); - - compiler.skipAnnotationProcessing = true; - - return result; - } - - /** Ensure output directory exists */ - private void ensureOutputDirectory(Path dir) { - if (!Files.exists(dir)) { - try { - Files.createDirectories(dir); - } catch (IOException e) { - throw ShowMessageException.error("Error created output directory " + dir, null); - } - } else if (!Files.isDirectory(dir)) - throw ShowMessageException.error( - "Output directory " + dir + " is not a directory", null); - } - - // TODO this should return Optional.empty() file URI is not file: and text is empty - private JavaFileObject findFile(URI file, Optional<String> text) { - return text.map(content -> (JavaFileObject) new StringFileObject(content, file)) - .orElseGet(() -> fileManager.getRegularFile(Paths.get(file).toFile())); - } - - private DiagnosticCollector<JavaFileObject> startCollectingErrors() { - DiagnosticCollector<JavaFileObject> errors = new DiagnosticCollector<>(); - - onErrorDelegate = - error -> { - if (error.getStartPosition() != Diagnostic.NOPOS) errors.report(error); - else LOG.warning("Skipped " + error.getMessage(null)); - }; - return errors; - } - - private void stopCollectingErrors() { - onErrorDelegate = error -> {}; - } - - private DiagnosticCollector<JavaFileObject> doCompile( - Map<URI, Optional<String>> files, BiConsumer<JavacTask, CompilationUnitTree> forEach) { - // TODO remove all URIs from fileManager - - List<JavaFileObject> objects = - files.entrySet() - .stream() - .map(e -> findFile(e.getKey(), e.getValue())) - .collect(Collectors.toList()); - - JavacTask task = createTask(objects, false); - - try { - DiagnosticCollector<JavaFileObject> errors = startCollectingErrors(); - Iterable<? extends CompilationUnitTree> parse = task.parse(); - - // TODO minimize memory use during this process - // Instead of doing parse-all / compileFileObjects-all, - // queue all files, then do parse / compileFileObjects on each - // If invoked correctly, javac should avoid reparsing the same file twice - // Then, use the same mechanism as the desugar / generate phases to remove method bodies, - // to reclaim memory as we go - - try { - Iterable<? extends Element> analyze = task.analyze(); - } catch (AssertionError e) { - if (!catchJavacError(e)) throw e; - } - - parse.forEach(tree -> forEach.accept(task, tree)); - - return errors; - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - stopCollectingErrors(); - } - } - - private boolean catchJavacError(AssertionError e) { - if (e.getStackTrace().length > 0 - && e.getStackTrace()[0].getClassName().startsWith("com.sun.tools.javac")) { - LOG.log(Level.WARNING, "Failed analyze phase", e); - - return true; - } else return false; - } -} diff --git a/src/main/java/org/javacs/Javadocs.java b/src/main/java/org/javacs/Javadocs.java deleted file mode 100644 index 959d411..0000000 --- a/src/main/java/org/javacs/Javadocs.java +++ /dev/null @@ -1,395 +0,0 @@ -package org.javacs; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.gson.JsonPrimitive; -import com.overzealous.remark.Options; -import com.overzealous.remark.Remark; -import com.sun.javadoc.*; -import com.sun.source.util.JavacTask; -import com.sun.tools.javac.api.JavacTool; -import com.sun.tools.javac.file.JavacFileManager; -import com.sun.tools.javadoc.api.JavadocTool; -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.text.BreakIterator; -import java.time.Instant; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.tools.DocumentationTool; -import javax.tools.JavaFileObject; -import javax.tools.StandardLocation; -import org.eclipse.lsp4j.CompletionItem; - -public class Javadocs { - - /** Cache for performance reasons */ - private final JavacFileManager actualFileManager; - - /** Empty file manager we pass to javadoc to prevent it from roaming all over the place */ - private final JavacFileManager emptyFileManager = - JavacTool.create().getStandardFileManager(JavaLanguageServer::onDiagnostic, null, null); - - /** All the classes we have indexed so far */ - private final Map<String, IndexedDoc> topLevelClasses = new ConcurrentHashMap<>(); - - private static class IndexedDoc { - final RootDoc doc; - final Instant updated; - - IndexedDoc(RootDoc doc, Instant updated) { - this.doc = doc; - this.updated = updated; - } - } - - private final JavacTask task; - - private final Function<URI, Optional<String>> activeContent; - - Javadocs( - Set<Path> sourcePath, - Set<Path> docPath, - Function<URI, Optional<String>> activeContent) { - this.actualFileManager = createFileManager(allSourcePaths(sourcePath, docPath)); - this.task = - JavacTool.create() - .getTask( - null, - emptyFileManager, - JavaLanguageServer::onDiagnostic, - null, - null, - null); - this.activeContent = activeContent; - } - - private static Set<File> allSourcePaths(Set<Path>... userSourcePath) { - Set<File> allSourcePaths = new HashSet<>(); - - // Add userSourcePath - for (Set<Path> eachPath : userSourcePath) { - for (Path each : eachPath) allSourcePaths.add(each.toFile()); - } - - // Add src.zip from JDK - findSrcZip().ifPresent(allSourcePaths::add); - - return allSourcePaths; - } - - private static JavacFileManager createFileManager(Set<File> allSourcePaths) { - JavacFileManager actualFileManager = - JavacTool.create() - .getStandardFileManager(JavaLanguageServer::onDiagnostic, null, null); - - try { - actualFileManager.setLocation(StandardLocation.SOURCE_PATH, allSourcePaths); - } catch (IOException e) { - throw new RuntimeException(e); - } - - return actualFileManager; - } - - /** Convert Javadoc HTML to Markdown */ - static String htmlToMarkdown(String commentText) { - Options options = new Options(); - - options.tables = Options.Tables.CONVERT_TO_CODE_BLOCK; - options.hardwraps = true; - options.inlineLinks = true; - options.autoLinks = true; - options.reverseHtmlSmartPunctuation = true; - - return new Remark(options).convertFragment(commentText); - } - - /** Get docstring for method, using inherited method if necessary */ - static Optional<String> commentText(MethodDoc doc) { - // TODO search interfaces as well - - while (doc != null && Strings.isNullOrEmpty(doc.commentText())) - doc = doc.overriddenMethod(); - - if (doc == null || Strings.isNullOrEmpty(doc.commentText())) return Optional.empty(); - else return Optional.of(doc.commentText()); - } - - Optional<? extends ProgramElementDoc> doc(Element el) { - if (el instanceof ExecutableElement) { - ExecutableElement method = (ExecutableElement) el; - - return methodDoc(methodKey(method)); - } else if (el instanceof TypeElement) { - TypeElement type = (TypeElement) el; - - return classDoc(type.getQualifiedName().toString()); - } else return Optional.empty(); - } - - String methodKey(ExecutableElement method) { - TypeElement classElement = (TypeElement) method.getEnclosingElement(); - - return String.format( - "%s#%s(%s)", - classElement.getQualifiedName(), - method.getSimpleName(), - paramsKey(method.getParameters())); - } - - private String paramsKey(List<? extends VariableElement> params) { - return params.stream().map(this::paramType).collect(Collectors.joining(",")); - } - - Optional<MethodDoc> methodDoc(String methodKey) { - String className = methodKey.substring(0, methodKey.indexOf('#')); - - return classDoc(className).flatMap(classDoc -> doMethodDoc(classDoc, methodKey)); - } - - private Optional<MethodDoc> doMethodDoc(ClassDoc classDoc, String methodKey) { - for (MethodDoc each : classDoc.methods(false)) { - if (methodMatches(methodKey, each)) return Optional.of(each); - } - - return Optional.empty(); - } - - private boolean methodMatches(String methodKey, MethodDoc doc) { - String docKey = - String.format( - "%s#%s(%s)", - doc.containingClass().qualifiedName(), - doc.name(), - paramSignature(doc.parameters())); - - return docKey.equals(methodKey); - } - - private String paramSignature(Parameter[] params) { - return Arrays.stream(params).map(this::docType).collect(Collectors.joining(",")); - } - - private String paramType(VariableElement param) { - return task.getTypes().erasure(param.asType()).toString(); - } - - private String docType(Parameter doc) { - return doc.type().qualifiedTypeName() + doc.type().dimension(); - } - - Optional<ConstructorDoc> constructorDoc(String methodKey) { - String className = methodKey.substring(0, methodKey.indexOf('#')); - - return classDoc(className).flatMap(classDoc -> doConstructorDoc(classDoc, methodKey)); - } - - private Optional<ConstructorDoc> doConstructorDoc(ClassDoc classDoc, String methodKey) { - for (ConstructorDoc each : classDoc.constructors(false)) { - if (constructorMatches(methodKey, each)) return Optional.of(each); - } - - return Optional.empty(); - } - - private boolean constructorMatches(String methodKey, ConstructorDoc doc) { - String docKey = - String.format( - "%s#<init>(%s)", - doc.containingClass().qualifiedName(), paramSignature(doc.parameters())); - - return docKey.equals(methodKey); - } - - Optional<ClassDoc> classDoc(String className) { - RootDoc index = index(className); - - return Optional.ofNullable(index.classNamed(className)); - } - - /** Get or compute the javadoc for `className` */ - RootDoc index(String className) { - if (needsUpdate(className)) topLevelClasses.put(className, doIndex(className)); - - return topLevelClasses.get(className).doc; - } - - private boolean needsUpdate(String className) { - if (!topLevelClasses.containsKey(className)) return true; - - IndexedDoc indexed = topLevelClasses.get(className); - - try { - JavaFileObject fromDisk = - actualFileManager.getJavaFileForInput( - StandardLocation.SOURCE_PATH, className, JavaFileObject.Kind.SOURCE); - - if (fromDisk == null) return true; - - Instant modified = Instant.ofEpochMilli(fromDisk.getLastModified()); - - return indexed.updated.isBefore(modified); - } catch (IOException e) { - throw new RuntimeException(); - } - } - - /** Read all the Javadoc for `className` */ - private IndexedDoc doIndex(String className) { - try { - JavaFileObject fromDisk = - actualFileManager.getJavaFileForInput( - StandardLocation.SOURCE_PATH, className, JavaFileObject.Kind.SOURCE); - - if (fromDisk == null) { - LOG.warning("No source file for " + className); - - return new IndexedDoc(EmptyRootDoc.INSTANCE, Instant.EPOCH); - } - JavaFileObject source = activeFile(fromDisk); - - LOG.info("Found " + source.toUri() + " for " + className); - - DocumentationTool.DocumentationTask task = - new JavadocTool() - .getTask( - null, - emptyFileManager, - JavaLanguageServer::onDiagnostic, - Javadocs.class, - null, - ImmutableList.of(source)); - - task.call(); - - return new IndexedDoc( - getSneakyReturn().orElse(EmptyRootDoc.INSTANCE), - Instant.ofEpochMilli(fromDisk.getLastModified())); - } catch (IOException e) { - throw new RuntimeException(); - } - } - - private JavaFileObject activeFile(JavaFileObject file) { - return activeContent - .apply(file.toUri()) - .map(content -> (JavaFileObject) new StringFileObject(content, file.toUri())) - .orElse(file); - } - - private Optional<RootDoc> getSneakyReturn() { - RootDoc result = sneakyReturn.get(); - sneakyReturn.remove(); - - if (result == null) { - LOG.warning("index did not return a RootDoc"); - - return Optional.empty(); - } else return Optional.of(result); - } - - /** start(RootDoc) uses this to return its result to doIndex(...) */ - private static ThreadLocal<RootDoc> sneakyReturn = new ThreadLocal<>(); - - /** - * Called by the javadoc tool - * - * <p>{@link Doclet} - */ - public static boolean start(RootDoc root) { - sneakyReturn.set(root); - - return true; - } - - /** Find the copy of src.zip that comes with the system-installed JDK */ - static Optional<File> findSrcZip() { - Path path = Paths.get(System.getProperty("java.home")); - - if (path.endsWith("jre")) path = path.getParent(); - - path = path.resolve("src.zip"); - - while (Files.isSymbolicLink(path)) { - try { - path = path.getParent().resolve(Files.readSymbolicLink(path)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - if (Files.exists(path)) return Optional.of(path.toFile()); - else { - LOG.severe(String.format("Could not find %s", path)); - - return Optional.empty(); - } - } - - public void resolveCompletionItem(CompletionItem unresolved) { - if (unresolved.getData() == null || unresolved.getDocumentation() != null) return; - - JsonPrimitive keyJson = (JsonPrimitive) unresolved.getData(); - String key = keyJson.getAsString(); - - LOG.info("Resolve javadoc for " + key); - - // my.package.MyClass#<init>() - if (key.contains("<init>")) { - constructorDoc(key) - .ifPresent( - doc -> - unresolved.setDocumentation( - Javadocs.htmlToMarkdown(doc.commentText()))); - } - // my.package.MyClass#myMethod() - else if (key.contains("#")) { - methodDoc(key) - .ifPresent( - doc -> - unresolved.setDocumentation( - Javadocs.htmlToMarkdown(doc.commentText()))); - } - // my.package.MyClass - else { - classDoc(key) - .ifPresent( - doc -> - unresolved.setDocumentation( - Javadocs.htmlToMarkdown(doc.commentText()))); - } - } - - /** - * Get the first sentence of a doc-comment. - * - * <p>In general, VS Code does a good job of only displaying the beginning of a doc-comment - * where appropriate. But if VS Code is displaying too much and you want to only show the first - * sentence, use this. - */ - public static String firstSentence(String doc) { - BreakIterator breaks = BreakIterator.getSentenceInstance(); - - breaks.setText(doc.replace('\n', ' ')); - - int start = breaks.first(), end = breaks.next(); - - if (start == -1 || end == -1) return doc; - - return doc.substring(start, end).trim(); - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/LangTools.java b/src/main/java/org/javacs/LangTools.java deleted file mode 100644 index 1001189..0000000 --- a/src/main/java/org/javacs/LangTools.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.javacs; - -import java.util.Objects; - -/** - * Loads langtools off the classpath, which contains our custom build of javac, instead of the - * platform. In Java 8, this was not necessary because tools.jar isn't loaded by default, so java - * only sees our custom build of javac. But in Java 9, tools.jar is in the main platform, so we need - * to avoid loading it through a custom classloader. - */ -class LangTools { - public static final String[] LANGTOOLS_PACKAGES = { - "com.sun.source", - "com.sun.tools", - "javax.annotation.processing", - "javax.lang.model", - "javax.tools", - "org.javacs" - }; - - public static ClassLoader createLangToolsClassLoader() { - String classPath = System.getProperty("java.class.path"); - Objects.requireNonNull(classPath, "java.class.path was null"); - ClassLoader parent = LangTools.class.getClassLoader(); - return ChildFirstClassLoader.fromClassPath(classPath, LANGTOOLS_PACKAGES, parent); - } -} diff --git a/src/main/java/org/javacs/LegacyConfig.java b/src/main/java/org/javacs/LegacyConfig.java deleted file mode 100644 index 542d365..0000000 --- a/src/main/java/org/javacs/LegacyConfig.java +++ /dev/null @@ -1,244 +0,0 @@ -package org.javacs; - -import static org.javacs.Main.JSON; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableSet; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; -import java.util.function.Function; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; -import org.eclipse.lsp4j.*; -import org.w3c.dom.Document; -import org.xml.sax.SAXException; - -class LegacyConfig { - private final Path workspaceRoot; - - LegacyConfig(Path workspaceRoot) { - this.workspaceRoot = workspaceRoot; - } - - Optional<JavacConfig> readJavaConfig(Path dir) { - Function<JavaConfigJson, JavacConfig> parseJavaConfigJson = - json -> { - Set<Path> classPath = readClassPath(dir, json.classPath, json.classPathFile), - docPath = readClassPath(dir, json.docPath, json.docPathFile), - sourcePath = - json.sourcePath - .stream() - .map(dir::resolve) - .collect(Collectors.toSet()); - Path outputDirectory = dir.resolve(json.outputDirectory); - - return new JavacConfig( - classPath, Collections.singleton(outputDirectory), docPath); - }; - if (Files.exists(dir.resolve("javaconfig.json"))) { - return readJavaConfigJson(dir.resolve("javaconfig.json")).map(parseJavaConfigJson); - } else return Optional.empty(); - } - - private Set<Path> readClassPath( - Path dir, Set<Path> jsonClassPath, Optional<Path> jsonClassPathFile) { - Set<Path> classPath = new HashSet<>(); - - jsonClassPathFile.ifPresent( - classPathFile -> { - Path classPathFilePath = dir.resolve(classPathFile); - Set<Path> paths = readClassPathFile(classPathFilePath); - - classPath.addAll(paths); - }); - - jsonClassPath.forEach(entry -> classPath.add(dir.resolve(entry))); - - return classPath; - } - - private JavacConfig readPomXml(Path dir, boolean testScope) { - Path originalPom = dir.resolve("pom.xml"); - Path effectivePom = generateEffectivePom(originalPom); - - // Invoke maven to get classpath - Set<Path> classPath = buildClassPath(originalPom, testScope, false); - - // Get source directory from pom.xml - Set<Path> sourcePath = sourceDirectories(effectivePom, testScope); - - // Get doc path from pom.xml - Set<Path> docPath = buildClassPath(originalPom, testScope, true); - - // Use maven output directory so incremental compilation uses maven-generated .class files - Set<Path> workspaceClassPath = - ImmutableSet.of( - Paths.get("target/test-classes").toAbsolutePath(), - Paths.get("target/classes").toAbsolutePath()); - - JavacConfig config = new JavacConfig(classPath, workspaceClassPath, docPath); - - LOG.info("Inferred from " + originalPom + ":"); - LOG.info("\tsourcePath: " + Joiner.on(" ").join(sourcePath)); - LOG.info("\tclassPath: " + Joiner.on(" ").join(classPath)); - LOG.info("\tworkspaceClassPath: " + Joiner.on(" ").join(workspaceClassPath)); - LOG.info("\tdocPath: " + Joiner.on(" ").join(docPath)); - - return config; - } - - private static Path generateEffectivePom(Path pomXml) { - try { - Objects.requireNonNull(pomXml, "pom.xml path is null"); - - Path effectivePom = Files.createTempFile("effective-pom", ".xml"); - - LOG.info(String.format("Emit effective pom for %s to %s", pomXml, effectivePom)); - - String cmd = - String.format( - "%s help:effective-pom -Doutput=%s", - InferConfig.getMvnCommand(), effectivePom); - File workingDirectory = pomXml.toAbsolutePath().getParent().toFile(); - int result = Runtime.getRuntime().exec(cmd, null, workingDirectory).waitFor(); - - if (result != 0) throw new RuntimeException("`" + cmd + "` returned " + result); - - return effectivePom; - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - // TODO For sourceJars = true: - // use mvn dependency:list to list the dependencies quickly, - // then fetch each one individually using mvn dependency:build-classpath -DincludeGroupIds=? -DincludeArtifactIds=? ... - private static Set<Path> buildClassPath(Path pomXml, boolean testScope, boolean sourceJars) { - try { - Objects.requireNonNull(pomXml, "pom.xml path is null"); - - // Tell maven to output classpath to a temporary file - // TODO if pom.xml already specifies outputFile, use that location - Path classPathTxt = - Files.createTempFile(sourceJars ? "sourcepath" : "classpath", ".txt"); - - LOG.info( - String.format( - "Emit %s to %s", sourceJars ? "docPath" : "classpath", classPathTxt)); - - String cmd = - String.format( - "%s dependency:build-classpath -DincludeScope=%s -Dmdep.outputFile=%s %s", - InferConfig.getMvnCommand(), - testScope ? "test" : "compile", - classPathTxt, - sourceJars ? "-Dclassifier=sources" : ""); - File workingDirectory = pomXml.toAbsolutePath().getParent().toFile(); - int result = Runtime.getRuntime().exec(cmd, null, workingDirectory).waitFor(); - - if (result != 0) throw new RuntimeException("`" + cmd + "` returned " + result); - - Set<Path> found = readClassPathFile(classPathTxt); - - LOG.info("Read " + Joiner.on(" ").join(found) + " from " + classPathTxt); - - return found; - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - private static Set<Path> sourceDirectories(Path pomXml, boolean testScope) { - return testScope - ? ImmutableSet.of( - onlySourceDirectories(pomXml, true), onlySourceDirectories(pomXml, false)) - : ImmutableSet.of(onlySourceDirectories(pomXml, false)); - } - - private static Path onlySourceDirectories(Path pomXml, boolean testScope) { - String defaultSourceDir = testScope ? "src/test/java" : "src/main/java"; - String xPath = - testScope ? "/project/build/testSourceDirectory" : "/project/build/sourceDirectory"; - Document doc = parsePomXml(pomXml); - - try { - String sourceDir = XPathFactory.newInstance().newXPath().compile(xPath).evaluate(doc); - - if (sourceDir == null || sourceDir.isEmpty()) { - LOG.info("Use default source directory " + defaultSourceDir); - - sourceDir = defaultSourceDir; - } else LOG.info("Use source directory from pom.xml " + sourceDir); - - return pomXml.resolveSibling(sourceDir).toAbsolutePath(); - } catch (XPathExpressionException e) { - throw new RuntimeException(e); - } - } - - private static Document parsePomXml(Path pomXml) { - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - - return builder.parse(pomXml.toFile()); - } catch (IOException | ParserConfigurationException | SAXException e) { - throw new RuntimeException(e); - } - } - - private Optional<JavaConfigJson> readJavaConfigJson(Path configFile) { - try { - JsonNode json = JSON.readValue(configFile.toFile(), JsonNode.class); - - if (json.isArray()) - return JSON.convertValue(json, new TypeReference<List<JavaConfigJson>>() {}); - else { - JavaConfigJson one = JSON.convertValue(json, JavaConfigJson.class); - - return Optional.of(one); - } - } catch (IOException e) { - MessageParams message = new MessageParams(); - - message.setMessage("Error reading " + configFile); - message.setType(MessageType.Error); - - throw new ShowMessageException(message, e); - } - } - - private static Set<Path> readClassPathFile(Path classPathFilePath) { - try { - InputStream in = Files.newInputStream(classPathFilePath); - String text = - new BufferedReader(new InputStreamReader(in)) - .lines() - .collect(Collectors.joining()); - Path dir = classPathFilePath.getParent(); - - return Arrays.stream(text.split(File.pathSeparator)) - .map(dir::resolve) - .collect(Collectors.toSet()); - } catch (IOException e) { - MessageParams message = new MessageParams(); - - message.setMessage("Error reading " + classPathFilePath); - message.setType(MessageType.Error); - - throw new ShowMessageException(message, e); - } - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/Lib.java b/src/main/java/org/javacs/Lib.java new file mode 100644 index 0000000..924f98c --- /dev/null +++ b/src/main/java/org/javacs/Lib.java @@ -0,0 +1,15 @@ +package org.javacs; + +import java.nio.file.*; + +class Lib { + static Path installRoot() { + var root = Paths.get(".").toAbsolutePath(); + var p = root; + while (p != null && !Files.exists(p.resolve("javaLsFlag.txt"))) p = p.getParent(); + if (p == null) throw new RuntimeException("Couldn't find javaLsFlag.txt in any parent of " + root); + return p; + } + + static final Path SRC_ZIP = installRoot().resolve("lib/src.zip"); +} diff --git a/src/main/java/org/javacs/Lints.java b/src/main/java/org/javacs/Lints.java deleted file mode 100644 index 3fc2c4a..0000000 --- a/src/main/java/org/javacs/Lints.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.javacs; - -import com.sun.tools.javac.api.ClientCodeWrapper; -import com.sun.tools.javac.util.DiagnosticSource; -import com.sun.tools.javac.util.JCDiagnostic; -import java.util.Locale; -import java.util.Optional; -import java.util.logging.Logger; -import javax.tools.JavaFileObject; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.DiagnosticSeverity; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; - -class Lints { - - static Optional<Diagnostic> convert(javax.tools.Diagnostic<? extends JavaFileObject> error) { - if (error.getStartPosition() != javax.tools.Diagnostic.NOPOS) { - Range range = position(error); - Diagnostic diagnostic = new Diagnostic(); - DiagnosticSeverity severity = severity(error.getKind()); - - diagnostic.setSeverity(severity); - diagnostic.setRange(range); - diagnostic.setCode(error.getCode()); - diagnostic.setMessage(error.getMessage(null)); - - return Optional.of(diagnostic); - } else { - LOG.warning("Skipped " + error.getMessage(Locale.getDefault())); - - return Optional.empty(); - } - } - - private static DiagnosticSeverity severity(javax.tools.Diagnostic.Kind kind) { - switch (kind) { - case ERROR: - return DiagnosticSeverity.Error; - case WARNING: - case MANDATORY_WARNING: - return DiagnosticSeverity.Warning; - case NOTE: - case OTHER: - default: - return DiagnosticSeverity.Information; - } - } - - private static Range position(javax.tools.Diagnostic<? extends JavaFileObject> error) { - if (error instanceof ClientCodeWrapper.DiagnosticSourceUnwrapper) - error = ((ClientCodeWrapper.DiagnosticSourceUnwrapper) error).d; - - JCDiagnostic diagnostic = (JCDiagnostic) error; - DiagnosticSource source = diagnostic.getDiagnosticSource(); - long start = error.getStartPosition(), end = error.getEndPosition(); - - if (end == start) end = start + 1; - - return new Range( - new Position( - source.getLineNumber((int) start) - 1, - source.getColumnNumber((int) start, true) - 1), - new Position( - source.getLineNumber((int) end) - 1, - source.getColumnNumber((int) end, true) - 1)); - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/LogFormat.java b/src/main/java/org/javacs/LogFormat.java index adf988c..7bffbbd 100644 --- a/src/main/java/org/javacs/LogFormat.java +++ b/src/main/java/org/javacs/LogFormat.java @@ -22,23 +22,17 @@ class LogFormat extends Formatter { } else { source = record.getLoggerName(); } - String message = formatMessage(record); - String throwable = ""; + var message = formatMessage(record); + var throwable = ""; if (record.getThrown() != null) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); + var sw = new StringWriter(); + var pw = new PrintWriter(sw); pw.println(); record.getThrown().printStackTrace(pw); pw.close(); throwable = sw.toString(); } return String.format( - format, - dat, - source, - record.getLoggerName(), - record.getLevel().getLocalizedName(), - message, - throwable); + format, dat, source, record.getLoggerName(), record.getLevel().getLocalizedName(), message, throwable); } } diff --git a/src/main/java/org/javacs/Main.java b/src/main/java/org/javacs/Main.java index 256ba6f..14a8f61 100644 --- a/src/main/java/org/javacs/Main.java +++ b/src/main/java/org/javacs/Main.java @@ -3,23 +3,22 @@ package org.javacs; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JSR310Module; -import com.sun.tools.javac.api.JavacTool; import java.io.IOException; -import java.lang.reflect.Method; -import java.net.Socket; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Objects; -import java.util.logging.Handler; +import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; -import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.launch.LSPLauncher; -import org.eclipse.lsp4j.services.LanguageClient; public class Main { public static final ObjectMapper JSON = @@ -34,20 +33,19 @@ public class Main { private static final Logger LOG = Logger.getLogger("main"); public static void setRootFormat() { - Logger root = Logger.getLogger(""); + var root = Logger.getLogger(""); - for (Handler h : root.getHandlers()) h.setFormatter(new LogFormat()); + for (var h : root.getHandlers()) h.setFormatter(new LogFormat()); } private static SimpleModule pathAsJson() { - SimpleModule m = new SimpleModule(); + var m = new SimpleModule(); m.addSerializer( Path.class, new JsonSerializer<Path>() { @Override - public void serialize( - Path path, JsonGenerator gen, SerializerProvider serializerProvider) + public void serialize(Path path, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { gen.writeString(path.toString()); } @@ -57,8 +55,7 @@ public class Main { Path.class, new JsonDeserializer<Path>() { @Override - public Path deserialize( - JsonParser parse, DeserializationContext deserializationContext) + public Path deserialize(JsonParser parse, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { return Paths.get(parse.getText()); } @@ -69,27 +66,15 @@ public class Main { public static void main(String[] args) { try { - ClassLoader langTools = LangTools.createLangToolsClassLoader(); - Class<?> main = Class.forName("org.javacs.Main", true, langTools); - Method run = main.getMethod("run"); - run.invoke(null); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Failed", e); - } - } + // Logger.getLogger("").addHandler(new FileHandler("javacs.%u.log", false)); + setRootFormat(); - public static ClassLoader checkJavacClassLoader() { - return JavacTool.create().getClass().getClassLoader(); - } + var server = new JavaLanguageServer(); + var threads = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "client")); + var launcher = + LSPLauncher.createServerLauncher( + server, System.in, System.out, threads, messageConsumer -> messageConsumer); - public static void run() { - assert checkJavacClassLoader() instanceof ChildFirstClassLoader; - setRootFormat(); - - try { - JavaLanguageServer server = new JavaLanguageServer(); - Launcher<LanguageClient> launcher = LSPLauncher.createServerLauncher(server, System.in, System.out); - server.installClient(launcher.getRemoteProxy()); launcher.startListening(); LOG.info(String.format("java.version is %s", System.getProperty("java.version"))); diff --git a/src/main/java/org/javacs/MethodInvocation.java b/src/main/java/org/javacs/MethodInvocation.java new file mode 100644 index 0000000..d5c3a4e --- /dev/null +++ b/src/main/java/org/javacs/MethodInvocation.java @@ -0,0 +1,26 @@ +package org.javacs; + +import com.sun.source.tree.ExpressionTree; +import java.util.List; +import java.util.Optional; +import javax.lang.model.element.ExecutableElement; + +public class MethodInvocation { + /** MethodInvocationTree or NewClassTree */ + public final ExpressionTree tree; + + public final Optional<ExecutableElement> activeMethod; + public final int activeParameter; + public final List<ExecutableElement> overloads; + + public MethodInvocation( + ExpressionTree tree, + Optional<ExecutableElement> activeMethod, + int activeParameter, + List<ExecutableElement> overloads) { + this.tree = tree; + this.activeMethod = activeMethod; + this.activeParameter = activeParameter; + this.overloads = overloads; + } +} diff --git a/src/main/java/org/javacs/Parser.java b/src/main/java/org/javacs/Parser.java new file mode 100644 index 0000000..6bf47a8 --- /dev/null +++ b/src/main/java/org/javacs/Parser.java @@ -0,0 +1,382 @@ +package org.javacs; + +import com.google.common.base.Joiner; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import com.sun.source.util.Trees; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Consumer; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; + +class Parser { + + private static final JavaCompiler compiler = ServiceLoader.load(JavaCompiler.class).iterator().next(); + private static final StandardJavaFileManager fileManager = + compiler.getStandardFileManager(__ -> {}, null, Charset.defaultCharset()); + + static JavacTask parseTask(JavaFileObject file) { + return (JavacTask) + compiler.getTask( + null, + fileManager, + err -> LOG.warning(err.getMessage(Locale.getDefault())), + Collections.emptyList(), + null, + Collections.singletonList(file)); + } + + static JavacTask parseTask(Path source) { + JavaFileObject file = + fileManager.getJavaFileObjectsFromFiles(Collections.singleton(source.toFile())).iterator().next(); + return parseTask(file); + } + + static CompilationUnitTree parse(JavaFileObject file) { + try { + return parseTask(file).parse().iterator().next(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static CompilationUnitTree parse(Path source) { + try { + return parseTask(source).parse().iterator().next(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Check if `candidate` contains all the characters of `find`, in-order, case-insensitive Matches can be + * discontinuous if the letters of `find` match the first letters of words in `candidate` For example, fb matches + * FooBar, but it doesn't match Foobar (exposed for testing) + */ + static boolean matchesTitleCase(CharSequence candidate, String find) { + int i = 0; + for (char f : find.toCharArray()) { + // If we have reached the end of candidate without matching all of find, fail + if (i >= candidate.length()) return false; + // If the next character in candidate matches, advance i + else if (Character.toLowerCase(f) == Character.toLowerCase(candidate.charAt(i))) i++; + else { + // Find the start of the next word + while (i < candidate.length()) { + char c = candidate.charAt(i); + boolean isStartOfWord = Character.isUpperCase(c); + boolean isMatch = Character.toLowerCase(f) == Character.toLowerCase(c); + if (isStartOfWord && isMatch) { + i++; + break; + } else i++; + } + if (i == candidate.length()) return false; + } + } + return true; + } + + private static final Pattern WORD = Pattern.compile("\\b\\w+\\b"); + + static boolean containsWordMatching(Path java, String query) { + try { + for (var line : Files.readAllLines(java)) { + var pattern = WORD.matcher(line); + while (pattern.find()) { + var word = pattern.group(0); + if (matchesTitleCase(word, query)) return true; + } + } + return false; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Stream<TreePath> findSymbolsMatching(CompilationUnitTree parse, String query) { + class Find extends TreePathScanner<Void, Void> { + List<TreePath> found = new ArrayList<>(); + + void accept(TreePath path) { + var node = path.getLeaf(); + if (node instanceof ClassTree) { + var c = (ClassTree) node; + if (matchesTitleCase(c.getSimpleName(), query)) found.add(path); + } else if (node instanceof MethodTree) { + var m = (MethodTree) node; + if (matchesTitleCase(m.getName(), query)) found.add(path); + } else if (node instanceof VariableTree) { + var v = (VariableTree) node; + if (matchesTitleCase(v.getName(), query)) found.add(path); + } + } + + @Override + public Void visitClass(ClassTree node, Void nothing) { + super.visitClass(node, nothing); + accept(getCurrentPath()); + for (var t : node.getMembers()) { + var child = new TreePath(getCurrentPath(), t); + accept(child); + } + return null; + } + + List<TreePath> run() { + scan(parse, null); + return found; + } + } + return new Find().run().stream(); + } + + /** Search `dir` for .java files containing important symbols matching `query` */ + static Stream<TreePath> findSymbols(Path dir, String query) { + var match = FileSystems.getDefault().getPathMatcher("glob:*.java"); + + try { + return Files.walk(dir) + .filter(java -> match.matches(java.getFileName())) + .filter(java -> containsWordMatching(java, query)) + .flatMap(java -> findSymbolsMatching(parse(java), query)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static List<TreePath> documentSymbols(Path java, String content) { + var parse = parse(new StringFileObject(content, java.toUri())); + return findSymbolsMatching(parse, "").collect(Collectors.toList()); + } + + static Location location(TreePath p) { + // This is very questionable, will this Trees object actually work? + var task = parseTask(p.getCompilationUnit().getSourceFile()); + var trees = Trees.instance(task); + var pos = trees.getSourcePositions(); + var cu = p.getCompilationUnit(); + var lines = cu.getLineMap(); + long start = pos.getStartPosition(cu, p.getLeaf()), end = pos.getEndPosition(cu, p.getLeaf()); + int startLine = (int) lines.getLineNumber(start) - 1, startCol = (int) lines.getColumnNumber(start) - 1; + int endLine = (int) lines.getLineNumber(end) - 1, endCol = (int) lines.getColumnNumber(end) - 1; + var dUri = cu.getSourceFile().toUri(); + return new Location( + dUri.toString(), new Range(new Position(startLine, startCol), new Position(endLine, endCol))); + } + + private static SymbolKind asSymbolKind(Tree.Kind k) { + switch (k) { + case ANNOTATION_TYPE: + case CLASS: + return SymbolKind.Class; + case ENUM: + return SymbolKind.Enum; + case INTERFACE: + return SymbolKind.Interface; + case METHOD: + return SymbolKind.Method; + case TYPE_PARAMETER: + return SymbolKind.TypeParameter; + case VARIABLE: + // This method is used for symbol-search functionality, + // where we only return fields, not local variables + return SymbolKind.Field; + default: + return null; + } + } + + private static String containerName(TreePath path) { + var parent = path.getParentPath(); + while (parent != null) { + var t = parent.getLeaf(); + if (t instanceof ClassTree) { + var c = (ClassTree) t; + return c.getSimpleName().toString(); + } else if (t instanceof CompilationUnitTree) { + var c = (CompilationUnitTree) t; + return Objects.toString(c.getPackageName(), ""); + } else { + parent = parent.getParentPath(); + } + } + return null; + } + + private static String symbolName(Tree t) { + if (t instanceof ClassTree) { + var c = (ClassTree) t; + return c.getSimpleName().toString(); + } else if (t instanceof MethodTree) { + var m = (MethodTree) t; + return m.getName().toString(); + } else if (t instanceof VariableTree) { + var v = (VariableTree) t; + return v.getName().toString(); + } else { + LOG.warning("Don't know how to create SymbolInformation from " + t); + return "???"; + } + } + + static SymbolInformation asSymbolInformation(TreePath path) { + var i = new SymbolInformation(); + var t = path.getLeaf(); + i.setKind(asSymbolKind(t.getKind())); + i.setName(symbolName(t)); + i.setContainerName(containerName(path)); + i.setLocation(Parser.location(path)); + return i; + } + + /** Find all already-imported symbols in all .java files in sourcePath */ + static ExistingImports existingImports(Collection<Path> sourcePath) { + var classes = new HashSet<String>(); + var packages = new HashSet<String>(); + var importClass = Pattern.compile("^import +(([\\w\\.]+)\\.\\w+);"); + var importStar = Pattern.compile("^import +([\\w\\.]+)\\.\\*;"); + var importSimple = Pattern.compile("^import +(\\w+);"); + Consumer<Path> findImports = + path -> { + try (var lines = Files.newBufferedReader(path)) { + while (true) { + var line = lines.readLine(); + // If we reach the end of the file, stop looking for imports + if (line == null) return; + // If we reach a class declaration, stop looking for imports + // TODO This could be a little more specific + if (line.contains("class")) return; + // import foo.bar.Doh; + var matchesClass = importClass.matcher(line); + if (matchesClass.matches()) { + String className = matchesClass.group(1), packageName = matchesClass.group(2); + packages.add(packageName); + classes.add(className); + } + // import foo.bar.* + var matchesStar = importStar.matcher(line); + if (matchesStar.matches()) { + var packageName = matchesStar.group(1); + packages.add(packageName); + } + // import Doh + var matchesSimple = importSimple.matcher(line); + if (matchesSimple.matches()) { + var className = matchesSimple.group(1); + classes.add(className); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + sourcePath.stream().flatMap(InferSourcePath::allJavaFiles).forEach(findImports); + return new ExistingImports(classes, packages); + } + + static String mostName(String name) { + var lastDot = name.lastIndexOf('.'); + return lastDot == -1 ? "" : name.substring(0, lastDot); + } + + static String lastName(String name) { + int i = name.lastIndexOf('.'); + if (i == -1) return name; + else return name.substring(i + 1); + } + + // TODO does this really belong in Parser? + private static Optional<String> resolveSymbol(String unresolved, ExistingImports imports, Set<String> classPath) { + // Try to disambiguate by looking for exact matches + // For example, Foo is exactly matched by `import com.bar.Foo` + // Foo is *not* exactly matched by `import com.bar.*` + var candidates = imports.classes.stream().filter(c -> c.endsWith("." + unresolved)).collect(Collectors.toSet()); + if (candidates.size() > 1) { + LOG.warning( + String.format( + "%s is ambiguous between previously imported candidates %s", + unresolved, Joiner.on(", ").join(candidates))); + return Optional.empty(); + } else if (candidates.size() == 1) { + return Optional.of(candidates.iterator().next()); + } + + // Try to disambiguate by looking at package names + // Both normal imports like `import com.bar.Foo`, and star-imports like `import com.bar.*`, + // are used to generate package names + candidates = + classPath + .stream() + .filter(c -> lastName(c).equals(unresolved)) + .filter(c -> imports.packages.contains(mostName(c))) + .collect(Collectors.toSet()); + if (candidates.size() > 1) { + LOG.warning( + String.format( + "%s is ambiguous between package-based candidates %s", + unresolved, Joiner.on(", ").join(candidates))); + return Optional.empty(); + } else if (candidates.size() == 1) { + return Optional.of(candidates.iterator().next()); + } + + // If there is only one class on the classpath with this name, use it + candidates = classPath.stream().filter(c -> lastName(c).equals(unresolved)).collect(Collectors.toSet()); + + if (candidates.size() > 1) { + LOG.warning( + String.format( + "%s is ambiguous between classpath candidates %s", + unresolved, Joiner.on(", ").join(candidates))); + } else if (candidates.size() == 1) { + return Optional.of(candidates.iterator().next()); + } else { + LOG.warning(unresolved + " does not appear on the classpath"); + } + + // Try to import from java stdlib + Comparator<String> order = + Comparator.comparing( + c -> { + if (c.startsWith("java.lang")) return 1; + else if (c.startsWith("java.util")) return 2; + else if (c.startsWith("java.io")) return 3; + else return 4; + }); + return candidates.stream().filter(c -> c.startsWith("java.")).sorted(order).findFirst(); + } + + // TODO does this really belong in Parser? + static Map<String, String> resolveSymbols( + Set<String> unresolvedSymbols, ExistingImports imports, Set<String> classPath) { + var result = new HashMap<String, String>(); + for (var s : unresolvedSymbols) { + resolveSymbol(s, imports, classPath).ifPresent(resolved -> result.put(s, resolved)); + } + return result; + } + + private static final Logger LOG = Logger.getLogger("main"); +} diff --git a/src/main/java/org/javacs/Pruner.java b/src/main/java/org/javacs/Pruner.java new file mode 100644 index 0000000..ff81b7c --- /dev/null +++ b/src/main/java/org/javacs/Pruner.java @@ -0,0 +1,120 @@ +package org.javacs; + +import com.sun.source.tree.*; +import com.sun.source.util.JavacTask; +import com.sun.source.util.TreeScanner; +import com.sun.source.util.Trees; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.ServiceLoader; +import java.util.logging.Logger; +import javax.tools.Diagnostic; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; + +class Pruner { + private static final Logger LOG = Logger.getLogger("main"); + // Parse-only compiler + // TODO this should come from Parser + private static final JavaCompiler COMPILER = ServiceLoader.load(JavaCompiler.class).iterator().next(); + private static final StandardJavaFileManager FILE_MANAGER = + COMPILER.getStandardFileManager(Pruner::report, null, Charset.defaultCharset()); + + private static void report(Diagnostic<? extends JavaFileObject> diags) { + LOG.warning(diags.getMessage(null)); + } + + private static JavacTask singleFileTask(URI file, String contents) { + return (JavacTask) + COMPILER.getTask( + null, + FILE_MANAGER, + Pruner::report, + Arrays.asList("-proc:none", "-g"), + Collections.emptyList(), + Collections.singletonList(new StringFileObject(contents, file))); + } + + private final JavacTask task; + private final CompilationUnitTree root; + private final StringBuilder contents; + + Pruner(URI file, String contents) { + this.task = singleFileTask(file, contents); + try { + this.root = task.parse().iterator().next(); + } catch (IOException e) { + throw new RuntimeException(e); + } + this.contents = new StringBuilder(contents); + } + + void prune(int line, int character) { + var sourcePositions = Trees.instance(task).getSourcePositions(); + var lines = root.getLineMap(); + var cursor = lines.getPosition(line, character); + + class Scan extends TreeScanner<Void, Void> { + boolean erasedAfterCursor = false; + + boolean containsCursor(Tree node) { + long start = sourcePositions.getStartPosition(root, node), + end = sourcePositions.getEndPosition(root, node); + return start <= cursor && cursor <= end; + } + + void erase(long start, long end) { + for (int i = (int) start; i < end; i++) { + switch (contents.charAt(i)) { + case '\r': + case '\n': + break; + default: + contents.setCharAt(i, ' '); + } + } + } + + @Override + public Void visitBlock(BlockTree node, Void aVoid) { + if (containsCursor(node)) { + super.visitBlock(node, aVoid); + // When we find the deepest block that includes the cursor + if (!erasedAfterCursor) { + var start = cursor; + var end = sourcePositions.getEndPosition(root, node); + // Find the next line + while (start < end && contents.charAt((int) start) != '\n') start++; + // Find the end of the block + while (end > start && contents.charAt((int) end) != '}') end--; + // Erase from next line to end of block + erase(start, end - 1); + erasedAfterCursor = true; + } + } else if (!node.getStatements().isEmpty()) { + var first = node.getStatements().get(0); + var last = node.getStatements().get(node.getStatements().size() - 1); + var start = sourcePositions.getStartPosition(root, first); + var end = sourcePositions.getEndPosition(root, last); + erase(start, end); + } + return null; + } + + @Override + public Void visitErroneous(ErroneousTree node, Void nothing) { + return super.scan(node.getErrorTrees(), nothing); + } + } + + new Scan().scan(root, null); + } + + String contents() { + return contents.toString(); + } +} diff --git a/src/main/java/org/javacs/ReachableClass.java b/src/main/java/org/javacs/ReachableClass.java deleted file mode 100644 index e7b49ce..0000000 --- a/src/main/java/org/javacs/ReachableClass.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.javacs; - -class ReachableClass { - final String packageName, className; - final boolean publicClass, publicConstructor, packagePrivateConstructor, hasTypeParameters; - - ReachableClass( - String packageName, - String className, - boolean publicClass, - boolean publicConstructor, - boolean packagePrivateConstructor, - boolean hasTypeParameters) { - this.packageName = packageName; - this.className = className; - this.publicClass = publicClass; - this.publicConstructor = publicConstructor; - this.packagePrivateConstructor = packagePrivateConstructor; - this.hasTypeParameters = hasTypeParameters; - } - - String qualifiedName() { - return packageName.isEmpty() ? className : packageName + "." + className; - } - - boolean hasAccessibleConstructor(String fromPackage) { - boolean samePackage = fromPackage.equals(packageName); - - return (publicClass && publicConstructor) || (samePackage && packagePrivateConstructor); - } -} diff --git a/src/main/java/org/javacs/RefactorFile.java b/src/main/java/org/javacs/RefactorFile.java deleted file mode 100644 index 0ffb02d..0000000 --- a/src/main/java/org/javacs/RefactorFile.java +++ /dev/null @@ -1,211 +0,0 @@ -package org.javacs; - -import com.google.common.collect.Lists; -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.tree.ImportTree; -import com.sun.source.tree.MemberSelectTree; -import com.sun.source.tree.Tree; -import com.sun.source.util.JavacTask; -import com.sun.source.util.SourcePositions; -import com.sun.source.util.Trees; -import com.sun.tools.javac.file.JavacFileManager; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.Pretty; -import com.sun.tools.javac.tree.TreeMaker; -import com.sun.tools.javac.util.Context; -import com.sun.tools.javac.util.Names; -import java.io.IOException; -import java.io.Reader; -import java.io.StringWriter; -import java.util.*; -import javax.tools.JavaFileObject; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.TextEdit; - -class RefactorFile { - private final JavacTask task; - private final CompilationUnitTree source; - private final SourcePositions pos; - private static final Position zero = new Position(0, 0); - - RefactorFile(JavacTask task, CompilationUnitTree source) { - this.task = task; - this.source = source; - this.pos = Trees.instance(task).getSourcePositions(); - } - - List<TextEdit> addImport(String packageName, String className) { - Objects.requireNonNull(packageName, "Package name is null"); - Objects.requireNonNull(className, "Class name is null"); - - if (alreadyImported(source, packageName, className)) return Collections.emptyList(); - - return Collections.singletonList(insertSomehow(packageName, className)); - } - - private TextEdit insertSomehow(String packageName, String className) { - Optional<TextEdit> done = insertInAlphabeticalOrder(packageName, className); - if (done.isPresent()) return done.get(); - - done = insertAfterImports(packageName, className); - if (done.isPresent()) return done.get(); - - done = insertAfterPackage(packageName, className); - if (done.isPresent()) return done.get(); - - return insertAtTop(packageName, className); - } - - private Optional<TextEdit> insertInAlphabeticalOrder(String packageName, String className) { - String insertLine = String.format("import %s.%s;\n", packageName, className); - - return source.getImports() - .stream() - .filter(i -> qualifiedName(i).compareTo(packageName + "." + className) > 0) - .map(this::startPosition) - .findFirst() - .map(at -> new TextEdit(new Range(at, at), insertLine)); - } - - private String qualifiedName(ImportTree tree) { - return tree.getQualifiedIdentifier().toString(); - } - - private Optional<TextEdit> insertAfterImports(String packageName, String className) { - String insertLine = String.format("\nimport %s.%s;", packageName, className); - - return endOfImports().map(at -> new TextEdit(new Range(at, at), insertLine)); - } - - private Optional<TextEdit> insertAfterPackage(String packageName, String className) { - String insertLine = String.format("\n\nimport %s.%s;", packageName, className); - - return endOfPackage().map(at -> new TextEdit(new Range(at, at), insertLine)); - } - - private TextEdit insertAtTop(String packageName, String className) { - String insertLine = String.format("import %s.%s;\n\n", packageName, className); - - return new TextEdit(new Range(zero, zero), insertLine); - } - - private Optional<Position> endOfImports() { - return source.getImports().stream().max(this::comparePosition).map(this::endPosition); - } - - private Optional<Position> endOfPackage() { - return Optional.ofNullable(source.getPackageName()).map(this::endPosition); - } - - private Position startPosition(Tree tree) { - return findOffset(pos.getStartPosition(source, tree), false); - } - - private Position endPosition(Tree tree) { - return findOffset(pos.getEndPosition(source, tree), true); - } - - /** Convert on offset-based position to a {@link Position} */ - private Position findOffset(long find, boolean endOfLine) { - JavaFileObject file = source.getSourceFile(); - - try (Reader in = file.openReader(true)) { - long offset = 0; - int line = 0; - int character = 0; - - while (offset < find) { - int next = in.read(); - - if (next < 0) break; - else { - offset++; - character++; - - if (next == '\n') { - line++; - character = 0; - } - } - } - - if (endOfLine) { - while (true) { - int next = in.read(); - - if (next < 0 || next == '\n') break; - else { - offset++; - character++; - } - } - } - - return new Position(line, character); - } catch (IOException e) { - throw ShowMessageException.error(e.getMessage(), e); - } - } - - private static boolean alreadyImported( - CompilationUnitTree source, String packageName, String className) { - return source.getImports().stream().anyMatch(i -> importEquals(i, packageName, className)); - } - - private static String organizeImports( - CompilationUnitTree source, String packageName, String className) { - Context context = new Context(); - JavacFileManager fileManager = new JavacFileManager(context, true, null); - Names names = Names.instance(context); - TreeMaker factory = TreeMaker.instance(context); - List<ImportTree> imports = Lists.newArrayList(source.getImports()); - - imports.add( - factory.Import( - factory.Select( - factory.Ident(names.fromString(packageName)), - names.fromString(className)), - false)); - - JCTree.JCCompilationUnit after = - factory.TopLevel( - list(JCTree.JCAnnotation.class, source.getPackageAnnotations()), - (JCTree.JCExpression) source.getPackageName(), - list(JCTree.class, imports)); - StringWriter buffer = new StringWriter(); - Pretty prettyPrint = new Pretty(buffer, true); - - try { - prettyPrint.printUnit(after, null); - } catch (IOException e) { - throw new RuntimeException(e); - } - - return buffer.toString(); - } - - private static <T> com.sun.tools.javac.util.List<T> list(Class<T> cast, List<?> from) { - List<T> out = new ArrayList<>(); - - for (Object each : from) { - out.add(cast.cast(each)); - } - - return com.sun.tools.javac.util.List.from(out); - } - - private int comparePosition(Tree left, Tree right) { - long leftPos = pos.getStartPosition(source, left); - long rightPos = pos.getStartPosition(source, right); - - return Long.compare(leftPos, rightPos); - } - - private static boolean importEquals(ImportTree i, String packageName, String className) { - MemberSelectTree access = (MemberSelectTree) i.getQualifiedIdentifier(); - - return access.getExpression().toString().equals(packageName) - && access.getIdentifier().toString().equals(className); - } -} diff --git a/src/main/java/org/javacs/References.java b/src/main/java/org/javacs/References.java deleted file mode 100644 index 32a4a2d..0000000 --- a/src/main/java/org/javacs/References.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.javacs; - -import com.sun.source.util.JavacTask; -import com.sun.source.util.TreePath; -import com.sun.source.util.Trees; -import java.util.Optional; -import java.util.stream.Stream; -import javax.lang.model.element.Element; -import org.eclipse.lsp4j.Location; - -public class References { - - public static Stream<Location> findReferences(FocusedResult compiled, FindSymbols find) { - return compiled.cursor - .map(cursor -> new References(compiled.task, find).doFindReferences(cursor)) - .orElseGet(Stream::empty); - } - - public static Optional<Location> gotoDefinition(FocusedResult compiled, FindSymbols find) { - return compiled.cursor.flatMap( - cursor -> new References(compiled.task, find).doGotoDefinition(cursor)); - } - - private final JavacTask task; - private final FindSymbols find; - - private References(JavacTask task, FindSymbols find) { - this.task = task; - this.find = find; - } - - private Stream<Location> doFindReferences(TreePath cursor) { - Trees trees = Trees.instance(task); - Element symbol = trees.getElement(cursor); - - if (symbol == null) return Stream.empty(); - else return find.references(symbol); - } - - private Optional<Location> doGotoDefinition(TreePath cursor) { - CursorContext context = CursorContext.from(cursor); - - if (context == CursorContext.NewClass) cursor = context.find(cursor); - - Trees trees = Trees.instance(task); - Element symbol = trees.getElement(cursor); - - return find.find(symbol); - } -} diff --git a/src/main/java/org/javacs/ShortTypePrinter.java b/src/main/java/org/javacs/ShortTypePrinter.java index a9f613c..988cbdb 100644 --- a/src/main/java/org/javacs/ShortTypePrinter.java +++ b/src/main/java/org/javacs/ShortTypePrinter.java @@ -1,8 +1,10 @@ package org.javacs; +import java.util.StringJoiner; import java.util.logging.Logger; import java.util.stream.Collectors; -import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; import javax.lang.model.type.*; import javax.lang.model.util.AbstractTypeVisitor8; @@ -17,18 +19,12 @@ class ShortTypePrinter extends AbstractTypeVisitor8<String, Void> { @Override public String visitIntersection(IntersectionType t, Void aVoid) { - return t.getBounds() - .stream() - .map(ShortTypePrinter::print) - .collect(Collectors.joining(" & ")); + return t.getBounds().stream().map(ShortTypePrinter::print).collect(Collectors.joining(" & ")); } @Override public String visitUnion(UnionType t, Void aVoid) { - return t.getAlternatives() - .stream() - .map(ShortTypePrinter::print) - .collect(Collectors.joining(" | ")); + return t.getAlternatives().stream().map(ShortTypePrinter::print).collect(Collectors.joining(" | ")); } @Override @@ -48,28 +44,18 @@ class ShortTypePrinter extends AbstractTypeVisitor8<String, Void> { @Override public String visitDeclared(DeclaredType t, Void aVoid) { - String result = ""; - - // If type is an inner class, add outer class name - if (t.asElement().getKind() == ElementKind.CLASS - && t.getEnclosingType().getKind() == TypeKind.DECLARED) { - - result += print(t.getEnclosingType()) + "."; - } - - result += t.asElement().getSimpleName().toString(); + var result = t.asElement().toString(); if (!t.getTypeArguments().isEmpty()) { String params = - t.getTypeArguments() - .stream() - .map(ShortTypePrinter::print) - .collect(Collectors.joining(", ")); + t.getTypeArguments().stream().map(ShortTypePrinter::print).collect(Collectors.joining(", ")); result += "<" + params + ">"; } - return result; + if (result.matches("java\\.lang\\.\\w+")) return result.substring("java.lang.".length()); + else if (result.startsWith("java\\.util\\.\\w+")) return result.substring("java.util.".length()); + else return result; } @Override @@ -109,4 +95,35 @@ class ShortTypePrinter extends AbstractTypeVisitor8<String, Void> { public String visitNoType(NoType t, Void aVoid) { return t.toString(); } + + static boolean missingParamNames(ExecutableElement e) { + return e.getParameters().stream().allMatch(p -> p.getSimpleName().toString().matches("arg\\d+")); + } + + private static String printArguments(ExecutableElement e) { + var result = new StringJoiner(", "); + var missingParamNames = missingParamNames(e); + for (var p : e.getParameters()) { + var s = new StringBuilder(); + s.append(ShortTypePrinter.print(p.asType())); + if (!missingParamNames) { + s.append(" ").append(p.getSimpleName()); + } + result.add(s); + } + return result.toString(); + } + + static String printMethod(ExecutableElement m) { + if (m.getSimpleName().contentEquals("<init>")) { + return m.getEnclosingElement().getSimpleName() + "(" + printArguments(m) + ")"; + } else { + var result = new StringBuilder(); + if (m.getModifiers().contains(Modifier.STATIC)) result.append("static "); + result.append(ShortTypePrinter.print(m.getReturnType())).append(" "); + result.append(m.getSimpleName()); + result.append("(").append(printArguments(m)).append(")"); + return result.toString(); + } + } } diff --git a/src/main/java/org/javacs/ShowMessageException.java b/src/main/java/org/javacs/ShowMessageException.java deleted file mode 100644 index 878f8d9..0000000 --- a/src/main/java/org/javacs/ShowMessageException.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.javacs; - -import org.eclipse.lsp4j.*; -import org.eclipse.lsp4j.services.*; - -class ShowMessageException extends RuntimeException { - private final MessageParams message; - - ShowMessageException(MessageParams message, Exception cause) { - super(message.getMessage(), cause); - - this.message = message; - } - - static ShowMessageException error(String message, Exception cause) { - return create(MessageType.Error, message, cause); - } - - static ShowMessageException warning(String message, Exception cause) { - return create(MessageType.Warning, message, cause); - } - - private static ShowMessageException create(MessageType type, String message, Exception cause) { - MessageParams m = new MessageParams(type, message); - - return new ShowMessageException(m, cause); - } -} diff --git a/src/main/java/org/javacs/Signatures.java b/src/main/java/org/javacs/Signatures.java deleted file mode 100644 index 3fa094d..0000000 --- a/src/main/java/org/javacs/Signatures.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.javacs; - -import com.sun.javadoc.ConstructorDoc; -import com.sun.javadoc.MethodDoc; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.Tree; -import com.sun.source.util.JavacTask; -import com.sun.source.util.SourcePositions; -import com.sun.source.util.TreePath; -import com.sun.source.util.Trees; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import javax.lang.model.element.*; -import org.eclipse.lsp4j.ParameterInformation; -import org.eclipse.lsp4j.SignatureHelp; -import org.eclipse.lsp4j.SignatureInformation; - -class Signatures { - static Optional<SignatureHelp> help( - FocusedResult compiled, int line, int column, Javadocs docs) { - long offset = compiled.compilationUnit.getLineMap().getPosition(line, column); - - return compiled.cursor.flatMap(c -> new Signatures(c, offset, compiled.task, docs).get()); - } - - private final TreePath cursor; - private final long cursorOffset; - private final JavacTask task; - private final Javadocs docs; - - private Signatures(TreePath cursor, long cursorOffset, JavacTask task, Javadocs docs) { - this.cursor = cursor; - this.cursorOffset = cursorOffset; - this.task = task; - this.docs = docs; - } - - private Optional<SignatureHelp> get() { - if (cursor.getLeaf().getKind() == Tree.Kind.METHOD_INVOCATION) - return Optional.of(methodHelp((MethodInvocationTree) cursor.getLeaf())); - if (cursor.getLeaf().getKind() == Tree.Kind.NEW_CLASS) - return Optional.of(constructorHelp((NewClassTree) cursor.getLeaf())); - if (cursor.getParentPath().getLeaf().getKind() == Tree.Kind.METHOD_INVOCATION) - return Optional.of(methodHelp((MethodInvocationTree) cursor.getParentPath().getLeaf())); - if (cursor.getParentPath().getLeaf().getKind() == Tree.Kind.NEW_CLASS) - return Optional.of(constructorHelp((NewClassTree) cursor.getParentPath().getLeaf())); - else return Optional.empty(); - } - - private SignatureHelp constructorHelp(NewClassTree leaf) { - Trees trees = Trees.instance(task); - TreePath identifierPath = - TreePath.getPath(cursor.getCompilationUnit(), leaf.getIdentifier()); - Element classElement = trees.getElement(identifierPath); - List<ExecutableElement> candidates = - classElement - .getEnclosedElements() - .stream() - .filter(member -> member.getKind() == ElementKind.CONSTRUCTOR) - .map(method -> (ExecutableElement) method) - .collect(Collectors.toList()); - List<SignatureInformation> signatures = - candidates - .stream() - .map(member -> constructorInfo(member)) - .collect(Collectors.toList()); - int activeSignature = candidates.indexOf(classElement); - - return new SignatureHelp( - signatures, - activeSignature < 0 ? null : activeSignature, - activeParameter(leaf.getArguments())); - } - - private SignatureHelp methodHelp(MethodInvocationTree leaf) { - Trees trees = Trees.instance(task); - TreePath methodPath = TreePath.getPath(cursor.getCompilationUnit(), leaf.getMethodSelect()); - Element methodElement = trees.getElement(methodPath); - Name name = methodElement.getSimpleName(); - List<ExecutableElement> candidates = - methodElement - .getEnclosingElement() - .getEnclosedElements() - .stream() - .filter( - member -> - member.getKind() == ElementKind.METHOD - && member.getSimpleName().equals(name)) - .map(method -> (ExecutableElement) method) - .collect(Collectors.toList()); - List<SignatureInformation> signatures = - candidates.stream().map(member -> methodInfo(member)).collect(Collectors.toList()); - int activeSignature = candidates.indexOf(methodElement); - - return new SignatureHelp( - signatures, - activeSignature < 0 ? null : activeSignature, - activeParameter(leaf.getArguments())); - } - - private SignatureInformation constructorInfo(ExecutableElement method) { - Optional<ConstructorDoc> doc = docs.constructorDoc(docs.methodKey(method)); - Optional<String> docText = - doc.flatMap(constructor -> Optional.ofNullable(constructor.commentText())) - .map(Javadocs::htmlToMarkdown) - .map(Javadocs::firstSentence); - - return new SignatureInformation( - Hovers.methodSignature(method, false, true), - docText.orElse(null), - paramInfo(method)); - } - - private SignatureInformation methodInfo(ExecutableElement method) { - Optional<MethodDoc> doc = docs.methodDoc(docs.methodKey(method)); - Optional<String> docText = - doc.flatMap(Javadocs::commentText) - .map(Javadocs::htmlToMarkdown) - .map(Javadocs::firstSentence); - - return new SignatureInformation( - Hovers.methodSignature(method, true, true), - docText.orElse(null), - paramInfo(method)); - } - - private List<ParameterInformation> paramInfo(ExecutableElement method) { - List<ParameterInformation> params = new ArrayList<>(); - - int i = 0; - for (VariableElement var : method.getParameters()) { - boolean varargs = method.isVarArgs() && i == method.getParameters().size() - 1; - - params.add( - new ParameterInformation( - Hovers.shortName(var, varargs), task.getElements().getDocComment(var))); - - i++; - } - return params; - } - - private Integer activeParameter(List<? extends ExpressionTree> arguments) { - SourcePositions pos = Trees.instance(task).getSourcePositions(); - - int i = 0; - for (ExpressionTree arg : arguments) { - if (pos.getEndPosition(cursor.getCompilationUnit(), arg) >= cursorOffset) return i; - - i++; - } - - return null; - } -} diff --git a/src/main/java/org/javacs/SourceFileIndex.java b/src/main/java/org/javacs/SourceFileIndex.java deleted file mode 100644 index ade65ea..0000000 --- a/src/main/java/org/javacs/SourceFileIndex.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.javacs; - -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; - -class SourceFileIndex { - String packageName = ""; - - /** Simple names of classes declared in this file */ - final Set<ReachableClass> topLevelClasses = new HashSet<>(); - - /** Simple name of declarations in this file, including classes, methods, and fields */ - final Set<String> declarations = new HashSet<>(); - - /** - * Simple names of reference appearing in this file. - * - * <p>This is fast to compute and provides a useful starting point for find-references - * operations. - */ - final Set<String> references = new HashSet<>(); - - final Instant updated = Instant.now(); -} diff --git a/src/main/java/org/javacs/SymbolIndex.java b/src/main/java/org/javacs/SymbolIndex.java deleted file mode 100644 index 839cf77..0000000 --- a/src/main/java/org/javacs/SymbolIndex.java +++ /dev/null @@ -1,556 +0,0 @@ -package org.javacs; - -import com.google.common.collect.Maps; -import com.sun.source.tree.*; -import com.sun.source.util.*; -import com.sun.tools.javac.api.JavacTool; -import com.sun.tools.javac.file.JavacFileManager; -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Instant; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.logging.Logger; -import java.util.stream.Stream; -import javax.lang.model.element.*; -import javax.tools.Diagnostic; -import javax.tools.JavaFileObject; -import org.eclipse.lsp4j.*; - -/** - * Global index of exported symbol declarations and references. such as classes, methods, and - * fields. - */ -class SymbolIndex { - - private final Path workspaceRoot; - private final Supplier<Collection<URI>> openFiles; - private final Function<URI, Optional<String>> activeContent; - private final JavacTool parser = JavacTool.create(); - private final JavacFileManager emptyFileManager = - parser.getStandardFileManager(__ -> {}, null, Charset.defaultCharset()); - - /** Source path files, for which we support methods and classes */ - private final Map<URI, SourceFileIndex> sourcePathFiles = new ConcurrentHashMap<>(); - - private final CompletableFuture<?> finishedInitialIndex = new CompletableFuture<>(); - - SymbolIndex( - Path workspaceRoot, - Supplier<Collection<URI>> openFiles, - Function<URI, Optional<String>> activeContent) { - this.workspaceRoot = workspaceRoot; - this.openFiles = openFiles; - this.activeContent = activeContent; - - new Thread(this::initialIndex, "Initial-Index").start(); - } - - private void initialIndex() { - // TODO send a progress bar to the user - updateIndex(InferConfig.allJavaFiles(workspaceRoot).map(Path::toUri)); - - finishedInitialIndex.complete(null); - } - - private void updateIndex(Stream<URI> files) { - files.forEach(this::updateFile); - } - - private void updateFile(URI each) { - if (needsUpdate(each)) { - CompilationUnitTree tree = parse(each); - - update(tree); - } - } - - private boolean needsUpdate(URI file) { - if (!sourcePathFiles.containsKey(file)) return true; - else { - try { - Instant updated = sourcePathFiles.get(file).updated; - Instant modified = Files.getLastModifiedTime(Paths.get(file)).toInstant(); - - return updated.isBefore(modified); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private final Map<URI, String> warnedPackageDirectoryConflict = new HashMap<>(); - - /** - * Guess the source path by looking at package declarations in .java files. - * - * <p>For example, if the file src/com/example/Test.java has the package declaration `package - * com.example;` then the source root is `src`. - */ - Set<Path> sourcePath() { - updateOpenFiles(); - - Set<Path> result = new HashSet<>(); - - sourcePathFiles.forEach( - (uri, index) -> { - Path dir = Paths.get(uri).getParent(); - String packagePath = index.packageName.replace('.', File.separatorChar); - - if (!dir.endsWith(packagePath) - && !warnedPackageDirectoryConflict - .getOrDefault(uri, "?") - .equals(packagePath)) { - LOG.warning("Java source file " + uri + " is not in " + packagePath); - - warnedPackageDirectoryConflict.put(uri, packagePath); - } else { - int up = Paths.get(packagePath).getNameCount(); - Path truncate = dir; - - for (int i = 0; i < up; i++) truncate = truncate.getParent(); - - result.add(truncate); - } - }); - - return result; - } - - /** Search all indexed symbols */ - Stream<SymbolInformation> search(String query) { - updateOpenFiles(); - - Predicate<CharSequence> nameMatchesQuery = - name -> Completions.containsCharactersInOrder(name, query, true); - Predicate<URI> fileMatchesQuery = - uri -> sourcePathFiles.get(uri).declarations.stream().anyMatch(nameMatchesQuery); - Collection<URI> open = openFiles.get(); - Stream<URI> openFirst = - Stream.concat( - open.stream(), - sourcePathFiles.keySet().stream().filter(uri -> !open.contains(uri))); - - return openFirst - .filter(fileMatchesQuery) - .flatMap(this::allInFile) - .filter(info -> nameMatchesQuery.test(info.getName())); - } - - void updateOpenFiles() { - finishedInitialIndex.join(); - - updateIndex(openFiles.get().stream()); - } - - /** Get all declarations in an open file */ - Stream<SymbolInformation> allInFile(URI source) { - LOG.info("Search " + source); - - JavacTask task = parseTask(source); - Trees trees = Trees.instance(task); - - class FindDeclarations extends TreePathScanner<Void, Void> { - List<SymbolInformation> result = new ArrayList<>(); - Optional<ClassTree> currentClass = Optional.empty(); - - @Override - public Void visitClass(ClassTree node, Void aVoid) { - String name = node.getSimpleName().toString(); - SymbolInformation info = new SymbolInformation(); - - info.setContainerName(packageName()); - info.setKind(SymbolKind.Class); - info.setName(name); - findTreeName(name, getCurrentPath(), trees).ifPresent(info::setLocation); - - result.add(info); - - // Push current class and continue - Optional<ClassTree> previousClass = currentClass; - - currentClass = Optional.of(node); - super.visitClass(node, aVoid); - currentClass = previousClass; - - return null; - } - - @Override - public Void visitMethod(MethodTree node, Void aVoid) { - boolean constructor = node.getName().contentEquals("<init>"); - String name = - constructor - ? currentClass.get().getSimpleName().toString() - : node.getName().toString(); - SymbolInformation info = new SymbolInformation(); - - info.setContainerName(qualifiedClassName()); - info.setKind(constructor ? SymbolKind.Constructor : SymbolKind.Method); - info.setName(name); - findTreeName(name, getCurrentPath(), trees).ifPresent(info::setLocation); - - result.add(info); - - return super.visitMethod(node, aVoid); - } - - @Override - public Void visitVariable(VariableTree node, Void aVoid) { - // If this is a field - if (getCurrentPath().getParentPath().getLeaf().getKind() == Tree.Kind.CLASS) { - String name = node.getName().toString(); - SymbolInformation info = new SymbolInformation(); - - info.setContainerName(qualifiedClassName()); - info.setName(name); - info.setKind(SymbolKind.Property); - findTreeName(name, getCurrentPath(), trees).ifPresent(info::setLocation); - - result.add(info); - } - - return super.visitVariable(node, aVoid); - } - - private String qualifiedClassName() { - String packageName = packageName(); - String className = currentClass.get().getSimpleName().toString(); - - return packageName.isEmpty() ? className : packageName + "." + className; - } - - private String packageName() { - return Objects.toString(getCurrentPath().getCompilationUnit().getPackageName(), ""); - } - } - - FindDeclarations scan = new FindDeclarations(); - - try { - CompilationUnitTree tree = task.parse().iterator().next(); - - scan.scan(tree, null); - - return scan.result.stream(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - boolean isTopLevelClass(String qualifiedName) { - return doFindDeclaringFile(qualifiedName).isPresent(); - } - - Stream<ReachableClass> accessibleTopLevelClasses(String fromPackage) { - finishedInitialIndex.join(); - - return sourcePathFiles.values().stream().flatMap(doAccessibleTopLevelClasses(fromPackage)); - } - - private Function<SourceFileIndex, Stream<ReachableClass>> doAccessibleTopLevelClasses( - String fromPackage) { - return index -> - index.topLevelClasses - .stream() - .filter(c -> c.publicClass || c.packageName.equals(fromPackage)); - } - - Stream<ReachableClass> allTopLevelClasses() { - finishedInitialIndex.join(); - - return sourcePathFiles.values().stream().flatMap(index -> index.topLevelClasses.stream()); - } - - Optional<URI> findDeclaringFile(TypeElement topLevelClass) { - updateOpenFiles(); - - String qualifiedName = topLevelClass.getQualifiedName().toString(); - - return doFindDeclaringFile(qualifiedName); - } - - private Optional<URI> doFindDeclaringFile(String qualifiedName) { - String packageName = Completions.mostIds(qualifiedName), - className = Completions.lastId(qualifiedName); - Predicate<Map.Entry<URI, SourceFileIndex>> containsClass = - entry -> { - SourceFileIndex index = entry.getValue(); - - return index.packageName.equals(packageName) - && index.topLevelClasses - .stream() - .anyMatch(c -> c.className.equals(className)); - }; - - return sourcePathFiles - .entrySet() - .stream() - .filter(containsClass) - .map(entry -> entry.getKey()) - .findFirst(); - } - - /** Update a file in the index */ - private void update(CompilationUnitTree compilationUnit) { - URI file = compilationUnit.getSourceFile().toUri(); - SourceFileIndex index = new SourceFileIndex(); - - index.packageName = Objects.toString(compilationUnit.getPackageName(), ""); - - new TreePathScanner<Void, Void>() { - int classDepth = 0; - - @Override - public Void visitClass(ClassTree node, Void aVoid) { - // If this is a top-level class, add qualified name to special topLevelClasses index - if (classDepth == 0) { - String className = node.getSimpleName().toString(); - Set<Modifier> flags = node.getModifiers().getFlags(); - boolean publicClass = flags.contains(Modifier.PUBLIC), - hasTypeParameters = !node.getTypeParameters().isEmpty(); - boolean publicConstructor = false, packagePrivateConstructor = false; - boolean hasExplicitConstructors = false; - - for (Tree each : node.getMembers()) { - if (each instanceof MethodTree) { - MethodTree method = (MethodTree) each; - - if (method.getName().contentEquals("<init>")) { - hasExplicitConstructors = true; - - Set<Modifier> methodFlags = method.getModifiers().getFlags(); - - if (publicClass && methodFlags.contains(Modifier.PUBLIC)) - publicConstructor = true; - else if (!methodFlags.contains(Modifier.PROTECTED) - && !methodFlags.contains(Modifier.PRIVATE)) - packagePrivateConstructor = true; - } - } - } - - if (!hasExplicitConstructors) { - publicConstructor = publicClass; - packagePrivateConstructor = !publicClass; - } - - index.topLevelClasses.add( - new ReachableClass( - index.packageName, - className, - publicClass, - publicConstructor, - packagePrivateConstructor, - hasTypeParameters)); - } - - // Add simple name to declarations - index.declarations.add(node.getSimpleName().toString()); - - // Recurse, but remember that anything inside isn't a top-level class - classDepth++; - super.visitClass(node, aVoid); - classDepth--; - - return null; - } - - @Override - public Void visitMethod(MethodTree node, Void aVoid) { - index.declarations.add(node.getName().toString()); - - return super.visitMethod(node, aVoid); - } - - @Override - public Void visitVariable(VariableTree node, Void aVoid) { - if (getCurrentPath().getParentPath().getLeaf().getKind() == Tree.Kind.CLASS) - index.declarations.add(node.getName().toString()); - - return super.visitVariable(node, aVoid); - } - - @Override - public Void visitMemberSelect(MemberSelectTree node, Void aVoid) { - index.references.add(node.getIdentifier().toString()); - - return super.visitMemberSelect(node, aVoid); - } - - @Override - public Void visitMemberReference(MemberReferenceTree node, Void aVoid) { - index.references.add(node.getName().toString()); - - return super.visitMemberReference(node, aVoid); - } - - @Override - public Void visitIdentifier(IdentifierTree node, Void aVoid) { - index.references.add(node.getName().toString()); - - return super.visitIdentifier(node, aVoid); - } - }.scan(compilationUnit, null); - - sourcePathFiles.put(file, index); - } - - Collection<URI> potentialReferences(String name) { - updateOpenFiles(); - - Map<URI, SourceFileIndex> hasName = - Maps.filterValues(sourcePathFiles, index -> index.references.contains(name)); - - return hasName.keySet(); - } - - /** Find a more accurate position for expression tree by searching for its name. */ - static Optional<Location> findTreeName(CharSequence name, TreePath path, Trees trees) { - if (path == null) return Optional.empty(); - - SourcePositions sourcePositions = trees.getSourcePositions(); - CompilationUnitTree compilationUnit = path.getCompilationUnit(); - long startExpr = sourcePositions.getStartPosition(compilationUnit, path.getLeaf()); - - if (startExpr == Diagnostic.NOPOS) return Optional.empty(); - - CharSequence content; - try { - content = compilationUnit.getSourceFile().getCharContent(false); - } catch (IOException e) { - throw new RuntimeException(e); - } - int startSymbol = indexOf(content, name, (int) startExpr); - - if (startSymbol == -1) return Optional.empty(); - - int line = (int) compilationUnit.getLineMap().getLineNumber(startSymbol); - int column = (int) compilationUnit.getLineMap().getColumnNumber(startSymbol); - - return Optional.of( - new Location( - compilationUnit.getSourceFile().toUri().toString(), - new Range( - new Position(line - 1, column - 1), - new Position(line - 1, column + name.length() - 1)))); - } - - /** - * Adapted from java.util.String. - * - * <p>The source is the character array being searched, and the target is the string being - * searched for. - * - * @param source the characters being searched. - * @param target the characters being searched for. - * @param fromIndex the index to begin searching from. - */ - private static int indexOf(CharSequence source, CharSequence target, int fromIndex) { - int sourceOffset = 0, - sourceCount = source.length(), - targetOffset = 0, - targetCount = target.length(); - - if (fromIndex >= sourceCount) { - return (targetCount == 0 ? sourceCount : -1); - } - if (fromIndex < 0) { - fromIndex = 0; - } - if (targetCount == 0) { - return fromIndex; - } - - char first = target.charAt(targetOffset); - int max = sourceOffset + (sourceCount - targetCount); - - for (int i = sourceOffset + fromIndex; i <= max; i++) { - /* Look for first character. */ - if (source.charAt(i) != first) { - while (++i <= max && source.charAt(i) != first) ; - } - - /* Found first character, now look apply the rest of v2 */ - if (i <= max) { - int j = i + 1; - int end = j + targetCount - 1; - for (int k = targetOffset + 1; - j < end && source.charAt(j) == target.charAt(k); - j++, k++) ; - - if (j == end) { - /* Found whole string. */ - return i - sourceOffset; - } - } - } - return -1; - } - - private static SymbolKind symbolInformationKind(ElementKind kind) { - switch (kind) { - case PACKAGE: - return SymbolKind.Package; - case ENUM: - case ENUM_CONSTANT: - return SymbolKind.Enum; - case CLASS: - return SymbolKind.Class; - case ANNOTATION_TYPE: - case INTERFACE: - return SymbolKind.Interface; - case FIELD: - return SymbolKind.Property; - case PARAMETER: - case LOCAL_VARIABLE: - case EXCEPTION_PARAMETER: - case TYPE_PARAMETER: - return SymbolKind.Variable; - case METHOD: - case STATIC_INIT: - case INSTANCE_INIT: - return SymbolKind.Method; - case CONSTRUCTOR: - return SymbolKind.Constructor; - case OTHER: - case RESOURCE_VARIABLE: - default: - return SymbolKind.String; - } - } - - private CompilationUnitTree parse(URI source) { - try { - return parseTask(source).parse().iterator().next(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private JavacTask parseTask(URI source) { - Optional<String> content = activeContent.apply(source); - JavaFileObject file = - content.map(text -> (JavaFileObject) new StringFileObject(text, source)) - .orElseGet(() -> emptyFileManager.getRegularFile(new File(source))); - - return parser.getTask( - null, - emptyFileManager, - err -> LOG.warning(err.getMessage(Locale.getDefault())), - Collections.emptyList(), - null, - Collections.singletonList(file)); - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/TestMethod.java b/src/main/java/org/javacs/TestMethod.java new file mode 100644 index 0000000..32d54de --- /dev/null +++ b/src/main/java/org/javacs/TestMethod.java @@ -0,0 +1,20 @@ +package org.javacs; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.JavacTask; + +public class TestMethod { + public final JavacTask parseTask; + public final CompilationUnitTree compilationUnit; + public final ClassTree enclosingClass; + public final MethodTree method; + + TestMethod(JavacTask parseTask, CompilationUnitTree compilationUnit, ClassTree enclosingClass, MethodTree method) { + this.parseTask = parseTask; + this.compilationUnit = compilationUnit; + this.enclosingClass = enclosingClass; + this.method = method; + } +} diff --git a/src/main/java/org/javacs/TreePruner.java b/src/main/java/org/javacs/TreePruner.java deleted file mode 100644 index 8948349..0000000 --- a/src/main/java/org/javacs/TreePruner.java +++ /dev/null @@ -1,182 +0,0 @@ -package org.javacs; - -import com.sun.source.tree.*; -import com.sun.source.util.JavacTask; -import com.sun.source.util.SourcePositions; -import com.sun.source.util.TreeScanner; -import com.sun.source.util.Trees; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.util.List; -import java.io.IOException; -import java.io.Reader; -import javax.tools.JavaFileObject; - -/** Fix up the tree to make it easier to autocomplete, index */ -class TreePruner { - private final JavacTask task; - - TreePruner(JavacTask task) { - this.task = task; - } - - void removeNonCursorMethodBodies(CompilationUnitTree source, int line, int column) { - long offset = source.getLineMap().getPosition(line, column); - - doRemoveNonCursorMethodBodies(source, offset); - } - - void removeAllMethodBodies(CompilationUnitTree source) { - doRemoveNonCursorMethodBodies(source, -1); - } - - private void doRemoveNonCursorMethodBodies(CompilationUnitTree source, long offset) { - SourcePositions sourcePositions = Trees.instance(task).getSourcePositions(); - - class Pruner extends TreeScanner<Void, Void> { - @Override - public Void visitBlock(BlockTree node, Void aVoid) { - if (containsCursor(node)) super.visitBlock(node, aVoid); - else { - JCTree.JCBlock impl = (JCTree.JCBlock) node; - - impl.stats = List.nil(); - } - - return null; - } - - boolean containsCursor(Tree leaf) { - long start = sourcePositions.getStartPosition(source, leaf); - long end = sourcePositions.getEndPosition(source, leaf); - - return start <= offset && offset <= end; - } - - @Override - public Void visitErroneous(ErroneousTree node, Void nothing) { - return super.scan(node.getErrorTrees(), nothing); - } - } - - new Pruner().scan(source, null); - } - - /** Remove all statements after the statement the cursor is in */ - void removeStatementsAfterCursor(CompilationUnitTree source, int line, int column) { - SourcePositions sourcePositions = Trees.instance(task).getSourcePositions(); - long offset = source.getLineMap().getPosition(line, column); - - class Pruner extends TreeScanner<Void, Void> { - @Override - public Void visitBlock(BlockTree node, Void aVoid) { - JCTree.JCBlock impl = (JCTree.JCBlock) node; - - impl.stats = pruneStatements(impl.stats); - - return super.visitBlock(node, aVoid); - } - - @Override - public Void visitSwitch(SwitchTree node, Void aVoid) { - JCTree.JCSwitch impl = (JCTree.JCSwitch) node; - - impl.cases = pruneStatements(impl.cases); - - return super.visitSwitch(node, aVoid); - } - - @Override - public Void visitCase(CaseTree node, Void aVoid) { - JCTree.JCCase impl = (JCTree.JCCase) node; - - impl.stats = pruneStatements(impl.stats); - - return super.visitCase(node, aVoid); - } - - private <T extends Tree> List<T> pruneStatements(List<T> stats) { - int countStatements = 0; - boolean foundCursor = false; - - // Scan up to statement containing cursor - while (countStatements < stats.size()) { - T s = stats.get(countStatements); - - if (containsCursor(s)) foundCursor = true; - else if (foundCursor) break; - else this.scan(s, null); - - countStatements++; - } - - // Remove all statements after statement containing cursor - return stats.take(countStatements); - } - - boolean containsCursor(Tree leaf) { - long start = sourcePositions.getStartPosition(source, leaf); - long end = sourcePositions.getEndPosition(source, leaf); - - return start <= offset && offset <= end; - } - - @Override - public Void visitErroneous(ErroneousTree node, Void nothing) { - return super.scan(node.getErrorTrees(), nothing); - } - } - - new Pruner().scan(source, null); - } - - /** - * Insert ';' after the users cursor so we recover from parse errors in a helpful way when doing - * autocomplete. - */ - static JavaFileObject putSemicolonAfterCursor( - JavaFileObject file, int cursorLine, int cursorCharacter) { - try (Reader reader = file.openReader(true)) { - StringBuilder acc = new StringBuilder(); - int line = 1, character = 1; - - // Go to the cursor - while (line < cursorLine || character < cursorCharacter) { - int next = reader.read(); - - if (next == -1) - throw new RuntimeException( - "End of file " - + file - + " before cursor " - + cursorLine - + ":" - + cursorCharacter); - else if (next == '\n') { - line++; - character = 1; - } else character++; - - acc.append((char) next); - } - - // Go to the end of the line - while (true) { - int next = reader.read(); - - if (next == -1 || next == '\n') break; - - acc.append((char) next); - } - - acc.append(";\n"); - - for (int next = reader.read(); next > 0; next = reader.read()) { - acc.append((char) next); - } - - return new StringFileObject(acc.toString(), file.toUri()); - } catch (IOException e) { - throw ShowMessageException.error("Error reading " + file, e); - } - } -} diff --git a/src/main/java/org/javacs/pubapi/ArrayTypeDesc.java b/src/main/java/org/javacs/pubapi/ArrayTypeDesc.java deleted file mode 100644 index 4668c22..0000000 --- a/src/main/java/org/javacs/pubapi/ArrayTypeDesc.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import java.io.Serializable; -import javax.lang.model.type.TypeKind; - -public class ArrayTypeDesc extends TypeDesc implements Serializable { - - private static final long serialVersionUID = -1177329549163314996L; - - TypeDesc compTypeDesc; - - public ArrayTypeDesc(TypeDesc compTypeDesc) { - super(TypeKind.ARRAY); - this.compTypeDesc = compTypeDesc; - } - - @Override - public boolean equals(Object obj) { - if (!super.equals(obj)) return false; - return compTypeDesc.equals(((ArrayTypeDesc) obj).compTypeDesc); - } - - @Override - public int hashCode() { - return super.hashCode() ^ compTypeDesc.hashCode(); - } -} diff --git a/src/main/java/org/javacs/pubapi/PrimitiveTypeDesc.java b/src/main/java/org/javacs/pubapi/PrimitiveTypeDesc.java deleted file mode 100644 index 81d1683..0000000 --- a/src/main/java/org/javacs/pubapi/PrimitiveTypeDesc.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import com.sun.tools.javac.util.StringUtils; -import java.io.Serializable; -import javax.lang.model.type.TypeKind; - -public class PrimitiveTypeDesc extends TypeDesc implements Serializable { - - private static final long serialVersionUID = 6051065543149129106L; - - public PrimitiveTypeDesc(TypeKind typeKind) { - super(typeKind); - if (!typeKind.isPrimitive() && typeKind != TypeKind.VOID) - throw new IllegalArgumentException("Only primitives or void accepted"); - } - - // This class has no fields, so the inherited hashCode and equals should do fine. - - @Override - public String toString() { - return StringUtils.toLowerCase(typeKind.toString()); - } -} diff --git a/src/main/java/org/javacs/pubapi/PubApi.java b/src/main/java/org/javacs/pubapi/PubApi.java deleted file mode 100644 index ff19686..0000000 --- a/src/main/java/org/javacs/pubapi/PubApi.java +++ /dev/null @@ -1,468 +0,0 @@ -/* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import static org.javacs.pubapi.Util.union; - -import com.sun.tools.javac.util.Assert; -import com.sun.tools.javac.util.StringUtils; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.lang.model.element.Modifier; - -public class PubApi implements Serializable { - - private static final long serialVersionUID = 5926627347801986850L; - - // Used to have Set here. Problem is that the objects are mutated during - // javac_state loading, causing them to change hash codes. We could probably - // change back to Set once javac_state loading is cleaned up. - public final Map<String, PubType> types = new HashMap<>(); - public final Map<String, PubVar> variables = new HashMap<>(); - public final Map<String, PubMethod> methods = new HashMap<>(); - - public PubApi() {} - - public PubApi( - Collection<PubType> types, - Collection<PubVar> variables, - Collection<PubMethod> methods) { - types.forEach(this::addPubType); - variables.forEach(this::addPubVar); - methods.forEach(this::addPubMethod); - } - - // Currently this is implemented as equality. This is far from optimal. It - // should preferably make sure that all previous methods are still available - // and no abstract methods are added. It should also be aware of inheritance - // of course. - public boolean isBackwardCompatibleWith(PubApi older) { - return equals(older); - } - - private static String typeLine(PubType type) { - if (type.fqName.isEmpty()) throw new RuntimeException("empty class name " + type); - return String.format("TYPE %s%s", asString(type.modifiers), type.fqName); - } - - private static String varLine(PubVar var) { - return String.format( - "VAR %s%s %s%s", - asString(var.modifiers), - TypeDesc.encodeAsString(var.type), - var.identifier, - var.getConstValue().map(v -> " = " + v).orElse("")); - } - - private static String methodLine(PubMethod method) { - return String.format( - "METHOD %s%s%s %s(%s)%s", - asString(method.modifiers), - method.typeParams.isEmpty() - ? "" - : ("<" - + method.typeParams - .stream() - .map(PubApiTypeParam::asString) - .collect(Collectors.joining(",")) - + "> "), - TypeDesc.encodeAsString(method.returnType), - method.identifier, - commaSeparated(method.paramTypes), - method.throwDecls.isEmpty() ? "" : " throws " + commaSeparated(method.throwDecls)); - } - - public List<String> asListOfStrings() { - List<String> lines = new ArrayList<>(); - - // Types - types.values() - .stream() - .sorted(Comparator.comparing(PubApi::typeLine)) - .forEach( - type -> { - lines.add(typeLine(type)); - for (String subline : type.pubApi.asListOfStrings()) - lines.add(" " + subline); - }); - - // Variables - variables.values().stream().map(PubApi::varLine).sorted().forEach(lines::add); - - // Methods - methods.values().stream().map(PubApi::methodLine).sorted().forEach(lines::add); - - return lines; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - PubApi other = (PubApi) obj; - return types.equals(other.types) - && variables.equals(other.variables) - && methods.equals(other.methods); - } - - @Override - public int hashCode() { - return types.keySet().hashCode() - ^ variables.keySet().hashCode() - ^ methods.keySet().hashCode(); - } - - private static String commaSeparated(List<TypeDesc> typeDescs) { - return typeDescs.stream().map(TypeDesc::encodeAsString).collect(Collectors.joining(",")); - } - - // Create space separated list of modifiers (with a trailing space) - private static String asString(Set<Modifier> modifiers) { - return modifiers.stream().map(mod -> mod + " ").sorted().collect(Collectors.joining()); - } - - // Used to combine class PubApis to package level PubApis - public static PubApi mergeTypes(PubApi api1, PubApi api2) { - Assert.check(api1.methods.isEmpty(), "Can only merge types."); - Assert.check(api2.methods.isEmpty(), "Can only merge types."); - Assert.check(api1.variables.isEmpty(), "Can only merge types."); - Assert.check(api2.variables.isEmpty(), "Can only merge types."); - PubApi merged = new PubApi(); - merged.types.putAll(api1.types); - merged.types.putAll(api2.types); - return merged; - } - - // Used for line-by-line parsing - private PubType lastInsertedType = null; - - private static final String MODIFIERS = - Stream.of(Modifier.values()) - .map(Modifier::name) - .map(StringUtils::toLowerCase) - .collect(Collectors.joining("|", "(", ")")); - - private static final Pattern MOD_PATTERN = Pattern.compile("(" + MODIFIERS + " )*"); - private static final Pattern METHOD_PATTERN = - Pattern.compile("(?<ret>.+?) (?<name>\\S+)\\((?<params>.*)\\)( throws (?<throws>.*))?"); - private static final Pattern VAR_PATTERN = - Pattern.compile( - "VAR (?<modifiers>(" - + MODIFIERS - + " )*)(?<type>.+?) (?<id>\\S+)( = (?<val>.*))?"); - private static final Pattern TYPE_PATTERN = - Pattern.compile("TYPE (?<modifiers>(" + MODIFIERS + " )*)(?<fullyQualified>\\S+)"); - - public void appendItem(String l) { - try { - if (l.startsWith(" ")) { - lastInsertedType.pubApi.appendItem(l.substring(2)); - return; - } - - if (l.startsWith("METHOD")) { - l = l.substring("METHOD ".length()); - Set<Modifier> modifiers = new HashSet<>(); - Matcher modMatcher = MOD_PATTERN.matcher(l); - if (modMatcher.find()) { - String modifiersStr = modMatcher.group(); - modifiers.addAll(parseModifiers(modifiersStr)); - l = l.substring(modifiersStr.length()); - } - List<PubApiTypeParam> typeParams = new ArrayList<>(); - if (l.startsWith("<")) { - int closingPos = findClosingTag(l, 0); - String str = l.substring(1, closingPos); - l = l.substring(closingPos + 1); - typeParams.addAll(parseTypeParams(splitOnTopLevelCommas(str))); - } - Matcher mm = METHOD_PATTERN.matcher(l); - if (!mm.matches()) - throw new AssertionError( - "Could not parse return type, identifier, parameter types or throws declaration of method: " - + l); - - List<String> params = splitOnTopLevelCommas(mm.group("params")); - String th = Optional.ofNullable(mm.group("throws")).orElse(""); - List<String> throwz = splitOnTopLevelCommas(th); - PubMethod m = - new PubMethod( - modifiers, - typeParams, - TypeDesc.decodeString(mm.group("ret")), - mm.group("name"), - parseTypeDescs(params), - parseTypeDescs(throwz)); - addPubMethod(m); - return; - } - - Matcher vm = VAR_PATTERN.matcher(l); - if (vm.matches()) { - addPubVar( - new PubVar( - parseModifiers(vm.group("modifiers")), - TypeDesc.decodeString(vm.group("type")), - vm.group("id"), - vm.group("val"))); - return; - } - - Matcher tm = TYPE_PATTERN.matcher(l); - if (tm.matches()) { - addPubType( - new PubType( - parseModifiers(tm.group("modifiers")), - tm.group("fullyQualified"), - new PubApi())); - return; - } - - throw new AssertionError("No matching line pattern."); - } catch (Throwable e) { - throw new AssertionError("Could not parse API line: " + l, e); - } - } - - public void addPubType(PubType t) { - types.put(t.fqName, t); - lastInsertedType = t; - } - - public void addPubVar(PubVar v) { - variables.put(v.identifier, v); - } - - public void addPubMethod(PubMethod m) { - methods.put(m.asSignatureString(), m); - } - - private static List<TypeDesc> parseTypeDescs(List<String> strs) { - return strs.stream().map(TypeDesc::decodeString).collect(Collectors.toList()); - } - - private static List<PubApiTypeParam> parseTypeParams(List<String> strs) { - return strs.stream().map(PubApi::parseTypeParam).collect(Collectors.toList()); - } - - // Parse a type parameter string. Example input: - // identifier - // identifier extends Type (& Type)* - private static PubApiTypeParam parseTypeParam(String typeParamString) { - int extPos = typeParamString.indexOf(" extends "); - if (extPos == -1) return new PubApiTypeParam(typeParamString, Collections.emptyList()); - String identifier = typeParamString.substring(0, extPos); - String rest = typeParamString.substring(extPos + " extends ".length()); - List<TypeDesc> bounds = parseTypeDescs(splitOnTopLevelChars(rest, '&')); - return new PubApiTypeParam(identifier, bounds); - } - - public Set<Modifier> parseModifiers(String modifiers) { - if (modifiers == null) return Collections.emptySet(); - return Stream.of(modifiers.split(" ")) - .map(String::trim) - .map(StringUtils::toUpperCase) - .filter(s -> !s.isEmpty()) - .map(Modifier::valueOf) - .collect(Collectors.toSet()); - } - - // Find closing tag of the opening tag at the given 'pos'. - private static int findClosingTag(String l, int pos) { - while (true) { - pos = pos + 1; - if (l.charAt(pos) == '>') return pos; - if (l.charAt(pos) == '<') pos = findClosingTag(l, pos); - } - } - - public List<String> splitOnTopLevelCommas(String s) { - return splitOnTopLevelChars(s, ','); - } - - public static List<String> splitOnTopLevelChars(String s, char split) { - if (s.isEmpty()) return Collections.emptyList(); - List<String> result = new ArrayList<>(); - StringBuilder buf = new StringBuilder(); - int depth = 0; - for (char c : s.toCharArray()) { - if (c == split && depth == 0) { - result.add(buf.toString().trim()); - buf = new StringBuilder(); - } else { - if (c == '<') depth++; - if (c == '>') depth--; - buf.append(c); - } - } - result.add(buf.toString().trim()); - return result; - } - - public boolean isEmpty() { - return types.isEmpty() && variables.isEmpty() && methods.isEmpty(); - } - - // Used for descriptive debug messages when figuring out what triggers - // recompilation. - public List<String> diff(PubApi prevApi) { - return diff("", prevApi); - } - - private List<String> diff(String scopePrefix, PubApi prevApi) { - - List<String> diffs = new ArrayList<>(); - - for (String typeKey : union(types.keySet(), prevApi.types.keySet())) { - PubType type = types.get(typeKey); - PubType prevType = prevApi.types.get(typeKey); - if (prevType == null) { - diffs.add("Type " + scopePrefix + typeKey + " was added"); - } else if (type == null) { - diffs.add("Type " + scopePrefix + typeKey + " was removed"); - } else { - // Check modifiers - if (!type.modifiers.equals(prevType.modifiers)) { - diffs.add( - "Modifiers for type " - + scopePrefix - + typeKey - + " changed from " - + prevType.modifiers - + " to " - + type.modifiers); - } - - // Recursively check types pub API - diffs.addAll(type.pubApi.diff(prevType.pubApi)); - } - } - - for (String varKey : union(variables.keySet(), prevApi.variables.keySet())) { - PubVar var = variables.get(varKey); - PubVar prevVar = prevApi.variables.get(varKey); - if (prevVar == null) { - diffs.add("Variable " + scopePrefix + varKey + " was added"); - } else if (var == null) { - diffs.add("Variable " + scopePrefix + varKey + " was removed"); - } else { - if (!var.modifiers.equals(prevVar.modifiers)) { - diffs.add( - "Modifiers for var " - + scopePrefix - + varKey - + " changed from " - + prevVar.modifiers - + " to " - + var.modifiers); - } - if (!var.type.equals(prevVar.type)) { - diffs.add( - "Type of " - + scopePrefix - + varKey - + " changed from " - + prevVar.type - + " to " - + var.type); - } - if (!var.getConstValue().equals(prevVar.getConstValue())) { - diffs.add( - "Const value of " - + scopePrefix - + varKey - + " changed from " - + prevVar.getConstValue().orElse("<none>") - + " to " - + var.getConstValue().orElse("<none>")); - } - } - } - - for (String methodKey : union(methods.keySet(), prevApi.methods.keySet())) { - PubMethod method = methods.get(methodKey); - PubMethod prevMethod = prevApi.methods.get(methodKey); - if (prevMethod == null) { - diffs.add("Method " + scopePrefix + methodKey + " was added"); - } else if (method == null) { - diffs.add("Method " + scopePrefix + methodKey + " was removed"); - } else { - if (!method.modifiers.equals(prevMethod.modifiers)) { - diffs.add( - "Modifiers for method " - + scopePrefix - + methodKey - + " changed from " - + prevMethod.modifiers - + " to " - + method.modifiers); - } - if (!method.typeParams.equals(prevMethod.typeParams)) { - diffs.add( - "Type parameters for method " - + scopePrefix - + methodKey - + " changed from " - + prevMethod.typeParams - + " to " - + method.typeParams); - } - if (!method.throwDecls.equals(prevMethod.throwDecls)) { - diffs.add( - "Throw decl for method " - + scopePrefix - + methodKey - + " changed from " - + prevMethod.throwDecls - + " to " - + " to " - + method.throwDecls); - } - } - } - - return diffs; - } - - public String toString() { - return String.format( - "%s[types: %s, variables: %s, methods: %s]", - getClass().getSimpleName(), types.values(), variables.values(), methods.values()); - } -} diff --git a/src/main/java/org/javacs/pubapi/PubApiTypeParam.java b/src/main/java/org/javacs/pubapi/PubApiTypeParam.java deleted file mode 100644 index 0b7139e..0000000 --- a/src/main/java/org/javacs/pubapi/PubApiTypeParam.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import java.io.Serializable; -import java.util.List; -import java.util.stream.Collectors; - -public class PubApiTypeParam implements Serializable { - - private static final long serialVersionUID = 8899204612014329162L; - - private final String identifier; - private final List<TypeDesc> bounds; - - public PubApiTypeParam(String identifier, List<TypeDesc> bounds) { - this.identifier = identifier; - this.bounds = bounds; - } - - @Override - public boolean equals(Object obj) { - if (getClass() != obj.getClass()) return false; - PubApiTypeParam other = (PubApiTypeParam) obj; - return identifier.equals(other.identifier) && bounds.equals(other.bounds); - } - - @Override - public int hashCode() { - return identifier.hashCode() ^ bounds.hashCode(); - } - - public String asString() { - if (bounds.isEmpty()) return identifier; - String boundsStr = - bounds.stream().map(TypeDesc::encodeAsString).collect(Collectors.joining(" & ")); - return identifier + " extends " + boundsStr; - } - - @Override - public String toString() { - return String.format( - "%s[id: %s, bounds: %s]", getClass().getSimpleName(), identifier, bounds); - } -} diff --git a/src/main/java/org/javacs/pubapi/PubMethod.java b/src/main/java/org/javacs/pubapi/PubMethod.java deleted file mode 100644 index c27a561..0000000 --- a/src/main/java/org/javacs/pubapi/PubMethod.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import java.io.Serializable; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import javax.lang.model.element.Modifier; - -public class PubMethod implements Serializable { - - private static final long serialVersionUID = -7813050194553446243L; - - Set<Modifier> modifiers; - List<PubApiTypeParam> typeParams; - TypeDesc returnType; - String identifier; - List<TypeDesc> paramTypes; - List<TypeDesc> throwDecls; - - public PubMethod( - Set<Modifier> modifiers, - List<PubApiTypeParam> typeParams, - TypeDesc returnType, - String identifier, - List<TypeDesc> paramTypes, - List<TypeDesc> throwDecls) { - this.modifiers = modifiers; - this.typeParams = typeParams; - this.returnType = returnType; - this.identifier = identifier; - this.paramTypes = paramTypes; - this.throwDecls = throwDecls; - } - - // We need to include return type and type parameters to be sure to have - // different values for different methods. (A method can be overloaded with - // the only difference being the upper bound of the return type.) - public String asSignatureString() { - StringBuilder sb = new StringBuilder(); - - // <A extends String, Serializable, B extends List> - if (typeParams.size() > 0) { - sb.append( - typeParams - .stream() - .map(PubApiTypeParam::asString) - .collect(Collectors.joining(",", "<", "> "))); - } - sb.append(TypeDesc.encodeAsString(returnType)); - sb.append(" "); - sb.append(identifier); - sb.append("("); - sb.append( - paramTypes.stream().map(TypeDesc::encodeAsString).collect(Collectors.joining(","))); - sb.append(")"); - return sb.toString(); - } - - @Override - public boolean equals(Object obj) { - if (getClass() != obj.getClass()) return false; - PubMethod other = (PubMethod) obj; - return modifiers.equals(other.modifiers) - && typeParams.equals(other.typeParams) - && returnType.equals(other.returnType) - && identifier.equals(other.identifier) - && paramTypes.equals(other.paramTypes) - && throwDecls.equals(other.throwDecls); - } - - @Override - public int hashCode() { - return modifiers.hashCode() - ^ typeParams.hashCode() - ^ returnType.hashCode() - ^ identifier.hashCode() - ^ paramTypes.hashCode() - ^ throwDecls.hashCode(); - } - - public String toString() { - return String.format( - "%s[modifiers: %s, typeParams: %s, retType: %s, identifier: %s, params: %s, throws: %s]", - getClass().getSimpleName(), - modifiers, - typeParams, - returnType, - identifier, - paramTypes, - throwDecls); - } -} diff --git a/src/main/java/org/javacs/pubapi/PubType.java b/src/main/java/org/javacs/pubapi/PubType.java deleted file mode 100644 index 67776f6..0000000 --- a/src/main/java/org/javacs/pubapi/PubType.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import java.io.Serializable; -import java.util.Set; -import javax.lang.model.element.Modifier; - -public class PubType implements Serializable { - - private static final long serialVersionUID = -7423416049253889793L; - - public final Set<Modifier> modifiers; - public final String fqName; - public final PubApi pubApi; - - public PubType(Set<Modifier> modifiers, String fqName, PubApi pubApi) { - this.modifiers = modifiers; - this.fqName = fqName; - this.pubApi = pubApi; - } - - public String getFqName() { - return fqName.toString(); - } - - @Override - public boolean equals(Object obj) { - if (getClass() != obj.getClass()) return false; - PubType other = (PubType) obj; - return modifiers.equals(other.modifiers) - && fqName.equals(other.fqName) - && pubApi.equals(other.pubApi); - } - - @Override - public int hashCode() { - return modifiers.hashCode() ^ fqName.hashCode() ^ pubApi.hashCode(); - } - - @Override - public String toString() { - return String.format( - "%s[modifiers: %s, fqName: %s, pubApi: %s]", - getClass().getSimpleName(), modifiers, fqName, pubApi); - } -} diff --git a/src/main/java/org/javacs/pubapi/PubVar.java b/src/main/java/org/javacs/pubapi/PubVar.java deleted file mode 100644 index 2221230..0000000 --- a/src/main/java/org/javacs/pubapi/PubVar.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import java.io.Serializable; -import java.util.Optional; -import java.util.Set; -import javax.lang.model.element.Modifier; - -public class PubVar implements Serializable { - - private static final long serialVersionUID = 5806536061153374575L; - - public final Set<Modifier> modifiers; - public final TypeDesc type; - public final String identifier; - private final String constValue; - - public PubVar(Set<Modifier> modifiers, TypeDesc type, String identifier, String constValue) { - this.modifiers = modifiers; - this.type = type; - this.identifier = identifier; - this.constValue = constValue; - } - - public String getIdentifier() { - return identifier; - } - - @Override - public boolean equals(Object obj) { - if (getClass() != obj.getClass()) return false; - PubVar other = (PubVar) obj; - return modifiers.equals(other.modifiers) - && type.equals(other.type) - && identifier.equals(other.identifier) - && getConstValue().equals(other.getConstValue()); - } - - @Override - public int hashCode() { - return modifiers.hashCode() - ^ type.hashCode() - ^ identifier.hashCode() - ^ getConstValue().hashCode(); - } - - public String toString() { - return String.format( - "%s[modifiers: %s, type: %s, identifier: %s, constValue: %s]", - getClass().getSimpleName(), modifiers, type, identifier, constValue); - } - - public Optional<String> getConstValue() { - return Optional.ofNullable(constValue); - } -} diff --git a/src/main/java/org/javacs/pubapi/PubapiVisitor.java b/src/main/java/org/javacs/pubapi/PubapiVisitor.java deleted file mode 100644 index e3a0f17..0000000 --- a/src/main/java/org/javacs/pubapi/PubapiVisitor.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import static javax.lang.model.element.Modifier.PRIVATE; - -import com.sun.tools.javac.code.Symbol.ClassSymbol; -import java.util.List; -import java.util.stream.Collectors; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.TypeParameterElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.ElementScanner8; - -/** - * Utility class that constructs a textual representation of the public api of a class. - * - * <p><b>This is NOT part of any supported API. If you write code that depends on this, you do so at - * your own risk. This code and its internal interfaces are subject to change or deletion without - * notice.</b> - */ -public class PubapiVisitor extends ElementScanner8<Void, Void> { - - private PubApi collectedApi = new PubApi(); - - private boolean isNonPrivate(Element e) { - return !e.getModifiers().contains(PRIVATE); - } - - @Override - public Void visitType(TypeElement e, Void p) { - if (isNonPrivate(e)) { - PubApi prevApi = collectedApi; - collectedApi = new PubApi(); - super.visitType(e, p); - if (!isAnonymous(e)) { - String name = ((ClassSymbol) e).flatname.toString(); - PubType t = - new PubType( - e.getModifiers(), - name, - //e.getQualifiedName().toString(), - collectedApi); - prevApi.types.put(t.fqName, t); - } - collectedApi = prevApi; - } - return null; - } - - private boolean isAnonymous(TypeElement e) { - return e.getQualifiedName().length() == 0; - } - - private static String encodeChar(int c) { - return String.format("\\u%04x", c); - } - - @Override - public Void visitVariable(VariableElement e, Void p) { - if (isNonPrivate(e)) { - Object constVal = e.getConstantValue(); - String constValStr = null; - // TODO: This doesn't seem to be entirely accurate. What if I change - // from, say, 0 to 0L? (And the field is public final static so that - // it could get inlined.) - if (constVal != null) { - if (e.asType().toString().equals("char")) { - // What type is 'value'? Is it already a char? - char c = constVal.toString().charAt(0); - constValStr = "'" + encodeChar(c) + "'"; - } else { - constValStr = - constVal.toString() - .chars() - .mapToObj(PubapiVisitor::encodeChar) - .collect(Collectors.joining("", "\"", "\"")); - } - } - - PubVar v = - new PubVar( - e.getModifiers(), - TypeDesc.fromType(e.asType()), - e.toString(), - constValStr); - collectedApi.variables.put(v.identifier, v); - } - - // Safe to not recurse here, because the only thing - // to visit here is the constructor of a variable declaration. - // If it happens to contain an anonymous inner class (which it might) - // then this class is never visible outside of the package anyway, so - // we are allowed to ignore it here. - return null; - } - - @Override - public Void visitExecutable(ExecutableElement e, Void p) { - if (isNonPrivate(e)) { - PubMethod m = - new PubMethod( - e.getModifiers(), - getTypeParameters(e.getTypeParameters()), - TypeDesc.fromType(e.getReturnType()), - e.getSimpleName().toString(), - getTypeDescs(getParamTypes(e)), - getTypeDescs(e.getThrownTypes())); - collectedApi.methods.put(m.asSignatureString(), m); - } - return null; - } - - private List<PubApiTypeParam> getTypeParameters(List<? extends TypeParameterElement> elements) { - return elements.stream() - .map( - e -> - new PubApiTypeParam( - e.getSimpleName().toString(), getTypeDescs(e.getBounds()))) - .collect(Collectors.toList()); - } - - private List<TypeMirror> getParamTypes(ExecutableElement e) { - return e.getParameters().stream().map(VariableElement::asType).collect(Collectors.toList()); - } - - private List<TypeDesc> getTypeDescs(List<? extends TypeMirror> list) { - return list.stream().map(TypeDesc::fromType).collect(Collectors.toList()); - } - - public PubApi getCollectedPubApi() { - return collectedApi; - } -} diff --git a/src/main/java/org/javacs/pubapi/ReferenceTypeDesc.java b/src/main/java/org/javacs/pubapi/ReferenceTypeDesc.java deleted file mode 100644 index 3aac9e5..0000000 --- a/src/main/java/org/javacs/pubapi/ReferenceTypeDesc.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import java.io.Serializable; -import javax.lang.model.type.TypeKind; - -public class ReferenceTypeDesc extends TypeDesc implements Serializable { - - private static final long serialVersionUID = 3357616754544796372L; - - // Example: "java.util.Vector<java.lang.String>" - String javaType; - - public ReferenceTypeDesc(String javaType) { - super(TypeKind.DECLARED); - this.javaType = javaType; - } - - @Override - public boolean equals(Object obj) { - if (!super.equals(obj)) return false; - return javaType.equals(((ReferenceTypeDesc) obj).javaType); - } - - @Override - public int hashCode() { - return super.hashCode() ^ javaType.hashCode(); - } - - @Override - public String toString() { - return String.format("%s[type: %s]", getClass().getSimpleName(), javaType); - } -} diff --git a/src/main/java/org/javacs/pubapi/TypeDesc.java b/src/main/java/org/javacs/pubapi/TypeDesc.java deleted file mode 100644 index 613f341..0000000 --- a/src/main/java/org/javacs/pubapi/TypeDesc.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import com.sun.tools.javac.code.Type.ClassType; -import com.sun.tools.javac.util.StringUtils; -import java.io.Serializable; -import javax.lang.model.type.ArrayType; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.ErrorType; -import javax.lang.model.type.NoType; -import javax.lang.model.type.PrimitiveType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.TypeVariable; -import javax.lang.model.type.TypeVisitor; -import javax.lang.model.util.SimpleTypeVisitor8; - -public abstract class TypeDesc implements Serializable { - - private static final long serialVersionUID = -8201634143915519172L; - - TypeKind typeKind; - - public TypeDesc(TypeKind typeKind) { - this.typeKind = typeKind; - } - - public static TypeDesc decodeString(String s) { - s = s.trim(); - if (s.endsWith("[]")) { - String componentPart = s.substring(0, s.length() - 2); - return new ArrayTypeDesc(decodeString(componentPart)); - } - - if (s.startsWith("#")) return new TypeVarTypeDesc(s.substring(1)); - - if (s.matches("boolean|byte|char|double|float|int|long|short|void")) { - TypeKind tk = TypeKind.valueOf(StringUtils.toUpperCase(s)); - return new PrimitiveTypeDesc(tk); - } - - return new ReferenceTypeDesc(s); - } - - public static String encodeAsString(TypeDesc td) { - if (td.typeKind.isPrimitive() || td.typeKind == TypeKind.VOID) - return StringUtils.toLowerCase(td.typeKind.toString()); - - if (td.typeKind == TypeKind.ARRAY) - return encodeAsString(((ArrayTypeDesc) td).compTypeDesc) + "[]"; - - if (td.typeKind == TypeKind.TYPEVAR) return "#" + ((TypeVarTypeDesc) td).identifier; - - if (td.typeKind == TypeKind.DECLARED) return ((ReferenceTypeDesc) td).javaType.toString(); - - throw new AssertionError("Unhandled type: " + td.typeKind); - } - - public static TypeDesc fromType(TypeMirror type) { - TypeVisitor<TypeDesc, Void> v = - new SimpleTypeVisitor8<TypeDesc, Void>() { - @Override - public TypeDesc visitArray(ArrayType t, Void p) { - return new ArrayTypeDesc(t.getComponentType().accept(this, p)); - } - - @Override - public TypeDesc visitDeclared(DeclaredType t, Void p) { - return new ReferenceTypeDesc(((ClassType) t).tsym.flatName().toString()); - } - - @Override - public TypeDesc visitNoType(NoType t, Void p) { - return new PrimitiveTypeDesc(TypeKind.VOID); - } - - @Override - public TypeDesc visitTypeVariable(TypeVariable t, Void p) { - return new TypeVarTypeDesc(t.toString()); - } - - @Override - public TypeDesc visitPrimitive(PrimitiveType t, Void p) { - return new PrimitiveTypeDesc(t.getKind()); - } - - @Override - public TypeDesc visitError(ErrorType t, Void p) { - return new ReferenceTypeDesc("<error type>"); - } - }; - - TypeDesc td = v.visit(type); - if (td == null) - throw new AssertionError( - "Unhandled type mirror: " + type + " (" + type.getClass() + ")"); - return td; - } - - @Override - public boolean equals(Object obj) { - if (getClass() != obj.getClass()) return false; - return typeKind.equals(((TypeDesc) obj).typeKind); - } - - @Override - public int hashCode() { - return typeKind.hashCode(); - } -} diff --git a/src/main/java/org/javacs/pubapi/TypeVarTypeDesc.java b/src/main/java/org/javacs/pubapi/TypeVarTypeDesc.java deleted file mode 100644 index 44cf997..0000000 --- a/src/main/java/org/javacs/pubapi/TypeVarTypeDesc.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import java.io.Serializable; -import javax.lang.model.type.TypeKind; - -public class TypeVarTypeDesc extends TypeDesc implements Serializable { - - private static final long serialVersionUID = 3357616754544796373L; - - String identifier; // Example: "T" - - public TypeVarTypeDesc(String identifier) { - super(TypeKind.TYPEVAR); - this.identifier = identifier; - } - - @Override - public boolean equals(Object obj) { - if (!super.equals(obj)) return false; - return identifier.equals(((TypeVarTypeDesc) obj).identifier); - } - - @Override - public int hashCode() { - return super.hashCode() ^ identifier.hashCode(); - } - - @Override - public String toString() { - return String.format("%s[identifier: %s]", getClass().getSimpleName(), identifier); - } -} diff --git a/src/main/java/org/javacs/pubapi/Util.java b/src/main/java/org/javacs/pubapi/Util.java deleted file mode 100644 index 17a76b8..0000000 --- a/src/main/java/org/javacs/pubapi/Util.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package org.javacs.pubapi; - -import java.io.File; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.function.Function; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Utilities. - * - * <p><b>This is NOT part of any supported API. If you write code that depends on this, you do so at - * your own risk. This code and its internal interfaces are subject to change or deletion without - * notice.</b> - */ -public class Util { - - public static String toFileSystemPath(String pkgId) { - if (pkgId == null || pkgId.length() == 0) return null; - String pn; - if (pkgId.charAt(0) == ':') { - // When the module is the default empty module. - // Do not prepend the module directory, because there is none. - // Thus :java.foo.bar translates to java/foo/bar (or \) - pn = pkgId.substring(1).replace('.', File.separatorChar); - } else { - // There is a module. Thus jdk.base:java.foo.bar translates - // into jdk.base/java/foo/bar - int cp = pkgId.indexOf(':'); - String mn = pkgId.substring(0, cp); - pn = mn + File.separatorChar + pkgId.substring(cp + 1).replace('.', File.separatorChar); - } - return pn; - } - - public static String justPackageName(String pkgName) { - int c = pkgName.indexOf(":"); - if (c == -1) - throw new IllegalArgumentException("Expected ':' in package name (" + pkgName + ")"); - return pkgName.substring(c + 1); - } - - public static String extractStringOption(String opName, String s) { - return extractStringOption(opName, s, null); - } - - public static String extractStringOption(String opName, String s, String deflt) { - int p = s.indexOf(opName + "="); - if (p == -1) return deflt; - p += opName.length() + 1; - int pe = s.indexOf(',', p); - if (pe == -1) pe = s.length(); - return s.substring(p, pe); - } - - public static boolean extractBooleanOption(String opName, String s, boolean deflt) { - String str = extractStringOption(opName, s); - return "true".equals(str) ? true : "false".equals(str) ? false : deflt; - } - - public static int extractIntOption(String opName, String s) { - return extractIntOption(opName, s, 0); - } - - public static int extractIntOption(String opName, String s, int deflt) { - int p = s.indexOf(opName + "="); - if (p == -1) return deflt; - p += opName.length() + 1; - int pe = s.indexOf(',', p); - if (pe == -1) pe = s.length(); - int v = 0; - try { - v = Integer.parseInt(s.substring(p, pe)); - } catch (Exception e) { - } - return v; - } - - /** - * Extract the package name from a fully qualified class name. - * - * <p>Example: Given "pkg.subpkg.A" this method returns ":pkg.subpkg". Given "C" this method - * returns ":". - * - * @returns package name of the given class name - */ - public static String pkgNameOfClassName(String fqClassName) { - int i = fqClassName.lastIndexOf('.'); - String pkg = i == -1 ? "" : fqClassName.substring(0, i); - return ":" + pkg; - } - - /** - * Clean out unwanted sub options supplied inside a primary option. For example to only had - * portfile remaining from: settings="--server:id=foo,portfile=bar" do settings = - * cleanOptions("--server:",Util.set("-portfile"),settings); now settings equals - * "--server:portfile=bar" - * - * @param allowedSubOptions A set of the allowed sub options, id portfile etc. - * @param s The option settings string. - */ - public static String cleanSubOptions(Set<String> allowedSubOptions, String s) { - StringBuilder sb = new StringBuilder(); - StringTokenizer st = new StringTokenizer(s, ","); - while (st.hasMoreTokens()) { - String o = st.nextToken(); - int p = o.indexOf('='); - if (p > 0) { - String key = o.substring(0, p); - String val = o.substring(p + 1); - if (allowedSubOptions.contains(key)) { - if (sb.length() > 0) sb.append(','); - sb.append(key + "=" + val); - } - } - } - return sb.toString(); - } - - /** Convenience method to create a set with strings. */ - public static Set<String> set(String... ss) { - Set<String> set = new HashSet<>(); - set.addAll(Arrays.asList(ss)); - return set; - } - - /** - * Normalize windows drive letter paths to upper case to enable string comparison. - * - * @param file File name to normalize - * @return The normalized string if file has a drive letter at the beginning, otherwise the - * original string. - */ - public static String normalizeDriveLetter(String file) { - if (file.length() > 2 && file.charAt(1) == ':') { - return Character.toUpperCase(file.charAt(0)) + file.substring(1); - } else if (file.length() > 3 && file.charAt(0) == '*' && file.charAt(2) == ':') { - // Handle a wildcard * at the beginning of the string. - return file.substring(0, 1) + Character.toUpperCase(file.charAt(1)) + file.substring(2); - } - return file; - } - - /** Locate the setting for the server properties. */ - public static String findServerSettings(String[] args) { - for (String s : args) { - if (s.startsWith("--server:")) { - return s; - } - } - return null; - } - - public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) { - Set<E> union = new HashSet<>(); - union.addAll(s1); - union.addAll(s2); - return union; - } - - public static <E> Set<E> subtract(Set<? extends E> orig, Set<? extends E> toSubtract) { - Set<E> difference = new HashSet<>(orig); - difference.removeAll(toSubtract); - return difference; - } - - public static String getStackTrace(Throwable t) { - StringWriter sw = new StringWriter(); - t.printStackTrace(new PrintWriter(sw)); - return sw.toString(); - } - - // TODO: Remove when refactoring from java.io.File to java.nio.file.Path. - public static File pathToFile(Path path) { - return path == null ? null : path.toFile(); - } - - public static <E> Set<E> intersection(Collection<? extends E> c1, Collection<? extends E> c2) { - Set<E> intersection = new HashSet<E>(c1); - intersection.retainAll(c2); - return intersection; - } - - public static <I, T> Map<I, T> indexBy( - Collection<? extends T> c, Function<? super T, ? extends I> indexFunction) { - return c.stream().collect(Collectors.<T, I, T>toMap(indexFunction, o -> o)); - } - - public static String fileSuffix(Path file) { - String fileNameStr = file.getFileName().toString(); - int dotIndex = fileNameStr.indexOf('.'); - return dotIndex == -1 ? "" : fileNameStr.substring(dotIndex); - } - - public static Stream<String> getLines(String str) { - return str.isEmpty() - ? Stream.empty() - : Stream.of(str.split(Pattern.quote(System.lineSeparator()))); - } -} diff --git a/src/test/java/org/javacs/ArtifactTest.java b/src/test/java/org/javacs/ArtifactTest.java index 02dbf60..e5c9297 100644 --- a/src/test/java/org/javacs/ArtifactTest.java +++ b/src/test/java/org/javacs/ArtifactTest.java @@ -13,8 +13,7 @@ public class ArtifactTest { @Test public void parseLong() { - assertThat( - Artifact.parse("foo:bar:jar:1:compile"), equalTo(new Artifact("foo", "bar", "1"))); + assertThat(Artifact.parse("foo:bar:jar:1:compile"), equalTo(new Artifact("foo", "bar", "1"))); } @Test(expected = IllegalArgumentException.class) diff --git a/src/test/java/org/javacs/ClassPathIndexTest.java b/src/test/java/org/javacs/ClassPathIndexTest.java deleted file mode 100644 index 99a3d4a..0000000 --- a/src/test/java/org/javacs/ClassPathIndexTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import com.google.common.reflect.ClassPath; -import java.io.IOException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Optional; -import org.junit.Test; - -public class ClassPathIndexTest { - - @Test - public void createEmptyLoader() throws ClassNotFoundException { - ClassLoader emptyClassLoader = ClassPathIndex.parentClassLoader(); - - assertThat(emptyClassLoader.loadClass("java.util.ArrayList"), not(nullValue())); - - try { - Class<?> found = emptyClassLoader.loadClass("com.google.common.collect.Lists"); - - fail("Found " + found); - } catch (ClassNotFoundException e) { - // OK - } - } - - @Test - public void java8Platform() throws IOException { - String javaHome = - Paths.get("./src/test/test-platforms/jdk8-home").toAbsolutePath().toString(); - URL[] resources = ClassPathIndex.java8Platform(javaHome); - assertThat( - "found example.jar", - resources, - hasItemInArray(hasToString(containsString("rt.jar")))); - ClassPath classPath = ClassPath.from(new URLClassLoader(resources, null)); - assertThat( - classPath.getTopLevelClasses(), - hasItem(hasProperty("simpleName", equalTo("HelloWorld")))); - } - - @Test - public void java9Platform() throws IOException { - String javaHome = - Paths.get("./src/test/test-platforms/jdk9-home").toAbsolutePath().toString(); - URL[] resources = ClassPathIndex.java9Platform(javaHome); - assertThat( - "found java.compiler.jmod", - resources, - hasItemInArray(hasToString(containsString("java.compiler.jmod")))); - ClassPath classPath = ClassPath.from(new URLClassLoader(resources, null)); - assertThat( - classPath.getTopLevelClasses(), - hasItem(hasProperty("simpleName", equalTo("JavaCompiler")))); - } - - @Test - public void topLevelClasses() { - ClassPathIndex index = new ClassPathIndex(Collections.emptySet()); - Optional<ClassPath.ClassInfo> arrayList = - index.topLevelClasses() - .filter(c -> c.getName().equals("java.util.ArrayList")) - .findFirst(); - assertTrue("java.util.ArrayList is on the classpath", arrayList.isPresent()); - } -} diff --git a/src/test/java/org/javacs/ClassesTest.java b/src/test/java/org/javacs/ClassesTest.java new file mode 100644 index 0000000..323dea4 --- /dev/null +++ b/src/test/java/org/javacs/ClassesTest.java @@ -0,0 +1,70 @@ +package org.javacs; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import com.google.common.reflect.ClassPath; +import java.net.URI; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.util.Collections; +import org.junit.Test; + +public class ClassesTest { + + @Test + public void list() { + var jdk = Classes.jdkTopLevelClasses(); + assertThat(jdk.classes(), hasItem("java.util.List")); + assertThat(jdk.load("java.util.List"), not(nullValue())); + + var empty = Classes.classPathTopLevelClasses(Collections.emptySet()); + assertThat(empty.classes(), not(hasItem("java.util.List"))); + } + + @Test + public void arrayList() { + var jdk = Classes.jdkTopLevelClasses(); + assertThat(jdk.classes(), hasItem("java.util.ArrayList")); + assertThat(jdk.load("java.util.ArrayList"), not(nullValue())); + } + + @Test + public void platformClassPath() throws Exception { + var fs = FileSystems.getFileSystem(URI.create("jrt:/")); + var path = fs.getPath("/"); + Files.walk(path).forEach(p -> System.out.println(p)); + } + + @Test + public void loadMain() throws Exception { + var classes = ClassPath.from(this.getClass().getClassLoader()); + var found = classes.getTopLevelClasses("org.javacs"); + assertThat(found, hasItem(hasToString("org.javacs.Main"))); + + var main = found.stream().filter(c -> c.getName().equals("org.javacs.Main")).findFirst(); + assertTrue(main.isPresent()); + + var load = main.get().load(); + assertNotNull(load); + } + + void ancestors(ClassLoader classLoader) { + while (classLoader != null) { + System.out.println(classLoader.toString()); + classLoader = classLoader.getParent(); + } + } + + @Test + public void printAncestors() throws Exception { + System.out.println("This:"); + ancestors(this.getClass().getClassLoader()); + System.out.println("List:"); + ancestors(java.util.List.class.getClassLoader()); + System.out.println("System:"); + ancestors(ClassLoader.getSystemClassLoader()); + System.out.println("Platform:"); + ancestors(ClassLoader.getPlatformClassLoader()); + } +} diff --git a/src/test/java/org/javacs/CodeActionsTest.java b/src/test/java/org/javacs/CodeActionsTest.java deleted file mode 100644 index d2bbc61..0000000 --- a/src/test/java/org/javacs/CodeActionsTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; -import static org.junit.Assert.assertThat; - -import java.io.*; -import java.net.URI; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.eclipse.lsp4j.*; -import org.junit.Before; -import org.junit.Test; - -public class CodeActionsTest { - - private static List<Diagnostic> diagnostics = new ArrayList<>(); - - @Before - public void before() { - diagnostics.clear(); - } - - private static final JavaLanguageServer server = - LanguageServerFixture.getJavaLanguageServer( - LanguageServerFixture.DEFAULT_WORKSPACE_ROOT, diagnostics::add); - - @Test - public void addImport() { - List<String> titles = - commands("/org/javacs/example/MissingImport.java", 5, 14) - .stream() - .map(c -> c.getTitle()) - .collect(Collectors.toList()); - - assertThat(titles, hasItem("Import java.util.ArrayList")); - } - - @Test - public void missingImport() { - String message = - "cannot find symbol\n" - + " symbol: class ArrayList\n" - + " location: class org.javacs.MissingImport"; - - assertThat( - CodeActions.cannotFindSymbolClassName(message), equalTo(Optional.of("ArrayList"))); - } - - private List<? extends Command> commands(String file, int row, int column) { - URI uri = FindResource.uri(file); - TextDocumentIdentifier document = new TextDocumentIdentifier(uri.toString()); - - try { - InputStream in = Files.newInputStream(new File(uri).toPath()); - String content = - new BufferedReader(new InputStreamReader(in)) - .lines() - .collect(Collectors.joining("\n")); - TextDocumentItem open = new TextDocumentItem(); - - open.setText(content); - open.setUri(uri.toString()); - open.setLanguageId("java"); - - server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(open, content)); - server.getTextDocumentService() - .didSave(new DidSaveTextDocumentParams(document, content)); - - return diagnostics - .stream() - .filter(diagnostic -> includes(diagnostic.getRange(), row - 1, column - 1)) - .flatMap(diagnostic -> codeActionsAt(document, diagnostic)) - .collect(Collectors.toList()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private boolean includes(Range range, int line, int character) { - boolean startCondition = - range.getStart().getLine() < line - || (range.getStart().getLine() == line - && range.getStart().getCharacter() <= character); - boolean endCondition = - line < range.getEnd().getLine() - || (line == range.getEnd().getLine() - && character <= range.getEnd().getCharacter()); - - return startCondition && endCondition; - } - - private Stream<? extends Command> codeActionsAt( - TextDocumentIdentifier documentId, Diagnostic diagnostic) { - CodeActionParams params = - new CodeActionParams( - documentId, diagnostic.getRange(), new CodeActionContext(diagnostics)); - - return server.getTextDocumentService().codeAction(params).join().stream(); - } -} diff --git a/src/test/java/org/javacs/CodeLensTest.java b/src/test/java/org/javacs/CodeLensTest.java new file mode 100644 index 0000000..cfc3714 --- /dev/null +++ b/src/test/java/org/javacs/CodeLensTest.java @@ -0,0 +1,27 @@ +package org.javacs; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.concurrent.ExecutionException; +import org.eclipse.lsp4j.CodeLensParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.Test; + +public class CodeLensTest { + + private static final JavaLanguageServer server = LanguageServerFixture.getJavaLanguageServer(); + + @Test + public void codeLens() { + var file = "/org/javacs/example/HasTest.java"; + var uri = FindResource.uri(file); + var params = new CodeLensParams(new TextDocumentIdentifier(uri.toString())); + try { + var lenses = server.getTextDocumentService().codeLens(params).get(); + assertThat(lenses, not(empty())); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/org/javacs/CompilerProfiling.java b/src/test/java/org/javacs/CompilerProfiling.java deleted file mode 100644 index 01308ea..0000000 --- a/src/test/java/org/javacs/CompilerProfiling.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.javacs; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Paths; -import java.time.Duration; -import java.util.Collections; -import java.util.Optional; -import java.util.logging.Logger; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaFileObject; -import org.junit.Ignore; -import org.junit.Test; - -@Ignore -public class CompilerProfiling { - private static final Logger LOG = Logger.getLogger("main"); - - @Test - public void parsingSpeed() throws IOException, URISyntaxException { - URI file = FindResource.uri("/org/javacs/example/LargeFile.java"); - - for (int i = 0; i < 10; i++) { - Duration duration = compileLargeFile(file); - - LOG.info(duration.toString()); - } - } - - private Duration compileLargeFile(URI file) { - long start = System.nanoTime(); - JavacHolder compiler = - JavacHolder.create( - Collections.singleton(Paths.get("src/test/test-project/workspace/src")), - Collections.emptySet()); - DiagnosticCollector<JavaFileObject> result = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - long finish = System.nanoTime(); - - return Duration.ofNanos(finish - start); - } -} diff --git a/src/test/java/org/javacs/CompletionsBase.java b/src/test/java/org/javacs/CompletionsBase.java index d68bcb6..929e808 100644 --- a/src/test/java/org/javacs/CompletionsBase.java +++ b/src/test/java/org/javacs/CompletionsBase.java @@ -1,31 +1,28 @@ package org.javacs; +import com.google.gson.Gson; import java.io.IOException; -import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.eclipse.lsp4j.TextDocumentPositionParams; public class CompletionsBase { protected static final Logger LOG = Logger.getLogger("main"); protected Set<String> insertTemplate(String file, int row, int column) throws IOException { - List<? extends CompletionItem> items = items(file, row, column); + var items = items(file, row, column); return items.stream().map(CompletionsBase::itemInsertTemplate).collect(Collectors.toSet()); } static String itemInsertTemplate(CompletionItem i) { - String text = i.getInsertText(); + var text = i.getInsertText(); if (text == null) text = i.getLabel(); @@ -35,19 +32,29 @@ public class CompletionsBase { } protected Set<String> insertText(String file, int row, int column) throws IOException { - List<? extends CompletionItem> items = items(file, row, column); + var items = items(file, row, column); return items.stream().map(CompletionsBase::itemInsertText).collect(Collectors.toSet()); } - protected Map<String, Integer> insertCount(String file, int row, int column) - throws IOException { - List<? extends CompletionItem> items = items(file, row, column); - Map<String, Integer> result = new HashMap<>(); + protected Set<String> detail(String file, int row, int column) throws IOException { + var items = items(file, row, column); + var result = new HashSet<String>(); + for (var i : items) { + i.setData(new Gson().toJsonTree(i.getData())); + var resolved = resolve(i); + result.add(resolved.getDetail()); + } + return result; + } - for (CompletionItem each : items) { - String key = itemInsertText(each); - int count = result.getOrDefault(key, 0) + 1; + protected Map<String, Integer> insertCount(String file, int row, int column) throws IOException { + var items = items(file, row, column); + var result = new HashMap<String, Integer>(); + + for (var each : items) { + var key = itemInsertText(each); + var count = result.getOrDefault(key, 0) + 1; result.put(key, count); } @@ -56,7 +63,7 @@ public class CompletionsBase { } static String itemInsertText(CompletionItem i) { - String text = i.getInsertText(); + var text = i.getInsertText(); if (text == null) text = i.getLabel(); @@ -68,28 +75,24 @@ public class CompletionsBase { } protected Set<String> documentation(String file, int row, int column) throws IOException { - List<? extends CompletionItem> items = items(file, row, column); + var items = items(file, row, column); return items.stream() .flatMap( i -> { if (i.getDocumentation() != null) - return Stream.of(i.getDocumentation().trim()); + return Stream.of(i.getDocumentation().getRight().getValue().trim()); else return Stream.empty(); }) .collect(Collectors.toSet()); } - protected static final JavaLanguageServer server = - LanguageServerFixture.getJavaLanguageServer(); + protected static final JavaLanguageServer server = LanguageServerFixture.getJavaLanguageServer(); protected List<? extends CompletionItem> items(String file, int row, int column) { - URI uri = FindResource.uri(file); - TextDocumentPositionParams position = - new TextDocumentPositionParams( - new TextDocumentIdentifier(uri.toString()), - uri.toString(), - new Position(row - 1, column - 1)); + var uri = FindResource.uri(file); + var position = + new CompletionParams(new TextDocumentIdentifier(uri.toString()), new Position(row - 1, column - 1)); try { return server.getTextDocumentService().completion(position).get().getRight().getItems(); @@ -97,4 +100,12 @@ public class CompletionsBase { throw new RuntimeException(e); } } + + protected CompletionItem resolve(CompletionItem item) { + try { + return server.getTextDocumentService().resolveCompletionItem(item).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/test/java/org/javacs/CompletionsScopesTest.java b/src/test/java/org/javacs/CompletionsScopesTest.java index 4f43498..50b4759 100644 --- a/src/test/java/org/javacs/CompletionsScopesTest.java +++ b/src/test/java/org/javacs/CompletionsScopesTest.java @@ -5,223 +5,239 @@ import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; import java.io.IOException; -import java.util.Set; +import org.junit.Ignore; import org.junit.Test; public class CompletionsScopesTest extends CompletionsBase { @Test public void staticSub() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // Static method - Set<String> suggestions = insertText(file, 15, 14); + var suggestions = insertText(file, 15, 14); // Locals - assertThat(suggestions, hasItems("localVariables", "arguments")); + assertThat(suggestions, hasItems("testLocalVariables", "testArguments")); // Static methods in enclosing scopes assertThat(suggestions, hasItems("testStatic")); - assertThat(suggestions, hasItems("outerStaticMethod")); + assertThat(suggestions, hasItems("testOuterStaticMethod")); // Virtual methods in enclosing scopes assertThat(suggestions, not(hasItems("testInner"))); assertThat(suggestions, hasItems("test")); - assertThat(suggestions, not(hasItems("outerMethods"))); + // TODO this is not accessible + // assertThat(suggestions, not(hasItems("testOuterMethods"))); // Inherited static methods - assertThat(suggestions, hasItems("inheritedStaticMethod")); + assertThat(suggestions, hasItems("testInheritedStaticMethod")); // Inherited virtual methods - assertThat(suggestions, hasItems("inheritedMethods")); + assertThat(suggestions, hasItems("testInheritedMethods")); // this/super in enclosing scopes - assertThat(suggestions, hasItems("this", "super")); + assertThat(suggestions, hasItems("this")); } @Test public void staticSubThisSuper() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // StaticSub.this, StaticSub.super - assertThat(insertText(file, 37, 23), hasItems("this", "super")); + assertThat(insertText(file, 37, 23), hasItems("this")); // AutocompleteScopes.this, AutocompleteScopes.super - assertThat(insertText(file, 39, 32), not(hasItems("this", "super"))); + // TODO this is not accessible + // assertThat(insertText(file, 39, 32), not(hasItems("this"))); // Super.this, Super.super - assertThat(insertText(file, 41, 19), not(hasItems("this", "super"))); + // TODO this is not accessible + // assertThat(insertText(file, 41, 19), not(hasItems("this"))); } @Test public void staticSubInner() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // Static method - Set<String> suggestions = insertText(file, 45, 22); + var suggestions = insertText(file, 45, 22); // Locals - assertThat(suggestions, hasItems("localVariables", "arguments")); + assertThat(suggestions, hasItems("testLocalVariables", "testArguments")); // Static methods in enclosing scopes assertThat(suggestions, hasItems("testStatic")); - assertThat(suggestions, hasItems("outerStaticMethod")); + assertThat(suggestions, hasItems("testOuterStaticMethod")); // Virtual methods in enclosing scopes assertThat(suggestions, hasItems("testInner")); assertThat(suggestions, hasItems("test")); - assertThat(suggestions, not(hasItems("outerMethods"))); + // TODO this is not accessible + // assertThat(suggestions, not(hasItems("testOuterMethods"))); // Inherited static methods - assertThat(suggestions, hasItems("inheritedStaticMethod")); + assertThat(suggestions, hasItems("testInheritedStaticMethod")); // Inherited virtual methods - assertThat(suggestions, hasItems("inheritedMethods")); + assertThat(suggestions, hasItems("testInheritedMethods")); // this/super in enclosing scopes - assertThat(suggestions, hasItems("this", "super")); + assertThat(suggestions, hasItems("this")); } @Test public void staticSubInnerThisSuper() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // StaticSub.this, StaticSub.super - assertThat(insertText(file, 67, 31), hasItems("this", "super")); + assertThat(insertText(file, 67, 31), hasItems("this")); // AutocompleteScopes.this, AutocompleteScopes.super - assertThat(insertText(file, 69, 40), not(hasItems("this", "super"))); + // TODO this is not accessible + // assertThat(insertText(file, 69, 40), not(hasItems("this"))); // Super.this, Super.super - assertThat(insertText(file, 71, 27), not(hasItems("this", "super"))); + // TODO this is not accessible + // assertThat(insertText(file, 71, 27), not(hasItems("this"))); } @Test public void staticSubStaticMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // Static method - Set<String> suggestions = insertText(file, 78, 14); + var suggestions = insertText(file, 78, 14); // Locals - assertThat(suggestions, hasItems("localVariables", "arguments")); + assertThat(suggestions, hasItems("testLocalVariables", "testArguments")); // Static methods in enclosing scopes assertThat(suggestions, hasItems("testStatic")); - assertThat(suggestions, hasItems("outerStaticMethod")); + assertThat(suggestions, hasItems("testOuterStaticMethod")); // Virtual methods in enclosing scopes assertThat(suggestions, not(hasItems("testInner"))); assertThat(suggestions, not(hasItems("test"))); - assertThat(suggestions, not(hasItems("outerMethods"))); + assertThat(suggestions, not(hasItems("testOuterMethods"))); // Inherited static methods - assertThat(suggestions, hasItems("inheritedStaticMethod")); + assertThat(suggestions, hasItems("testInheritedStaticMethod")); // Inherited virtual methods - assertThat(suggestions, not(hasItems("inheritedMethods"))); + assertThat(suggestions, not(hasItems("testInheritedMethods"))); // this/super in enclosing scopes - assertThat(suggestions, not(hasItems("this", "super"))); + // TODO this is not accessible + // assertThat(suggestions, not(hasItems("this"))); } + // TODO this is not accessible + @Ignore @Test public void staticSubStaticMethodThisSuper() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // StaticSub.this, StaticSub.super - assertThat(insertText(file, 100, 23), not(hasItems("this", "super"))); + assertThat(insertText(file, 100, 23), not(hasItems("this"))); // AutocompleteScopes.this, AutocompleteScopes.super - assertThat(insertText(file, 102, 32), not(hasItems("this", "super"))); + assertThat(insertText(file, 102, 32), not(hasItems("this"))); // Super.this, Super.super - assertThat(insertText(file, 104, 19), not(hasItems("this", "super"))); + assertThat(insertText(file, 104, 19), not(hasItems("this"))); } @Test public void staticSubStaticMethodInner() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // Static method - Set<String> suggestions = insertText(file, 108, 22); + var suggestions = insertText(file, 108, 22); // Locals - assertThat(suggestions, hasItems("localVariables", "arguments")); + assertThat(suggestions, hasItems("testLocalVariables", "testArguments")); // Static methods in enclosing scopes assertThat(suggestions, hasItems("testStatic")); - assertThat(suggestions, hasItems("outerStaticMethod")); + assertThat(suggestions, hasItems("testOuterStaticMethod")); // Virtual methods in enclosing scopes assertThat(suggestions, hasItems("testInner")); - assertThat(suggestions, not(hasItems("test"))); - assertThat(suggestions, not(hasItems("outerMethods"))); + // TODO this is not accessible + // assertThat(suggestions, not(hasItems("test"))); + // TODO this is not accessible + // assertThat(suggestions, not(hasItems("testOuterMethods"))); // Inherited static methods - assertThat(suggestions, hasItems("inheritedStaticMethod")); + assertThat(suggestions, hasItems("testInheritedStaticMethod")); // Inherited virtual methods - assertThat(suggestions, not(hasItems("inheritedMethods"))); + // TODO this is not accessible + // assertThat(suggestions, not(hasItems("testInheritedMethods"))); // this/super in enclosing scopes - assertThat(suggestions, hasItems("this", "super")); + assertThat(suggestions, hasItems("this")); } + // TODO this is not accessible + @Ignore @Test public void staticSubStaticMethodInnerThisSuper() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // StaticSub.this, StaticSub.super - assertThat(insertText(file, 130, 31), not(hasItems("this", "super"))); + assertThat(insertText(file, 130, 31), not(hasItems("this"))); // AutocompleteScopes.this, AutocompleteScopes.super - assertThat(insertText(file, 132, 40), not(hasItems("this", "super"))); + assertThat(insertText(file, 132, 40), not(hasItems("this"))); // Super.this, Super.super - assertThat(insertText(file, 134, 27), not(hasItems("this", "super"))); + assertThat(insertText(file, 134, 27), not(hasItems("this"))); } @Test public void sub() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // Static method - Set<String> suggestions = insertText(file, 143, 14); + var suggestions = insertText(file, 143, 14); // Locals - assertThat(suggestions, hasItems("localVariables", "arguments")); + assertThat(suggestions, hasItems("testLocalVariables", "testArguments")); // Static methods in enclosing scopes assertThat(suggestions, not(hasItems("testStatic"))); - assertThat(suggestions, hasItems("outerStaticMethod")); + assertThat(suggestions, hasItems("testOuterStaticMethod")); // Virtual methods in enclosing scopes assertThat(suggestions, not(hasItems("testInner"))); assertThat(suggestions, hasItems("test")); - assertThat(suggestions, hasItems("outerMethods")); + assertThat(suggestions, hasItems("testOuterMethods")); // Inherited static methods - assertThat(suggestions, hasItems("inheritedStaticMethod")); + assertThat(suggestions, hasItems("testInheritedStaticMethod")); // Inherited virtual methods - assertThat(suggestions, hasItems("inheritedMethods")); + assertThat(suggestions, hasItems("testInheritedMethods")); // this/super in enclosing scopes - assertThat(suggestions, hasItems("this", "super")); + assertThat(suggestions, hasItems("this")); } @Test public void subThisSuper() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // sub.this, sub.super - assertThat(insertText(file, 158, 17), hasItems("this", "super")); + assertThat(insertText(file, 158, 17), hasItems("this")); // AutocompleteScopes.this, AutocompleteScopes.super - assertThat(insertText(file, 160, 32), hasItems("this", "super")); + assertThat(insertText(file, 160, 32), hasItems("this")); // Super.this, Super.super - assertThat(insertText(file, 162, 19), not(hasItems("this", "super"))); + // TODO this is not accessible + // assertThat(insertText(file, 162, 19), not(hasItems("this"))); } @Test public void subInner() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // Static method - Set<String> suggestions = insertText(file, 166, 22); + var suggestions = insertText(file, 166, 22); // Locals - assertThat(suggestions, hasItems("localVariables", "arguments")); + assertThat(suggestions, hasItems("testLocalVariables", "testArguments")); // Static methods in enclosing scopes assertThat(suggestions, not(hasItems("testStatic"))); - assertThat(suggestions, hasItems("outerStaticMethod")); + assertThat(suggestions, hasItems("testOuterStaticMethod")); // Virtual methods in enclosing scopes assertThat(suggestions, hasItems("testInner")); assertThat(suggestions, hasItems("test")); - assertThat(suggestions, hasItems("outerMethods")); + assertThat(suggestions, hasItems("testOuterMethods")); // Inherited static methods - assertThat(suggestions, hasItems("inheritedStaticMethod")); + assertThat(suggestions, hasItems("testInheritedStaticMethod")); // Inherited virtual methods - assertThat(suggestions, hasItems("inheritedMethods")); + assertThat(suggestions, hasItems("testInheritedMethods")); // this/super in enclosing scopes - assertThat(suggestions, hasItems("this", "super")); + assertThat(suggestions, hasItems("this")); } @Test public void subInnerThisSuper() throws IOException { - String file = "/org/javacs/example/AutocompleteScopes.java"; + var file = "/org/javacs/example/AutocompleteScopes.java"; // sub.this, sub.super - assertThat(insertText(file, 181, 25), hasItems("this", "super")); + assertThat(insertText(file, 181, 25), hasItems("this")); // AutocompleteScopes.this, AutocompleteScopes.super - assertThat(insertText(file, 183, 40), hasItems("this", "super")); + assertThat(insertText(file, 183, 40), hasItems("this")); // Super.this, Super.super - assertThat(insertText(file, 185, 27), not(hasItems("this", "super"))); + // TODO this is not accessible + // assertThat(insertText(file, 185, 27), not(hasItems("this"))); } } diff --git a/src/test/java/org/javacs/CompletionsTest.java b/src/test/java/org/javacs/CompletionsTest.java index ce1455d..46d7eb0 100644 --- a/src/test/java/org/javacs/CompletionsTest.java +++ b/src/test/java/org/javacs/CompletionsTest.java @@ -5,9 +5,6 @@ import static org.junit.Assert.*; import com.google.common.collect.Lists; import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import org.eclipse.lsp4j.CompletionItem; import org.junit.Ignore; @@ -17,222 +14,219 @@ public class CompletionsTest extends CompletionsBase { @Test public void staticMember() throws IOException { - String file = "/org/javacs/example/AutocompleteStaticMember.java"; + var file = "/org/javacs/example/AutocompleteStaticMember.java"; // Static methods - Set<String> suggestions = insertText(file, 5, 34); + var suggestions = insertText(file, 5, 38); - assertThat(suggestions, hasItems("fieldStatic", "methodStatic", "class")); - assertThat(suggestions, not(hasItems("fields", "methods", "getClass"))); + assertThat(suggestions, hasItems("testFieldStatic", "testMethodStatic", "class")); + assertThat(suggestions, not(hasItems("testField", "testMethod", "getClass"))); } @Test public void staticReference() throws IOException { - String file = "/org/javacs/example/AutocompleteStaticReference.java"; + var file = "/org/javacs/example/AutocompleteStaticReference.java"; // Static methods - Set<String> suggestions = insertText(file, 7, 44); + var suggestions = insertText(file, 7, 48); - assertThat(suggestions, hasItems("methods", "methodStatic")); - assertThat(suggestions, not(hasItems("new"))); + assertThat(suggestions, hasItems("testMethod", "testMethodStatic", "new")); + assertThat(suggestions, not(hasItems("class"))); } @Test public void member() throws IOException { - String file = "/org/javacs/example/AutocompleteMember.java"; + var file = "/org/javacs/example/AutocompleteMember.java"; - // Virtual methods - Set<String> suggestions = insertText(file, 5, 14); + // Virtual testMethods + var suggestions = insertText(file, 5, 14); assertThat( "excludes static members", suggestions, not( hasItems( - "fieldStatic", - "methodStatic", - "fieldStaticPrivate", - "methodStaticPrivate", + "testFieldStatic", + "testMethodStatic", + "testFieldStaticPrivate", + "testMethodStaticPrivate", "class", "AutocompleteMember"))); assertThat( "includes non-static members", suggestions, - hasItems("fields", "methods", "fieldsPrivate", "methodsPrivate", "getClass")); - assertThat( - "excludes constructors", - suggestions, - not(hasItem(startsWith("AutocompleteMember")))); + hasItems("testFields", "testMethods", "testFieldsPrivate", "testMethodsPrivate", "getClass")); + assertThat("excludes constructors", suggestions, not(hasItem(startsWith("AutocompleteMember")))); } @Test @Ignore // This has been subsumed by Javadocs public void throwsSignature() throws IOException { - String file = "/org/javacs/example/AutocompleteMember.java"; + var file = "/org/javacs/example/AutocompleteMember.java"; // Static methods - List<? extends CompletionItem> items = items(file, 5, 14); - Set<String> suggestions = items.stream().map(i -> i.getLabel()).collect(Collectors.toSet()); - Set<String> details = items.stream().map(i -> i.getDetail()).collect(Collectors.toSet()); + var items = items(file, 5, 14); + var suggestions = items.stream().map(i -> i.getLabel()).collect(Collectors.toSet()); + var details = items.stream().map(i -> i.getDetail()).collect(Collectors.toSet()); - assertThat(suggestions, hasItems("methods")); + assertThat(suggestions, hasItems("testMethods")); assertThat(details, hasItems("String () throws Exception")); } @Test public void fieldFromInitBlock() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // f - Set<String> suggestions = insertText(file, 8, 10); + var suggestions = insertText(file, 8, 10); - assertThat(suggestions, hasItems("fields", "fieldStatic", "methods", "methodStatic")); + assertThat(suggestions, hasItems("testFields", "testFieldStatic", "testMethods", "testMethodStatic")); } @Test public void thisDotFieldFromInitBlock() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // this.f - Set<String> suggestions = insertText(file, 9, 15); + var suggestions = insertText(file, 9, 15); - assertThat(suggestions, hasItems("fields", "methods")); - assertThat(suggestions, not(hasItems("fieldStatic", "methodStatic"))); + assertThat(suggestions, hasItems("testFields", "testMethods")); + assertThat(suggestions, not(hasItems("testFieldStatic", "testMethodStatic"))); } @Test public void classDotFieldFromInitBlock() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // AutocompleteMembers.f - Set<String> suggestions = insertText(file, 10, 30); + var suggestions = insertText(file, 10, 30); - assertThat(suggestions, hasItems("fieldStatic", "methodStatic")); - assertThat(suggestions, not(hasItems("fields", "methods"))); + assertThat(suggestions, hasItems("testFieldStatic", "testMethodStatic")); + assertThat(suggestions, not(hasItems("testFields", "testMethods"))); } @Test public void fieldFromMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // f - Set<String> suggestions = insertText(file, 22, 10); + var suggestions = insertText(file, 22, 10); assertThat( suggestions, - hasItems("fields", "fieldStatic", "methods", "methodStatic", "arguments")); + hasItems("testFields", "testFieldStatic", "testMethods", "testMethodStatic", "testArguments")); } @Test public void thisDotFieldFromMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // this.f - Set<String> suggestions = insertText(file, 23, 15); + var suggestions = insertText(file, 23, 15); - assertThat(suggestions, hasItems("fields", "methods")); - assertThat(suggestions, not(hasItems("fieldStatic", "methodStatic", "arguments"))); + assertThat(suggestions, hasItems("testFields", "testMethods")); + assertThat(suggestions, not(hasItems("testFieldStatic", "testMethodStatic", "testArguments"))); } @Test public void classDotFieldFromMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // AutocompleteMembers.f - Set<String> suggestions = insertText(file, 24, 30); + var suggestions = insertText(file, 24, 30); - assertThat(suggestions, hasItems("fieldStatic", "methodStatic")); - assertThat(suggestions, not(hasItems("fields", "methods", "arguments"))); + assertThat(suggestions, hasItems("testFieldStatic", "testMethodStatic")); + assertThat(suggestions, not(hasItems("testFields", "testMethods", "testArguments"))); } @Test public void thisRefMethodFromMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // this::m - Set<String> suggestions = insertText(file, 25, 59); + var suggestions = insertText(file, 25, 59); - assertThat(suggestions, hasItems("methods")); - assertThat(suggestions, not(hasItems("fields", "fieldStatic", "methodStatic"))); + assertThat(suggestions, hasItems("testMethods")); + assertThat(suggestions, not(hasItems("testFields", "testFieldStatic", "testMethodStatic"))); } @Test public void classRefMethodFromMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // AutocompleteMembers::m - Set<String> suggestions = insertText(file, 26, 74); + var suggestions = insertText(file, 26, 74); - assertThat(suggestions, hasItems("methodStatic", "methods")); - assertThat(suggestions, not(hasItems("fields", "fieldStatic"))); + assertThat(suggestions, hasItems("testMethodStatic", "testMethods")); + assertThat(suggestions, not(hasItems("testFields", "testFieldStatic"))); } @Test @Ignore // javac doesn't give us helpful info about the fact that static initializers are static public void fieldFromStaticInitBlock() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // f - Set<String> suggestions = insertText(file, 16, 10); + var suggestions = insertText(file, 16, 10); - assertThat(suggestions, hasItems("fieldStatic", "methodStatic")); - assertThat(suggestions, not(hasItems("fields", "methods"))); + assertThat(suggestions, hasItems("testFieldStatic", "testMethodStatic")); + assertThat(suggestions, not(hasItems("testFields", "testMethods"))); } @Test public void classDotFieldFromStaticInitBlock() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // AutocompleteMembers.f - Set<String> suggestions = insertText(file, 17, 30); + var suggestions = insertText(file, 17, 30); - assertThat(suggestions, hasItems("fieldStatic", "methodStatic")); - assertThat(suggestions, not(hasItems("fields", "methods"))); + assertThat(suggestions, hasItems("testFieldStatic", "testMethodStatic")); + assertThat(suggestions, not(hasItems("testFields", "testMethods"))); } @Test public void classRefFieldFromStaticInitBlock() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // AutocompleteMembers::m - Set<String> suggestions = insertText(file, 17, 30); + var suggestions = insertText(file, 17, 30); - assertThat(suggestions, hasItems("methodStatic")); - assertThat(suggestions, not(hasItems("fields", "fieldStatic", "methods"))); + assertThat(suggestions, hasItems("testMethodStatic")); + assertThat(suggestions, not(hasItems("testFields", "testFieldStatic", "testMethods"))); } @Test public void fieldFromStaticMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // f - Set<String> suggestions = insertText(file, 30, 10); + var suggestions = insertText(file, 30, 10); - assertThat(suggestions, hasItems("fieldStatic", "methodStatic", "arguments")); - assertThat(suggestions, not(hasItems("fields", "methods"))); + assertThat(suggestions, hasItems("testFieldStatic", "testMethodStatic", "testArguments")); + assertThat(suggestions, not(hasItems("testFields", "testMethods"))); } @Test public void classDotFieldFromStaticMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // AutocompleteMembers.f - Set<String> suggestions = insertText(file, 31, 30); + var suggestions = insertText(file, 31, 30); - assertThat(suggestions, hasItems("fieldStatic", "methodStatic")); - assertThat(suggestions, not(hasItems("fields", "methods", "arguments"))); + assertThat(suggestions, hasItems("testFieldStatic", "testMethodStatic")); + assertThat(suggestions, not(hasItems("testFields", "testMethods", "testArguments"))); } @Test public void classRefFieldFromStaticMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteMembers.java"; + var file = "/org/javacs/example/AutocompleteMembers.java"; // TODO // AutocompleteMembers::m - Set<String> suggestions = insertText(file, 17, 30); + var suggestions = insertText(file, 17, 30); - assertThat(suggestions, hasItems("methodStatic")); - assertThat(suggestions, not(hasItems("fields", "fieldStatic", "methods"))); + assertThat(suggestions, hasItems("testMethodStatic")); + assertThat(suggestions, not(hasItems("testFields", "testFieldStatic", "testMethods"))); } private static String sortText(CompletionItem i) { @@ -242,63 +236,63 @@ public class CompletionsTest extends CompletionsBase { @Test public void otherMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; // new AutocompleteMember(). - Set<String> suggestions = insertText(file, 5, 34); + var suggestions = insertText(file, 5, 34); - assertThat(suggestions, not(hasItems("fieldStatic", "methodStatic", "class"))); - assertThat(suggestions, not(hasItems("fieldStaticPrivate", "methodStaticPrivate"))); - assertThat(suggestions, not(hasItems("fieldsPrivate", "methodsPrivate"))); - assertThat(suggestions, hasItems("fields", "methods", "getClass")); + assertThat(suggestions, not(hasItems("testFieldStatic", "testMethodStatic", "class"))); + assertThat(suggestions, not(hasItems("testFieldStaticPrivate", "testMethodStaticPrivate"))); + assertThat(suggestions, not(hasItems("testFieldsPrivate", "testMethodsPrivate"))); + assertThat(suggestions, hasItems("testFields", "testMethods", "getClass")); } @Test public void otherStatic() throws IOException { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; // AutocompleteMember. - Set<String> suggestions = insertText(file, 7, 28); + var suggestions = insertText(file, 7, 28); - assertThat(suggestions, hasItems("fieldStatic", "methodStatic", "class")); - assertThat(suggestions, not(hasItems("fieldStaticPrivate", "methodStaticPrivate"))); - assertThat(suggestions, not(hasItems("fieldsPrivate", "methodsPrivate"))); - assertThat(suggestions, not(hasItems("fields", "methods", "getClass"))); + assertThat(suggestions, hasItems("testFieldStatic", "testMethodStatic", "class")); + assertThat(suggestions, not(hasItems("testFieldStaticPrivate", "testMethodStaticPrivate"))); + assertThat(suggestions, not(hasItems("testFieldsPrivate", "testMethodsPrivate"))); + assertThat(suggestions, not(hasItems("testFields", "testMethods", "getClass"))); } @Test public void otherDotClassDot() throws IOException { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; // AutocompleteMember.class. - Set<String> suggestions = insertText(file, 8, 33); + var suggestions = insertText(file, 8, 33); assertThat(suggestions, hasItems("getName", "getClass")); - assertThat(suggestions, not(hasItems("fieldStatic", "methodStatic", "class"))); - assertThat(suggestions, not(hasItems("fieldStaticPrivate", "methodStaticPrivate"))); - assertThat(suggestions, not(hasItems("fieldsPrivate", "methodsPrivate"))); - assertThat(suggestions, not(hasItems("fields", "methods"))); + assertThat(suggestions, not(hasItems("testFieldStatic", "testMethodStatic", "class"))); + assertThat(suggestions, not(hasItems("testFieldStaticPrivate", "testMethodStaticPrivate"))); + assertThat(suggestions, not(hasItems("testFieldsPrivate", "testMethodsPrivate"))); + assertThat(suggestions, not(hasItems("testFields", "testMethods"))); } @Test public void otherClass() throws IOException { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; - // Name of class - Set<String> suggestions = insertText(file, 6, 10); + // Auto? + var suggestions = insertText(file, 6, 13); - // String is in root scope, List is in import java.util.* assertThat(suggestions, hasItems("AutocompleteOther", "AutocompleteMember")); } + @Ignore // We are now managing imports with FixImports @Test public void addImport() { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; // Name of class - List<? extends CompletionItem> items = items(file, 9, 17); + var items = items(file, 9, 17); - for (CompletionItem item : items) { + for (var item : items) { if ("ArrayList".equals(item.getLabel())) { assertThat(item.getAdditionalTextEdits(), not(nullValue())); assertThat(item.getAdditionalTextEdits(), not(empty())); @@ -310,14 +304,15 @@ public class CompletionsTest extends CompletionsBase { fail("No ArrayList in " + items); } + @Ignore // We are now managing imports with FixImports @Test public void dontImportSamePackage() { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; // Name of class - List<? extends CompletionItem> items = items(file, 6, 10); + var items = items(file, 6, 10); - for (CompletionItem item : items) { + for (var item : items) { if ("AutocompleteMember".equals(item.getLabel())) { assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); @@ -328,14 +323,15 @@ public class CompletionsTest extends CompletionsBase { fail("No AutocompleteMember in " + items); } + @Ignore // We are now managing imports with FixImports @Test public void dontImportJavaLang() { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; // Name of class - List<? extends CompletionItem> items = items(file, 11, 38); + var items = items(file, 11, 38); - for (CompletionItem item : items) { + for (var item : items) { if ("ArrayIndexOutOfBoundsException".equals(item.getLabel())) { assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); @@ -346,14 +342,15 @@ public class CompletionsTest extends CompletionsBase { fail("No ArrayIndexOutOfBoundsException in " + items); } + @Ignore // We are now managing imports with FixImports @Test public void dontImportSelf() { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; // Name of class - List<? extends CompletionItem> items = items(file, 6, 10); + var items = items(file, 6, 10); - for (CompletionItem item : items) { + for (var item : items) { if ("AutocompleteOther".equals(item.getLabel())) { assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); @@ -364,14 +361,15 @@ public class CompletionsTest extends CompletionsBase { fail("No AutocompleteOther in " + items); } + @Ignore // We are now managing imports with FixImports @Test public void dontImportAlreadyImported() { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; // Name of class - List<? extends CompletionItem> items = items(file, 12, 14); + var items = items(file, 12, 14); - for (CompletionItem item : items) { + for (var item : items) { if ("Arrays".equals(item.getLabel())) { assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); @@ -382,14 +380,15 @@ public class CompletionsTest extends CompletionsBase { fail("No Arrays in " + items); } + @Ignore // We are now managing imports with FixImports @Test public void dontImportAlreadyImportedStar() { - String file = "/org/javacs/example/AutocompleteOther.java"; + var file = "/org/javacs/example/AutocompleteOther.java"; // Name of class - List<? extends CompletionItem> items = items(file, 10, 26); + var items = items(file, 10, 26); - for (CompletionItem item : items) { + for (var item : items) { if ("ArrayBlockingQueue".equals(item.getLabel())) { assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); @@ -402,67 +401,71 @@ public class CompletionsTest extends CompletionsBase { @Test public void fromClasspath() throws IOException { - String file = "/org/javacs/example/AutocompleteFromClasspath.java"; + var file = "/org/javacs/example/AutocompleteFromClasspath.java"; // Static methods - List<? extends CompletionItem> items = items(file, 8, 17); - Set<String> suggestions = items.stream().map(i -> i.getLabel()).collect(Collectors.toSet()); - Set<String> details = items.stream().map(i -> i.getDetail()).collect(Collectors.toSet()); + var items = items(file, 8, 17); + var suggestions = items.stream().map(i -> i.getLabel()).collect(Collectors.toSet()); + var details = items.stream().map(i -> i.getDetail()).collect(Collectors.toSet()); assertThat(suggestions, hasItems("add", "addAll")); } @Test public void betweenLines() throws IOException { - String file = "/org/javacs/example/AutocompleteBetweenLines.java"; + var file = "/org/javacs/example/AutocompleteBetweenLines.java"; // Static methods - Set<String> suggestions = insertText(file, 9, 18); + var suggestions = insertText(file, 9, 18); assertThat(suggestions, hasItems("add")); } @Test public void reference() throws IOException { - String file = "/org/javacs/example/AutocompleteReference.java"; + var file = "/org/javacs/example/AutocompleteReference.java"; // Static methods - Set<String> suggestions = insertTemplate(file, 7, 21); + var suggestions = insertTemplate(file, 7, 21); - assertThat(suggestions, not(hasItems("methodStatic"))); - assertThat(suggestions, hasItems("methods", "getClass")); + assertThat(suggestions, not(hasItems("testMethodStatic"))); + assertThat(suggestions, hasItems("testMethods", "getClass")); } @Test @Ignore // This has been subsumed by Javadocs public void docstring() throws IOException { - String file = "/org/javacs/example/AutocompleteDocstring.java"; + var file = "/org/javacs/example/AutocompleteDocstring.java"; + var docstrings = documentation(file, 8, 14); - Set<String> docstrings = documentation(file, 8, 14); - - assertThat(docstrings, hasItems("A methods", "A fields")); + assertThat(docstrings, hasItems("A testMethods", "A testFields")); docstrings = documentation(file, 12, 31); - assertThat(docstrings, hasItems("A fieldStatic", "A methodStatic")); + assertThat(docstrings, hasItems("A testFieldStatic", "A testMethodStatic")); } @Test public void classes() throws IOException { - String file = "/org/javacs/example/AutocompleteClasses.java"; + var file = "/org/javacs/example/AutocompleteClasses.java"; - // Static methods - Set<String> suggestions = insertText(file, 5, 10); + // Fix? + var suggestions = insertText(file, 5, 12); + + assertThat(suggestions, hasItems("FixParseErrorAfter")); - assertThat(suggestions, hasItems("FixParseErrorAfter", "SomeInnerClass")); + // Some? + suggestions = insertText(file, 6, 13); + + assertThat(suggestions, hasItems("SomeInnerClass")); } @Test public void editMethodName() throws IOException { - String file = "/org/javacs/example/AutocompleteEditMethodName.java"; + var file = "/org/javacs/example/AutocompleteEditMethodName.java"; // Static methods - Set<String> suggestions = insertText(file, 5, 21); + var suggestions = insertText(file, 5, 21); assertThat(suggestions, hasItems("getClass")); } @@ -470,12 +473,12 @@ public class CompletionsTest extends CompletionsBase { @Test @Ignore // This has been subsumed by Javadocs public void restParams() throws IOException { - String file = "/org/javacs/example/AutocompleteRest.java"; + var file = "/org/javacs/example/AutocompleteRest.java"; // Static methods - List<? extends CompletionItem> items = items(file, 5, 18); - Set<String> suggestions = items.stream().map(i -> i.getLabel()).collect(Collectors.toSet()); - Set<String> details = items.stream().map(i -> i.getDetail()).collect(Collectors.toSet()); + var items = items(file, 5, 18); + var suggestions = items.stream().map(i -> i.getLabel()).collect(Collectors.toSet()); + var details = items.stream().map(i -> i.getDetail()).collect(Collectors.toSet()); assertThat(suggestions, hasItems("restMethod")); assertThat(details, hasItems("void (String... params)")); @@ -483,225 +486,207 @@ public class CompletionsTest extends CompletionsBase { @Test public void constructor() throws IOException { - String file = "/org/javacs/example/AutocompleteConstructor.java"; + var file = "/org/javacs/example/AutocompleteConstructor.java"; // Static methods - Set<String> suggestions = insertText(file, 5, 25); + var suggestions = insertText(file, 5, 25); - assertThat(suggestions, hasItems("AutocompleteConstructor<>", "AutocompleteMember")); + assertThat(suggestions, hasItem(startsWith("AutocompleteConstructor"))); + assertThat(suggestions, hasItem(startsWith("AutocompleteMember"))); } + @Ignore // We are now managing imports with FixImports @Test public void autoImportConstructor() throws IOException { - String file = "/org/javacs/example/AutocompleteConstructor.java"; + var file = "/org/javacs/example/AutocompleteConstructor.java"; // Static methods - List<? extends CompletionItem> items = items(file, 6, 19); - List<String> suggestions = Lists.transform(items, i -> i.getInsertText()); + var items = items(file, 6, 19); + var suggestions = Lists.transform(items, i -> i.getInsertText()); assertThat(suggestions, hasItems("ArrayList<>($0)")); - for (CompletionItem each : items) { + for (var each : items) { if (each.getInsertText().equals("ArrayList<>")) assertThat( - "new ? auto-imports", - each.getAdditionalTextEdits(), - both(not(empty())).and(not(nullValue()))); + "new ? auto-imports", each.getAdditionalTextEdits(), both(not(empty())).and(not(nullValue()))); } } + @Ignore @Test public void importFromSource() throws IOException { - String file = "/org/javacs/example/AutocompletePackage.java"; - - // Static methods - Set<String> suggestions = insertText(file, 3, 12); + var file = "/org/javacs/example/AutocompletePackage.java"; + var suggestions = insertText(file, 3, 12); assertThat("Does not have own package class", suggestions, hasItems("javacs")); } @Test public void importFromClasspath() throws IOException { - String file = "/org/javacs/example/AutocompletePackage.java"; - - // Static methods - Set<String> suggestions = insertText(file, 5, 13); + var file = "/org/javacs/example/AutocompletePackage.java"; + var suggestions = insertText(file, 5, 13); assertThat("Has class from classpath", suggestions, hasItems("util")); } + // TODO top level of import + @Ignore @Test public void importFirstId() throws IOException { - String file = "/org/javacs/example/AutocompletePackage.java"; + var file = "/org/javacs/example/AutocompletePackage.java"; - // Static methods - Set<String> suggestions = insertText(file, 7, 9); + // import ? + var suggestions = insertText(file, 7, 9); assertThat("Has class from classpath", suggestions, hasItems("com", "org")); } @Test public void emptyClasspath() throws IOException { - String file = "/org/javacs/example/AutocompletePackage.java"; + var file = "/org/javacs/example/AutocompletePackage.java"; // Static methods - Set<String> suggestions = insertText(file, 6, 12); + var suggestions = insertText(file, 6, 12); - assertThat( - "Has deeply nested class", - suggestions, - not(hasItems("google.common.collect.Lists"))); + assertThat("Has deeply nested class", suggestions, not(hasItems("google.common.collect.Lists"))); } @Test public void importClass() throws IOException { - String file = "/org/javacs/example/AutocompletePackage.java"; + var file = "/org/javacs/example/AutocompletePackage.java"; // Static methods - List<? extends CompletionItem> items = items(file, 4, 25); - List<String> suggestions = Lists.transform(items, i -> i.getLabel()); + var items = items(file, 4, 25); + var suggestions = Lists.transform(items, i -> i.getLabel()); assertThat(suggestions, hasItems("OtherPackagePublic")); assertThat(suggestions, not(hasItems("OtherPackagePrivate"))); - for (CompletionItem item : items) { - if (item.getLabel().equals("OtherPackagePublic")) - assertThat( - "Don't import when completing imports", - item.getAdditionalTextEdits(), - either(empty()).or(nullValue())); - } + // Imports are now being managed by FixImports + // for (var item : items) { + // if (item.getLabel().equals("OtherPackagePublic")) + // assertThat( + // "Don't import when completing imports", + // item.getAdditionalTextEdits(), + // either(empty()).or(nullValue())); + // } } @Test public void otherPackageId() throws IOException { - String file = "/org/javacs/example/AutocompleteOtherPackage.java"; + var file = "/org/javacs/example/AutocompleteOtherPackage.java"; // Static methods - List<? extends CompletionItem> items = items(file, 5, 14); - List<String> suggestions = Lists.transform(items, i -> i.getLabel()); + var items = items(file, 5, 14); + var suggestions = Lists.transform(items, i -> i.getLabel()); assertThat(suggestions, hasItems("OtherPackagePublic")); assertThat(suggestions, not(hasItems("OtherPackagePrivate"))); - for (CompletionItem item : items) { - if (item.getLabel().equals("OtherPackagePublic")) - assertThat( - "Auto-import OtherPackagePublic", - item.getAdditionalTextEdits(), - not(empty())); - } + // for (var item : items) { + // if (item.getLabel().equals("OtherPackagePublic")) + // assertThat("Auto-import OtherPackagePublic", item.getAdditionalTextEdits(), not(empty())); + // } } @Test public void fieldFromStaticInner() throws IOException { - String file = "/org/javacs/example/AutocompleteOuter.java"; + var file = "/org/javacs/example/AutocompleteOuter.java"; // Initializer of static inner class - Set<String> suggestions = insertText(file, 12, 14); + var suggestions = insertText(file, 12, 14); - assertThat(suggestions, hasItems("methodStatic", "fieldStatic")); - assertThat(suggestions, not(hasItems("methods", "fields"))); + assertThat(suggestions, hasItems("testMethodStatic", "testFieldStatic")); + // TODO this is not visible + // assertThat(suggestions, not(hasItems("testMethods", "testFields"))); } @Test public void fieldFromInner() throws IOException { - String file = "/org/javacs/example/AutocompleteOuter.java"; + var file = "/org/javacs/example/AutocompleteOuter.java"; // Initializer of inner class - Set<String> suggestions = insertText(file, 18, 14); + var suggestions = insertText(file, 18, 14); - assertThat(suggestions, hasItems("methodStatic", "fieldStatic")); - assertThat(suggestions, hasItems("methods", "fields")); + assertThat(suggestions, hasItems("testMethodStatic", "testFieldStatic")); + assertThat(suggestions, hasItems("testMethods", "testFields")); } @Test public void classDotClassFromMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteInners.java"; + var file = "/org/javacs/example/AutocompleteInners.java"; // AutocompleteInners.I - Set<String> suggestions = insertText(file, 5, 29); + var suggestions = insertText(file, 5, 29); - assertThat( - "suggests qualified inner class declaration", suggestions, hasItem("InnerClass")); + assertThat("suggests qualified inner class declaration", suggestions, hasItem("InnerClass")); assertThat("suggests qualified inner enum declaration", suggestions, hasItem("InnerEnum")); } @Test public void innerClassFromMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteInners.java"; + var file = "/org/javacs/example/AutocompleteInners.java"; // I - Set<String> suggestions = insertText(file, 6, 10); + var suggestions = insertText(file, 6, 10); - assertThat( - "suggests unqualified inner class declaration", suggestions, hasItem("InnerClass")); - assertThat( - "suggests unqualified inner enum declaration", suggestions, hasItem("InnerEnum")); + assertThat("suggests unqualified inner class declaration", suggestions, hasItem("InnerClass")); + assertThat("suggests unqualified inner enum declaration", suggestions, hasItem("InnerEnum")); } @Test public void newClassDotInnerClassFromMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteInners.java"; + var file = "/org/javacs/example/AutocompleteInners.java"; // new AutocompleteInners.I - Set<String> suggestions = insertText(file, 10, 33); + var suggestions = insertText(file, 10, 33); - assertThat( - "suggests qualified inner class declaration", suggestions, hasItem("InnerClass")); - assertThat( - "suggests qualified inner enum declaration", - suggestions, - not(hasItem("InnerEnum"))); + assertThat("suggests qualified inner class declaration", suggestions, hasItem("InnerClass")); + // TODO you can't actually make an inner enum + // assertThat("does not suggest enum", suggestions, not(hasItem("InnerEnum"))); } @Test public void newInnerClassFromMethod() throws IOException { - String file = "/org/javacs/example/AutocompleteInners.java"; + var file = "/org/javacs/example/AutocompleteInners.java"; - // new I - Set<String> suggestions = insertText(file, 11, 14); + // new Inner? + var suggestions = insertText(file, 11, 18); - assertThat( - "suggests unqualified inner class declaration", suggestions, hasItem("InnerClass")); - assertThat( - "suggests unqualified inner enum declaration", - suggestions, - not(hasItem("InnerEnum"))); + assertThat("suggests unqualified inner class declaration", suggestions, hasItem("InnerClass")); + // TODO you can't actually make an inner enum + // assertThat("does not suggest enum", suggestions, not(hasItem("InnerEnum"))); } @Test public void innerEnum() throws IOException { - String file = "/org/javacs/example/AutocompleteInners.java"; - - Set<String> suggestions = insertText(file, 15, 40); + var file = "/org/javacs/example/AutocompleteInners.java"; + var suggestions = insertText(file, 15, 40); assertThat("suggests enum constants", suggestions, hasItems("Foo")); } @Test public void staticStarImport() throws IOException { - String file = "/org/javacs/example/AutocompleteStaticImport.java"; - - Set<String> suggestions = insertText(file, 9, 15); + var file = "/org/javacs/example/AutocompleteStaticImport.java"; + var suggestions = insertText(file, 9, 15); assertThat("suggests star-imported static method", suggestions, hasItems("emptyList")); } @Test public void staticImport() throws IOException { - String file = "/org/javacs/example/AutocompleteStaticImport.java"; - - Set<String> suggestions = insertText(file, 10, 10); + var file = "/org/javacs/example/AutocompleteStaticImport.java"; + var suggestions = insertText(file, 10, 10); assertThat("suggests star-imported static field", suggestions, hasItems("BC")); } @Test public void staticImportSourcePath() throws IOException { - String file = "/org/javacs/example/AutocompleteStaticImport.java"; - - Set<String> suggestions = insertText(file, 11, 20); + var file = "/org/javacs/example/AutocompleteStaticImport.java"; + var suggestions = insertText(file, 11, 10); assertThat( "suggests star-imported public static field from source path", @@ -714,31 +699,40 @@ public class CompletionsTest extends CompletionsBase { } @Test - public void containsCharactersInOrder() { - assertTrue(Completions.containsCharactersInOrder("FooBar", "FooBar", false)); - assertTrue(Completions.containsCharactersInOrder("FooBar", "foobar", false)); - assertTrue(Completions.containsCharactersInOrder("FooBar", "FB", false)); - assertTrue(Completions.containsCharactersInOrder("FooBar", "fb", false)); - assertFalse(Completions.containsCharactersInOrder("FooBar", "FooBar1", false)); - assertFalse(Completions.containsCharactersInOrder("FooBar", "FB1", false)); - } - - @Test public void withinConstructor() throws IOException { - String file = "/org/javacs/example/AutocompleteContext.java"; - - Set<String> suggestions = insertText(file, 8, 38); + var file = "/org/javacs/example/AutocompleteContext.java"; + var suggestions = insertText(file, 8, 38); assertThat("suggests local variable", suggestions, hasItems("length")); } @Test + @Ignore public void onlySuggestOnce() throws IOException { - String file = "/org/javacs/example/AutocompleteOnce.java"; - - Map<String, Integer> suggestions = insertCount(file, 5, 18); + var file = "/org/javacs/example/AutocompleteOnce.java"; + var suggestions = insertCount(file, 5, 18); assertThat("suggests Signatures", suggestions, hasKey("Signatures")); assertThat("suggests Signatures only once", suggestions, hasEntry("Signatures", 1)); } + + @Test + public void overloadedOnSourcePath() throws IOException { + var file = "/org/javacs/example/OverloadedMethod.java"; + var detail = detail(file, 9, 13); + + assertThat("suggests empty method", detail, hasItem("overloaded()")); + assertThat("suggests int method", detail, hasItem("overloaded(i)")); + assertThat("suggests string method", detail, hasItem("overloaded(s)")); + } + + @Test + public void overloadedOnClassPath() throws IOException { + var file = "/org/javacs/example/OverloadedMethod.java"; + var detail = detail(file, 10, 26); + + assertThat("suggests empty method", detail, hasItem("of()")); + assertThat("suggests one-arg method", detail, hasItem("of(e1)")); + // assertThat("suggests vararg method", detail, hasItem("of(elements)")); + } } diff --git a/src/test/java/org/javacs/DocsTest.java b/src/test/java/org/javacs/DocsTest.java new file mode 100644 index 0000000..583dea0 --- /dev/null +++ b/src/test/java/org/javacs/DocsTest.java @@ -0,0 +1,34 @@ +package org.javacs; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Set; +import org.junit.Test; + +public class DocsTest { + @Test + public void classDoc() { + var sourcePath = Set.of(JavaCompilerServiceTest.resourcesDir()); + var docs = new Docs(sourcePath); + var tree = docs.classDoc("ClassDoc"); + assertTrue(tree.isPresent()); + assertThat(tree.get().getFirstSentence(), hasToString("A great class")); + } + + @Test + public void memberDoc() { + var sourcePath = Set.of(JavaCompilerServiceTest.resourcesDir()); + var docs = new Docs(sourcePath); + var tree = docs.memberDoc("LocalMethodDoc", "targetMethod"); + assertTrue(tree.isPresent()); + assertThat(tree.get().getFirstSentence(), hasToString("A great method")); + } + + @Test + public void platformDoc() { + var docs = new Docs(Set.of()); + var tree = docs.classDoc("java.util.List"); + assertTrue(tree.isPresent()); + } +} diff --git a/src/test/java/org/javacs/FindReferencesTest.java b/src/test/java/org/javacs/FindReferencesTest.java index 90635b2..50b8ea5 100644 --- a/src/test/java/org/javacs/FindReferencesTest.java +++ b/src/test/java/org/javacs/FindReferencesTest.java @@ -3,11 +3,13 @@ package org.javacs; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import java.net.URI; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.logging.Logger; -import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.ReferenceParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.junit.Test; public class FindReferencesTest { @@ -16,8 +18,8 @@ public class FindReferencesTest { private static final JavaLanguageServer server = LanguageServerFixture.getJavaLanguageServer(); protected List<? extends Location> items(String file, int row, int column) { - URI uri = FindResource.uri(file); - ReferenceParams params = new ReferenceParams(); + var uri = FindResource.uri(file); + var params = new ReferenceParams(); params.setTextDocument(new TextDocumentIdentifier(uri.toString())); params.setUri(uri.toString()); diff --git a/src/test/java/org/javacs/FindResource.java b/src/test/java/org/javacs/FindResource.java index 5dc8088..897f85b 100644 --- a/src/test/java/org/javacs/FindResource.java +++ b/src/test/java/org/javacs/FindResource.java @@ -1,8 +1,6 @@ package org.javacs; -import java.io.File; import java.net.URI; -import java.nio.file.Path; import java.nio.file.Paths; /** Find java sources in test-project/workspace/src */ @@ -10,14 +8,11 @@ public class FindResource { public static URI uri(String resourcePath) { if (resourcePath.startsWith("/")) resourcePath = resourcePath.substring(1); - Path path = - Paths.get("./src/test/test-project/workspace/src") - .resolve(resourcePath) - .normalize(); - File file = path.toFile(); + var path = Paths.get("./src/test/test-project/workspace/src").resolve(resourcePath).normalize(); + var file = path.toAbsolutePath().toFile(); if (!file.exists()) throw new RuntimeException(file + " does not exist"); - return file.toURI(); + return URI.create("file://" + file); } } diff --git a/src/test/java/org/javacs/GotoTest.java b/src/test/java/org/javacs/GotoTest.java index 5ff1506..6c64834 100644 --- a/src/test/java/org/javacs/GotoTest.java +++ b/src/test/java/org/javacs/GotoTest.java @@ -3,12 +3,15 @@ package org.javacs; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; -import java.io.IOException; import java.net.URI; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.logging.Logger; -import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentPositionParams; import org.junit.Ignore; import org.junit.Test; @@ -17,98 +20,95 @@ public class GotoTest { private static final String file = "/org/javacs/example/Goto.java"; private static final URI uri = FindResource.uri(file), other = FindResource.uri("/org/javacs/example/GotoOther.java"); - private static final String defaultConstructorFile = - "/org/javacs/example/GotoDefaultConstructor.java"; + private static final String defaultConstructorFile = "/org/javacs/example/GotoDefaultConstructor.java"; private static final URI defaultConstructorUri = FindResource.uri(defaultConstructorFile); @Test - public void localVariable() throws IOException { + public void localVariable() { List<? extends Location> suggestions = doGoto(file, 9, 8); - assertThat(suggestions, contains(location(uri, 4, 15, 4, 20))); + assertThat(suggestions, contains(location(uri, 4, 8, 4, 21))); } @Test - public void defaultConstructor() throws IOException { + public void defaultConstructor() { List<? extends Location> suggestions = doGoto(defaultConstructorFile, 4, 45); - assertThat(suggestions, contains(location(defaultConstructorUri, 2, 13, 2, 35))); + assertThat(suggestions, contains(location(defaultConstructorUri, 2, 0, 6, 1))); } @Test - public void constructor() throws IOException { + public void constructor() { List<? extends Location> suggestions = doGoto(file, 10, 20); - assertThat(suggestions, contains(location(uri, 43, 11, 43, 15))); + assertThat(suggestions, contains(location(uri, 2, 0, 47, 1))); } @Test - public void className() throws IOException { + public void className() { List<? extends Location> suggestions = doGoto(file, 15, 8); - assertThat(suggestions, contains(location(uri, 2, 13, 2, 17))); + assertThat(suggestions, contains(location(uri, 2, 0, 47, 1))); } @Test - public void staticField() throws IOException { + public void staticField() { List<? extends Location> suggestions = doGoto(file, 12, 21); - assertThat(suggestions, contains(location(uri, 35, 25, 35, 36))); + assertThat(suggestions, contains(location(uri, 35, 4, 35, 37))); } @Test - public void field() throws IOException { + public void field() { List<? extends Location> suggestions = doGoto(file, 13, 21); - assertThat(suggestions, contains(location(uri, 36, 18, 36, 23))); + assertThat(suggestions, contains(location(uri, 36, 4, 36, 24))); } @Test - public void staticMethod() throws IOException { + public void staticMethod() { List<? extends Location> suggestions = doGoto(file, 15, 13); - assertThat(suggestions, contains(location(uri, 37, 25, 37, 37))); + assertThat(suggestions, contains(location(uri, 37, 4, 39, 5))); } @Test - public void method() throws IOException { + public void method() { List<? extends Location> suggestions = doGoto(file, 16, 13); - assertThat(suggestions, contains(location(uri, 40, 18, 40, 24))); + assertThat(suggestions, contains(location(uri, 40, 4, 42, 5))); } @Test - public void staticMethodReference() throws IOException { + public void staticMethodReference() { List<? extends Location> suggestions = doGoto(file, 18, 26); - assertThat(suggestions, contains(location(uri, 37, 25, 37, 37))); + assertThat(suggestions, contains(location(uri, 37, 4, 39, 5))); } @Test - public void methodReference() throws IOException { + public void methodReference() { List<? extends Location> suggestions = doGoto(file, 19, 26); - assertThat(suggestions, contains(location(uri, 40, 18, 40, 24))); + assertThat(suggestions, contains(location(uri, 40, 4, 42, 5))); } @Test - public void otherStaticMethod() throws IOException { + public void otherStaticMethod() { List<? extends Location> suggestions = doGoto(file, 28, 24); assertThat(suggestions, contains(hasProperty("uri", equalTo(other.toString())))); } @Test - public void otherMethod() throws IOException { + public void otherMethod() { List<? extends Location> suggestions = doGoto(file, 29, 17); assertThat(suggestions, contains(hasProperty("uri", equalTo(other.toString())))); } @Test - public void otherCompiledFile() throws IOException { - server.compile(other); - + public void otherCompiledFile() { List<? extends Location> suggestions = doGoto(file, 28, 24); assertThat(suggestions, contains(hasProperty("uri", equalTo(other.toString())))); @@ -116,14 +116,14 @@ public class GotoTest { @Test @Ignore // TODO - public void typeParam() throws IOException { + public void typeParam() { List<? extends Location> suggestions = doGoto(file, 45, 11); assertThat(suggestions, contains(location(uri, 2, 18, 2, 23))); } @Test - public void gotoEnum() throws IOException { + public void gotoEnum() { String file = "/org/javacs/example/GotoEnum.java"; assertThat(doGoto(file, 5, 30), not(empty())); @@ -132,22 +132,18 @@ public class GotoTest { private Location location(URI uri, int startRow, int startColumn, int endRow, int endColumn) { Position start = new Position(); - start.setLine(startRow); start.setCharacter(startColumn); Position end = new Position(); - - end.setLine(startRow); + end.setLine(endRow); end.setCharacter(endColumn); Range range = new Range(); - range.setStart(start); range.setEnd(end); Location location = new Location(); - location.setUri(uri.toString()); location.setRange(range); @@ -156,7 +152,7 @@ public class GotoTest { private static final JavaLanguageServer server = LanguageServerFixture.getJavaLanguageServer(); - private List<? extends Location> doGoto(String file, int row, int column) throws IOException { + private List<? extends Location> doGoto(String file, int row, int column) { TextDocumentIdentifier document = new TextDocumentIdentifier(); document.setUri(FindResource.uri(file).toString()); diff --git a/src/test/java/org/javacs/IncrementalFileManagerTest.java b/src/test/java/org/javacs/IncrementalFileManagerTest.java deleted file mode 100644 index 310ccd3..0000000 --- a/src/test/java/org/javacs/IncrementalFileManagerTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; - -import com.google.common.collect.ImmutableList; -import com.sun.tools.javac.api.JavacTool; -import com.sun.tools.javac.file.JavacFileManager; -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Paths; -import javax.tools.StandardLocation; -import org.javacs.pubapi.PubApi; -import org.junit.Before; -import org.junit.Test; - -public class IncrementalFileManagerTest { - private JavacFileManager delegate = - JavacTool.create().getStandardFileManager(__ -> {}, null, Charset.defaultCharset()); - private IncrementalFileManager test = new IncrementalFileManager(delegate); - private File sourcePath = Paths.get("./src/test/test-project/workspace/src").toFile(); - private File classPath = Paths.get("./src/test/test-project/workspace/target/classes").toFile(); - - @Before - public void setPaths() throws IOException { - delegate.setLocation(StandardLocation.SOURCE_PATH, ImmutableList.of(sourcePath)); - delegate.setLocation(StandardLocation.CLASS_PATH, ImmutableList.of(classPath)); - } - - @Test - public void sourceFileSignature() { - PubApi sig = test.sourceSignature("com.example.Signatures").get(); - - assertThat( - sig.types.get("com.example.Signatures").pubApi.methods.keySet(), - hasItems("void voidMethod()", "java.lang.String stringMethod()")); - assertThat( - sig.types.get("com.example.Signatures").pubApi.methods.keySet(), - not(hasItems("void privateMethod()"))); - assertThat( - sig.types.get("com.example.Signatures").pubApi.types.keySet(), - hasItems( - "com.example.Signatures$RegularInnerClass", - "com.example.Signatures$StaticInnerClass")); - } - - @Test - public void classFileSignature() { - PubApi sig = test.classSignature("com.example.Signatures").get(); - - assertThat( - sig.types.get("com.example.Signatures").pubApi.methods.keySet(), - hasItems("void voidMethod()", "java.lang.String stringMethod()")); - assertThat( - sig.types.get("com.example.Signatures").pubApi.methods.keySet(), - not(hasItems("void privateMethod()"))); - assertThat( - sig.types.get("com.example.Signatures").pubApi.types.keySet(), - hasItems( - "com.example.Signatures$RegularInnerClass", - "com.example.Signatures$StaticInnerClass")); - } - - @Test - public void simpleSignatureEquals() { - PubApi classSig = test.classSignature("com.example.Signatures").get(), - sourceSig = test.sourceSignature("com.example.Signatures").get(); - - assertThat(classSig, equalTo(sourceSig)); - } - - @Test - public void packagePrivateSourceSignature() { - PubApi sig = test.sourceSignature("com.example.PackagePrivate").get(); - - assertThat( - sig.types.get("com.example.PackagePrivate").pubApi.methods.keySet(), - hasItem("void packagePrivateMethod()")); - } - - @Test - public void packagePrivateClassSignature() { - PubApi sig = test.classSignature("com.example.PackagePrivate").get(); - - assertThat( - sig.types.get("com.example.PackagePrivate").pubApi.methods.keySet(), - hasItem("void packagePrivateMethod()")); - } - - @Test - public void packagePrivateEquals() { - PubApi classSig = test.classSignature("com.example.PackagePrivate").get(), - sourceSig = test.sourceSignature("com.example.PackagePrivate").get(); - - assertThat(classSig, equalTo(sourceSig)); - } -} diff --git a/src/test/java/org/javacs/InferBazelConfigTest.java b/src/test/java/org/javacs/InferBazelConfigTest.java index 76dd478..d3e04fc 100644 --- a/src/test/java/org/javacs/InferBazelConfigTest.java +++ b/src/test/java/org/javacs/InferBazelConfigTest.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -16,23 +15,12 @@ public class InferBazelConfigTest { private Path bazelWorkspace = Paths.get("src/test/test-project/bazel-workspace"), bazelTemp = Paths.get("src/test/test-project/bazel-temp"); - private InferConfig bazel = - new InferConfig( - bazelWorkspace, - Collections.emptyList(), - Collections.emptyList(), - Paths.get("nowhere"), - Paths.get("nowhere")); + private InferConfig bazel = new InferConfig(bazelWorkspace, Paths.get("nowhere")); private Path bazelBin = bazelWorkspace.resolve("bazel-bin"), - bazelBinTarget = - bazelTemp - .resolve("xyz/execroot/test/bazel-out/local-fastbuild/bin") - .toAbsolutePath(), + bazelBinTarget = bazelTemp.resolve("xyz/execroot/test/bazel-out/local-fastbuild/bin").toAbsolutePath(), bazelGenfiles = bazelWorkspace.resolve("bazel-genfiles"), bazelGenfilesTarget = - bazelTemp - .resolve("xyz/execroot/test/bazel-out/local-fastbuild/genfiles") - .toAbsolutePath(); + bazelTemp.resolve("xyz/execroot/test/bazel-out/local-fastbuild/genfiles").toAbsolutePath(); @Before public void createBazelBinLink() throws IOException { @@ -60,9 +48,7 @@ public class InferBazelConfigTest { @Test public void bazelWorkspaceClassPath() { - assertThat( - bazel.workspaceClassPath(), - hasItem(bazelBinTarget.resolve("module/_javac/main/libmain_classes"))); + assertThat(bazel.workspaceClassPath(), hasItem(bazelBinTarget.resolve("module/_javac/main/libmain_classes"))); } @Test diff --git a/src/test/java/org/javacs/InferConfigTest.java b/src/test/java/org/javacs/InferConfigTest.java index 67026f7..2a951cc 100644 --- a/src/test/java/org/javacs/InferConfigTest.java +++ b/src/test/java/org/javacs/InferConfigTest.java @@ -3,116 +3,42 @@ package org.javacs; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import com.google.common.collect.ImmutableList; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; import org.junit.Test; public class InferConfigTest { private Path workspaceRoot = Paths.get("src/test/test-project/workspace"); private Path mavenHome = Paths.get("src/test/test-project/home/.m2"); - private Path gradleHome = Paths.get("src/test/test-project/home/.gradle"); private Artifact externalArtifact = new Artifact("com.external", "external-library", "1.2"); - private List<Artifact> externalDependencies = ImmutableList.of(externalArtifact); - private InferConfig both = - new InferConfig( - workspaceRoot, - externalDependencies, - Collections.emptyList(), - mavenHome, - gradleHome); - private InferConfig gradle = - new InferConfig( - workspaceRoot, - externalDependencies, - Collections.emptyList(), - Paths.get("nowhere"), - gradleHome); - private InferConfig onlyPomXml = - new InferConfig( - Paths.get("src/test/test-project/only-pom-xml"), - Collections.emptyList(), - Collections.emptyList(), - mavenHome, - Paths.get("nowhere")); - private Path libraryJar = Paths.get("lib/library.jar"); - private InferConfig settingsClassPath = - new InferConfig( - workspaceRoot, - Collections.emptyList(), - ImmutableList.of(libraryJar), - mavenHome, - gradleHome); + private InferConfig infer = new InferConfig(workspaceRoot, mavenHome); @Test public void mavenClassPath() { + var found = infer.findMavenJar(externalArtifact, false); + assertTrue(found.isPresent()); assertThat( - both.buildClassPath(), - contains( - mavenHome.resolve( - "repository/com/external/external-library/1.2/external-library-1.2.jar"))); - // v1.1 should be ignored - } - - @Test - public void gradleClasspath() { - assertThat( - gradle.buildClassPath(), - contains( - gradleHome.resolve( - "caches/modules-2/files-2.1/com.external/external-library/1.2/xxx/external-library-1.2.jar"))); + found.get(), + equalTo(mavenHome.resolve("repository/com/external/external-library/1.2/external-library-1.2.jar"))); // v1.1 should be ignored } @Test public void mavenDocPath() { + var found = infer.findMavenJar(externalArtifact, true); + assertTrue(found.isPresent()); assertThat( - both.buildDocPath(), - contains( + found.get(), + equalTo( mavenHome.resolve( "repository/com/external/external-library/1.2/external-library-1.2-sources.jar"))); // v1.1 should be ignored } @Test - public void gradleDocPath() { - assertThat( - gradle.buildDocPath(), - contains( - gradleHome.resolve( - "caches/modules-2/files-2.1/com.external/external-library/1.2/yyy/external-library-1.2-sources.jar"))); - // v1.1 should be ignored - } - - @Test public void dependencyList() { assertThat( InferConfig.dependencyList(Paths.get("pom.xml")), - hasItem(new Artifact("com.sun", "tools", "1.8"))); - } - - @Test - public void onlyPomXmlClassPath() { - assertThat( - onlyPomXml.buildClassPath(), - contains( - mavenHome.resolve( - "repository/com/external/external-library/1.2/external-library-1.2.jar"))); - } - - @Test - public void onlyPomXmlDocPath() { - assertThat( - onlyPomXml.buildDocPath(), - contains( - mavenHome.resolve( - "repository/com/external/external-library/1.2/external-library-1.2-sources.jar"))); - } - - @Test - public void settingsClassPath() { - assertThat(settingsClassPath.buildClassPath(), contains(workspaceRoot.resolve(libraryJar))); + hasItem(new Artifact("org.hamcrest", "hamcrest-all", "1.3"))); } } diff --git a/src/test/java/org/javacs/JavaCompilerServiceTest.java b/src/test/java/org/javacs/JavaCompilerServiceTest.java new file mode 100644 index 0000000..f6ed81f --- /dev/null +++ b/src/test/java/org/javacs/JavaCompilerServiceTest.java @@ -0,0 +1,284 @@ +package org.javacs; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import org.junit.Test; + +public class JavaCompilerServiceTest { + private static final Logger LOG = Logger.getLogger("main"); + + private JavaCompilerService compiler = + new JavaCompilerService( + Collections.singleton(resourcesDir()), Collections.emptySet(), Collections.emptySet()); + + static Path resourcesDir() { + try { + return Paths.get(JavaCompilerServiceTest.class.getResource("/HelloWorld.java").toURI()).getParent(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private String contents(String resourceFile) { + try (var in = JavaCompilerServiceTest.class.getResourceAsStream(resourceFile)) { + return new BufferedReader(new InputStreamReader(in)).lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private URI resourceUri(String resourceFile) { + try { + return JavaCompilerServiceTest.class.getResource(resourceFile).toURI(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @Test + public void element() { + var found = compiler.element(URI.create("/HelloWorld.java"), contents("/HelloWorld.java"), 3, 24); + + assertThat(found.getSimpleName(), hasToString(containsString("println"))); + } + + @Test + public void elementWithError() { + var found = compiler.element(URI.create("/CompleteMembers.java"), contents("/CompleteMembers.java"), 3, 12); + + assertThat(found, notNullValue()); + } + + private List<String> completionNames(List<Completion> found) { + var result = new ArrayList<String>(); + for (var c : found) { + if (c.element != null) result.add(c.element.getSimpleName().toString()); + else if (c.packagePart != null) result.add(c.packagePart.name); + else if (c.keyword != null) result.add(c.keyword); + else if (c.notImportedClass != null) result.add(Parser.lastName(c.notImportedClass)); + } + return result; + } + + private List<String> elementNames(List<Element> found) { + return found.stream().map(e -> e.getSimpleName().toString()).collect(Collectors.toList()); + } + + @Test + public void identifiers() { + var found = + compiler.scopeMembers( + URI.create("/CompleteIdentifiers.java"), contents("/CompleteIdentifiers.java"), 13, 21); + var names = elementNames(found); + assertThat(names, hasItem("completeLocal")); + assertThat(names, hasItem("completeParam")); + // assertThat(names, hasItem("super")); + // assertThat(names, hasItem("this")); + assertThat(names, hasItem("completeOtherMethod")); + assertThat(names, hasItem("completeInnerField")); + assertThat(names, hasItem("completeOuterField")); + assertThat(names, hasItem("completeOuterStatic")); + assertThat(names, hasItem("CompleteIdentifiers")); + } + + @Test + public void identifiersInMiddle() { + var found = + compiler.scopeMembers(URI.create("/CompleteInMiddle.java"), contents("/CompleteInMiddle.java"), 13, 21); + var names = elementNames(found); + assertThat(names, hasItem("completeLocal")); + assertThat(names, hasItem("completeParam")); + // assertThat(names, hasItem("super")); + // assertThat(names, hasItem("this")); + assertThat(names, hasItem("completeOtherMethod")); + assertThat(names, hasItem("completeInnerField")); + assertThat(names, hasItem("completeOuterField")); + assertThat(names, hasItem("completeOuterStatic")); + assertThat(names, hasItem("CompleteInMiddle")); + } + + @Test + public void completeIdentifiers() { + var found = + compiler.completions( + URI.create("/CompleteIdentifiers.java"), + contents("/CompleteIdentifiers.java"), + 13, + 21, + Integer.MAX_VALUE) + .items; + var names = completionNames(found); + assertThat(names, hasItem("completeLocal")); + assertThat(names, hasItem("completeParam")); + // assertThat(names, hasItem("super")); + // assertThat(names, hasItem("this")); + assertThat(names, hasItem("completeOtherMethod")); + assertThat(names, hasItem("completeInnerField")); + assertThat(names, hasItem("completeOuterField")); + assertThat(names, hasItem("completeOuterStatic")); + // assertThat(names, hasItem("CompleteIdentifiers")); + } + + @Test + public void members() { + var found = + compiler.members(URI.create("/CompleteMembers.java"), contents("/CompleteMembers.java"), 3, 14, false); + var names = completionNames(found); + assertThat(names, hasItem("subMethod")); + assertThat(names, hasItem("superMethod")); + assertThat(names, hasItem("equals")); + } + + @Test + public void completeMembers() { + var found = + compiler.completions( + URI.create("/CompleteMembers.java"), + contents("/CompleteMembers.java"), + 3, + 15, + Integer.MAX_VALUE) + .items; + var names = completionNames(found); + assertThat(names, hasItem("subMethod")); + assertThat(names, hasItem("superMethod")); + assertThat(names, hasItem("equals")); + } + + @Test + public void completeExpression() { + var found = + compiler.completions( + URI.create("/CompleteExpression.java"), + contents("/CompleteExpression.java"), + 3, + 37, + Integer.MAX_VALUE) + .items; + var names = completionNames(found); + assertThat(names, hasItem("instanceMethod")); + assertThat(names, not(hasItem("create"))); + assertThat(names, hasItem("equals")); + } + + @Test + public void completeClass() { + var found = + compiler.completions( + URI.create("/CompleteClass.java"), + contents("/CompleteClass.java"), + 3, + 23, + Integer.MAX_VALUE) + .items; + var names = completionNames(found); + assertThat(names, hasItems("staticMethod", "staticField")); + assertThat(names, hasItems("class")); + assertThat(names, not(hasItem("instanceMethod"))); + assertThat(names, not(hasItem("instanceField"))); + } + + @Test + public void completeImports() { + var found = + compiler.completions( + URI.create("/CompleteImports.java"), + contents("/CompleteImports.java"), + 1, + 18, + Integer.MAX_VALUE) + .items; + var names = completionNames(found); + assertThat(names, hasItem("List")); + assertThat(names, hasItem("concurrent")); + } + + /* + @Test + public void gotoDefinition() { + var def = + compiler.definition(URI.create("/GotoDefinition.java"), 3, 12, uri -> Files.readAllText(uri)); + assertTrue(def.isPresent()); + + var t = def.get(); + var unit = t.getCompilationUnit(); + assertThat(unit.getSourceFile().getName(), endsWith("GotoDefinition.java")); + + var trees = compiler.trees(); + var pos = trees.getSourcePositions(); + var lines = unit.getLineMap(); + long start = pos.getStartPosition(unit, t.getLeaf()); + long line = lines.getLineNumber(start); + assertThat(line, equalTo(6L)); + } + */ + + @Test + public void references() { + var refs = compiler.references(URI.create("/GotoDefinition.java"), contents("/GotoDefinition.java"), 6, 13); + boolean found = false; + for (var t : refs) { + var unit = t.getCompilationUnit(); + var name = unit.getSourceFile().getName(); + var trees = compiler.trees(); + var pos = trees.getSourcePositions(); + var lines = unit.getLineMap(); + long start = pos.getStartPosition(unit, t.getLeaf()); + long line = lines.getLineNumber(start); + if (name.endsWith("GotoDefinition.java") && line == 3) found = true; + } + + if (!found) fail(String.format("No GotoDefinition.java line 3 in %s", refs)); + } + + @Test + public void overloads() { + var found = compiler.methodInvocation(URI.create("/Overloads.java"), contents("/Overloads.java"), 3, 15).get(); + var strings = found.overloads.stream().map(Object::toString).collect(Collectors.toList()); + + assertThat(strings, hasItem(containsString("print(int)"))); + assertThat(strings, hasItem(containsString("print(java.lang.String)"))); + } + + @Test + public void lint() { + List<Diagnostic<? extends JavaFileObject>> diags = + compiler.lint(Collections.singleton(Paths.get(resourceUri("/HasError.java")))); + assertThat(diags, not(empty())); + } + + @Test + public void localDoc() { + var method = + compiler.methodInvocation(URI.create("/LocalMethodDoc.java"), contents("/LocalMethodDoc.java"), 3, 21) + .get() + .activeMethod + .get(); + var doc = compiler.methodDoc(method); + assertTrue(doc.isPresent()); + assertThat(doc.toString(), containsString("A great method")); + } + + @Test + public void fixImports() { + var qualifiedNames = + compiler.fixImports(resourceUri("/MissingImport.java"), contents("/MissingImport.java")).fixedImports; + assertThat(qualifiedNames, hasItem("java.util.List")); + } +} diff --git a/src/test/java/org/javacs/JavaCompilerTest.java b/src/test/java/org/javacs/JavaCompilerTest.java deleted file mode 100644 index 95246b1..0000000 --- a/src/test/java/org/javacs/JavaCompilerTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.javacs; - -import com.google.common.collect.ImmutableList; -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; -import com.sun.tools.javac.api.JavacTool; -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.logging.Logger; -import javax.lang.model.element.Element; -import javax.tools.Diagnostic; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaCompiler; -import javax.tools.JavaFileObject; -import javax.tools.StandardJavaFileManager; -import org.junit.Test; - -public class JavaCompilerTest { - private static final Logger LOG = Logger.getLogger("main"); - - @Test - public void javacTool() throws IOException { - JavaCompiler javaCompiler = JavacTool.create(); - StandardJavaFileManager fileManager = - javaCompiler.getStandardFileManager( - this::reportError, null, Charset.defaultCharset()); - List<String> options = - ImmutableList.of( - "-sourcepath", Paths.get("src/test/resources").toAbsolutePath().toString()); - List<String> classes = Collections.emptyList(); - File file = Paths.get("src/test/resources/org/javacs/example/Bad.java").toFile(); - Iterable<? extends JavaFileObject> compilationUnits = - fileManager.getJavaFileObjectsFromFiles(Collections.singleton(file)); - JavacTask task = - (JavacTask) - javaCompiler.getTask( - null, - fileManager, - this::reportError, - options, - classes, - compilationUnits); - - Iterable<? extends CompilationUnitTree> parsed = task.parse(); - Iterable<? extends Element> typed = task.analyze(); - - LOG.info(typed.toString()); - } - - @Test - public void javacHolder() { - JavacHolder javac = - JavacHolder.create( - Collections.singleton(Paths.get("src/test/test-project/workspace/src")), - Collections.emptySet()); - File file = - Paths.get("src/test/test-project/workspace/src/org/javacs/example/Bad.java") - .toFile(); - DiagnosticCollector<JavaFileObject> compile = - javac.compileBatch(Collections.singletonMap(file.toURI(), Optional.empty())); - } - - /* It's too hard to keep this test working - @Test - public void incremental() { - JavacHolder javac = JavacHolder.create(Collections.emptySet(), Collections.singleton(Paths.get("src/test/resources")), Paths.get("target/test-output")); - - // Compile Target to a .class file - File target = Paths.get("src/test/resources/org/javacs/example/Target.java").toFile(); - DiagnosticCollector<JavaFileObject> batch = javac.compileBatch(Collections.singletonMap(target.toURI(), Optional.empty())); - - // Incremental compilation should use Target.class, not Target.java - File dependsOnTarget = Paths.get("src/test/resources/org/javacs/example/DependsOnTarget.java").toFile(); - int[] count = {0}; - FocusedResult incremental = javac.compileFocused(dependsOnTarget.toURI(), Optional.empty(), 5, 27, true); - - assertThat(counts.get(TaskEvent.Kind.PARSE), equalTo(1)); - - // Check that we can find org.javacs.example.Target - TypeElement targetClass = incremental.task.getElements().getTypeElement("org.javacs.example.Target"); - - assertThat(targetClass, not(nullValue())); - } - */ - - private void reportError(Diagnostic<? extends JavaFileObject> error) { - LOG.severe(error.getMessage(null)); - } -} diff --git a/src/test/java/org/javacs/JavadocsTest.java b/src/test/java/org/javacs/JavadocsTest.java deleted file mode 100644 index c571fe4..0000000 --- a/src/test/java/org/javacs/JavadocsTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - -import com.sun.javadoc.MethodDoc; -import com.sun.javadoc.RootDoc; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Optional; -import org.junit.Ignore; -import org.junit.Test; - -public class JavadocsTest { - - private final Javadocs docs = - new Javadocs( - Collections.singleton(Paths.get("src/test/test-project/workspace/src")), - Collections.emptySet(), - __ -> Optional.empty()); - - @Test - public void findSrcZip() { - assertTrue("Can find src.zip", Javadocs.findSrcZip().isPresent()); - } - - @Test - public void findSystemDoc() throws IOException { - RootDoc root = docs.index("java.util.ArrayList"); - - assertThat(root.classes(), not(emptyArray())); - } - - @Test - public void findMethodDoc() { - assertTrue( - "Found method", - docs.methodDoc( - "org.javacs.docs.TrickyDocstring#example(java.lang.String,java.lang.String[],java.util.List)") - .isPresent()); - } - - @Test - public void findParameterizedDoc() { - assertTrue( - "Found method", - docs.methodDoc("org.javacs.docs.TrickyDocstring#parameterized(java.lang.Object)") - .isPresent()); - } - - @Test - @Ignore // Blocked by emptyFileManager - public void findInheritedDoc() { - Optional<MethodDoc> found = docs.methodDoc("org.javacs.docs.SubDoc#method()"); - - assertTrue("Found method", found.isPresent()); - - Optional<String> docstring = found.flatMap(Javadocs::commentText); - - assertTrue("Has inherited doc", docstring.isPresent()); - assertThat("Inherited doc is not empty", docstring.get(), not(isEmptyOrNullString())); - } - - @Test - @Ignore // Doesn't work yet - public void findInterfaceDoc() { - Optional<MethodDoc> found = docs.methodDoc("org.javacs.docs.SubDoc#interfaceMethod()"); - - assertTrue("Found method", found.isPresent()); - - Optional<String> docstring = found.flatMap(Javadocs::commentText); - - assertTrue("Has inherited doc", docstring.isPresent()); - assertThat("Inherited doc is not empty", docstring.get(), not(isEmptyOrNullString())); - } -} diff --git a/src/test/java/org/javacs/LanguageServerFixture.java b/src/test/java/org/javacs/LanguageServerFixture.java index 3058b43..94ba764 100644 --- a/src/test/java/org/javacs/LanguageServerFixture.java +++ b/src/test/java/org/javacs/LanguageServerFixture.java @@ -5,25 +5,27 @@ import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.logging.Logger; -import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.MessageActionItem; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.ShowMessageRequestParams; import org.eclipse.lsp4j.services.LanguageClient; class LanguageServerFixture { - public static Path DEFAULT_WORKSPACE_ROOT = - Paths.get("src/test/test-project/workspace").toAbsolutePath(); + public static Path DEFAULT_WORKSPACE_ROOT = Paths.get("src/test/test-project/workspace").toAbsolutePath(); static { Main.setRootFormat(); } static JavaLanguageServer getJavaLanguageServer() { - return getJavaLanguageServer( - DEFAULT_WORKSPACE_ROOT, diagnostic -> LOG.info(diagnostic.getMessage())); + return getJavaLanguageServer(DEFAULT_WORKSPACE_ROOT, diagnostic -> LOG.info(diagnostic.getMessage())); } - static JavaLanguageServer getJavaLanguageServer( - Path workspaceRoot, Consumer<Diagnostic> onError) { + static JavaLanguageServer getJavaLanguageServer(Path workspaceRoot, Consumer<Diagnostic> onError) { return getJavaLanguageServer( workspaceRoot, new LanguageClient() { @@ -31,8 +33,7 @@ class LanguageServerFixture { public void telemetryEvent(Object o) {} @Override - public void publishDiagnostics( - PublishDiagnosticsParams publishDiagnosticsParams) { + public void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams) { publishDiagnosticsParams.getDiagnostics().forEach(onError); } @@ -50,18 +51,15 @@ class LanguageServerFixture { }); } - private static JavaLanguageServer getJavaLanguageServer( - Path workspaceRoot, LanguageClient client) { - JavaLanguageServer server = new JavaLanguageServer(); - InitializeParams init = new InitializeParams(); + private static JavaLanguageServer getJavaLanguageServer(Path workspaceRoot, LanguageClient client) { + var server = new JavaLanguageServer(); + var init = new InitializeParams(); - init.setRootPath(workspaceRoot.toString()); + init.setRootUri(workspaceRoot.toUri().toString()); server.initialize(init); server.installClient(client); - server.maxItems = 100; - return server; } diff --git a/src/test/java/org/javacs/LinterTest.java b/src/test/java/org/javacs/LinterTest.java deleted file mode 100644 index ab7468a..0000000 --- a/src/test/java/org/javacs/LinterTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; - -import java.io.IOException; -import java.net.URI; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Optional; -import java.util.logging.Logger; -import javax.tools.Diagnostic; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaFileObject; -import org.junit.Test; - -public class LinterTest { - private static final Logger LOG = Logger.getLogger("main"); - - private static final JavacHolder compiler = - JavacHolder.create( - Collections.singleton(Paths.get("src/test/test-project/workspace/src")), - Collections.emptySet()); - - @Test - public void compile() throws IOException { - URI file = FindResource.uri("/org/javacs/example/HelloWorld.java"); - DiagnosticCollector<JavaFileObject> errors = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - - assertThat(errors.getDiagnostics(), empty()); - } - - @Test - public void missingMethodBody() throws IOException { - URI file = FindResource.uri("/org/javacs/example/MissingMethodBody.java"); - DiagnosticCollector<JavaFileObject> compile = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - - assertThat(compile.getDiagnostics(), not(empty())); - } - - @Test - public void incompleteAssignment() throws IOException { - URI file = FindResource.uri("/org/javacs/example/IncompleteAssignment.java"); - DiagnosticCollector<JavaFileObject> compile = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - - assertThat(compile.getDiagnostics(), not(empty())); - } - - @Test - public void undefinedSymbol() throws IOException { - URI file = FindResource.uri("/org/javacs/example/UndefinedSymbol.java"); - DiagnosticCollector<JavaFileObject> compile = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - - assertThat(compile.getDiagnostics(), not(empty())); - - Diagnostic<? extends JavaFileObject> d = compile.getDiagnostics().get(0); - - // Error position should span entire 'foo' symbol - assertThat(d.getLineNumber(), greaterThan(0L)); - assertThat(d.getStartPosition(), greaterThan(0L)); - assertThat(d.getEndPosition(), greaterThan(d.getStartPosition() + 1)); - } - - @Test(expected = IllegalArgumentException.class) - public void notJava() { - URI file = FindResource.uri("/org/javacs/example/NotJava.java.txt"); - DiagnosticCollector<JavaFileObject> compile = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - } - - @Test - public void errorInDependency() { - URI file = FindResource.uri("/org/javacs/example/ErrorInDependency.java"); - DiagnosticCollector<JavaFileObject> compile = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - - assertThat(compile.getDiagnostics(), not(empty())); - } - - @Test - public void deprecationWarning() { - URI file = FindResource.uri("/org/javacs/example/DeprecationWarning.java"); - DiagnosticCollector<JavaFileObject> compile = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - - assertThat(compile.getDiagnostics(), not(empty())); - } - - @Test - public void parseError() { - URI file = URI.create("/org/javacs/example/ArrowTry.java"); - String source = - "package org.javacs.example;\n" - + "\n" - + "class Example {\n" - + " private static <In, Out> Function<In, Stream<Out>> catchClasspathErrors(Function<In, Stream<Out>> f) {\n" - + " return in -> try {\n" - + " return f.apply(in);\n" - + " } catch (Symbol.CompletionFailure failed) {\n" - + " LOG.warning(failed.getMessage());\n" - + " return Stream.empty();\n" - + " };\n" - + " }\n" - + "}"; - DiagnosticCollector<JavaFileObject> compile = - compiler.compileBatch(Collections.singletonMap(file, Optional.of(source))); - - assertThat(compile.getDiagnostics(), not(empty())); - } -} diff --git a/src/test/java/org/javacs/MainTest.java b/src/test/java/org/javacs/MainTest.java deleted file mode 100644 index 4aabeb8..0000000 --- a/src/test/java/org/javacs/MainTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import org.junit.Test; - -public class MainTest { - @Test - public void checkJavacClassLoader() - throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, - InvocationTargetException { - assertThat(Main.checkJavacClassLoader(), not(instanceOf(ChildFirstClassLoader.class))); - - ClassLoader langTools = LangTools.createLangToolsClassLoader(); - Class<?> main = langTools.loadClass("org.javacs.Main"); - assertThat( - "Main was loaded by ChildFirstClassLoader", - main.getClassLoader(), - instanceOf(ChildFirstClassLoader.class)); - Method checkJavacClassLoader = main.getMethod("checkJavacClassLoader"); - assertThat( - "JavacTool was loaded by ChildFirstClassLoader", - checkJavacClassLoader.invoke(null), - instanceOf(ChildFirstClassLoader.class)); - } -} diff --git a/src/test/java/org/javacs/ParserFixImportsTest.java b/src/test/java/org/javacs/ParserFixImportsTest.java new file mode 100644 index 0000000..eddd105 --- /dev/null +++ b/src/test/java/org/javacs/ParserFixImportsTest.java @@ -0,0 +1,16 @@ +package org.javacs; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Collections; +import org.junit.Test; + +public class ParserFixImportsTest { + @Test + public void findExistingImports() { + var find = Parser.existingImports(Collections.singleton(JavaCompilerServiceTest.resourcesDir())); + assertThat(find.classes, hasItem("java.util.List")); + assertThat(find.packages, hasItem("java.util")); + } +} diff --git a/src/test/java/org/javacs/ParserTest.java b/src/test/java/org/javacs/ParserTest.java new file mode 100644 index 0000000..580ce92 --- /dev/null +++ b/src/test/java/org/javacs/ParserTest.java @@ -0,0 +1,36 @@ +package org.javacs; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.nio.file.Paths; +import java.util.Set; +import org.junit.Test; + +public class ParserTest { + @Test + public void testMatchesTitleCase() { + assertTrue(Parser.matchesTitleCase("FooBar", "fb")); + assertTrue(Parser.matchesTitleCase("FooBar", "fob")); + assertTrue(Parser.matchesTitleCase("AnyPrefixFooBar", "fb")); + assertTrue(Parser.matchesTitleCase("AutocompleteBetweenLines", "ABetweenLines")); + assertTrue(Parser.matchesTitleCase("UPPERFooBar", "fb")); + assertFalse(Parser.matchesTitleCase("Foobar", "fb")); + } + + @Test + public void findAutocompleteBetweenLines() { + var rel = Paths.get("src", "org", "javacs", "example", "AutocompleteBetweenLines.java"); + var file = LanguageServerFixture.DEFAULT_WORKSPACE_ROOT.resolve(rel); + assertTrue(Parser.containsWordMatching(file, "ABetweenLines")); + } + + @Test + public void findExistingImports() { + var rel = Paths.get("src", "org", "javacs", "doimport"); + var dir = LanguageServerFixture.DEFAULT_WORKSPACE_ROOT.resolve(rel); + var existing = Parser.existingImports(Set.of(dir)); + assertThat(existing.classes, hasItems("java.util.List")); + assertThat(existing.packages, hasItems("java.util", "java.io")); + } +} diff --git a/src/test/java/org/javacs/PrunerTest.java b/src/test/java/org/javacs/PrunerTest.java new file mode 100644 index 0000000..23c825c --- /dev/null +++ b/src/test/java/org/javacs/PrunerTest.java @@ -0,0 +1,54 @@ +package org.javacs; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.util.stream.Collectors; +import org.junit.Test; + +public class PrunerTest { + + private String contents(String resourceFile) { + try (var in = JavaCompilerServiceTest.class.getResourceAsStream(resourceFile)) { + return new BufferedReader(new InputStreamReader(in)).lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void pruneMethods() { + var pruner = new Pruner(URI.create("/PruneMethods.java"), contents("/PruneMethods.java")); + pruner.prune(6, 19); + var expected = contents("/PruneMethods_erased.java"); + assertThat(pruner.contents(), equalToIgnoringWhiteSpace(expected)); + } + + @Test + public void pruneToEndOfBlock() { + var pruner = new Pruner(URI.create("/PruneToEndOfBlock.java"), contents("/PruneToEndOfBlock.java")); + pruner.prune(4, 18); + var expected = contents("/PruneToEndOfBlock_erased.java"); + assertThat(pruner.contents(), equalToIgnoringWhiteSpace(expected)); + } + + @Test + public void pruneMiddle() { + var pruner = new Pruner(URI.create("/PruneMiddle.java"), contents("/PruneMiddle.java")); + pruner.prune(4, 12); + var expected = contents("/PruneMiddle_erased.java"); + assertThat(pruner.contents(), equalToIgnoringWhiteSpace(expected)); + } + + @Test + public void pruneDot() { + var pruner = new Pruner(URI.create("/PruneDot.java"), contents("/PruneDot.java")); + pruner.prune(3, 11); + var expected = contents("/PruneDot_erased.java"); + assertThat(pruner.contents(), equalToIgnoringWhiteSpace(expected)); + } +} diff --git a/src/test/java/org/javacs/RecompileTest.java b/src/test/java/org/javacs/RecompileTest.java deleted file mode 100644 index 103e318..0000000 --- a/src/test/java/org/javacs/RecompileTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertThat; - -import java.io.IOException; -import java.net.URI; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Optional; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaFileObject; -import org.junit.Test; - -public class RecompileTest { - @Test - public void compileTwice() { - URI file = FindResource.uri("/org/javacs/example/CompileTwice.java"); - JavacHolder compiler = newCompiler(); - DiagnosticCollector<JavaFileObject> compile = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - - assertThat(compile.getDiagnostics(), empty()); - - // Compile again - compile = compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - - assertThat(compile.getDiagnostics(), empty()); - } - - @Test - public void fixParseError() { - URI bad = FindResource.uri("/org/javacs/example/FixParseErrorBefore.java"); - URI good = FindResource.uri("/org/javacs/example/FixParseErrorAfter.java"); - JavacHolder compiler = newCompiler(); - DiagnosticCollector<JavaFileObject> badErrors = - compiler.compileBatch(Collections.singletonMap(bad, Optional.empty())); - - assertThat(badErrors.getDiagnostics(), not(empty())); - - // Parse again - DiagnosticCollector<JavaFileObject> goodErrors = - compiler.compileBatch(Collections.singletonMap(good, Optional.empty())); - - assertThat(goodErrors.getDiagnostics(), empty()); - } - - @Test - public void fixTypeError() { - URI bad = FindResource.uri("/org/javacs/example/FixTypeErrorBefore.java"); - URI good = FindResource.uri("/org/javacs/example/FixTypeErrorAfter.java"); - JavacHolder compiler = newCompiler(); - DiagnosticCollector<JavaFileObject> badErrors = - compiler.compileBatch(Collections.singletonMap(bad, Optional.empty())); - - assertThat(badErrors.getDiagnostics(), not(empty())); - - // Parse again - DiagnosticCollector<JavaFileObject> goodErrors = - compiler.compileBatch(Collections.singletonMap(good, Optional.empty())); - - assertThat(goodErrors.getDiagnostics(), empty()); - } - - private static JavacHolder newCompiler() { - return JavacHolder.create( - Collections.singleton(Paths.get("src/test/test-project/workspace/src")), - Collections.emptySet()); - } - - @Test - public void keepTypeError() throws IOException { - URI file = FindResource.uri("/org/javacs/example/UndefinedSymbol.java"); - JavacHolder compiler = newCompiler(); - - // Compile once - DiagnosticCollector<JavaFileObject> errors = - compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - assertThat(errors.getDiagnostics(), not(empty())); - - // Compile twice - errors = compiler.compileBatch(Collections.singletonMap(file, Optional.empty())); - - assertThat(errors.getDiagnostics(), not(empty())); - } -} diff --git a/src/test/java/org/javacs/RefactorFileTest.java b/src/test/java/org/javacs/RefactorFileTest.java deleted file mode 100644 index 957ff15..0000000 --- a/src/test/java/org/javacs/RefactorFileTest.java +++ /dev/null @@ -1,223 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertThat; - -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.net.URI; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.logging.Logger; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.TextEdit; -import org.junit.Test; - -public class RefactorFileTest { - - private static final Logger LOG = Logger.getLogger("main"); - private static final URI FAKE_FILE = - URI.create("test/imaginary-resources/org/javacs/Example.java"); - - @Test - public void addImportToEmpty() { - String before = "package org.javacs;\n" + "\n" + "public class Example { void main() { } }"; - List<TextEdit> edits = addImport(before, "org.javacs", "Foo"); - String after = applyEdits(before, edits); - - assertThat( - after, - equalTo( - "package org.javacs;\n" - + "\n" - + "import org.javacs.Foo;\n" - + "\n" - + "public class Example { void main() { } }")); - } - - @Test - public void addImportToExisting() { - String before = - "package org.javacs;\n" - + "\n" - + "import java.util.List;\n" - + "\n" - + "public class Example { void main() { } }"; - List<TextEdit> edits = addImport(before, "org.javacs", "Foo"); - String after = applyEdits(before, edits); - - assertThat( - after, - equalTo( - "package org.javacs;\n" - + "\n" - + "import java.util.List;\n" - + "import org.javacs.Foo;\n" - + "\n" - + "public class Example { void main() { } }")); - } - - @Test - public void addImportAtBeginning() { - String before = - "package org.javacs;\n" - + "\n" - + "import org.javacs.Foo;\n" - + "\n" - + "public class Example { void main() { } }"; - List<TextEdit> edits = addImport(before, "java.util", "List"); - String after = applyEdits(before, edits); - - assertThat( - after, - equalTo( - "package org.javacs;\n" - + "\n" - + "import java.util.List;\n" - + "import org.javacs.Foo;\n" - + "\n" - + "public class Example { void main() { } }")); - } - - @Test - public void importAlreadyExists() { - String before = - "package org.javacs;\n" - + "\n" - + "import java.util.List;\n" - + "\n" - + "public class Example { void main() { } }"; - List<TextEdit> edits = addImport(before, "java.util", "List"); - String after = applyEdits(before, edits); - - assertThat( - after, - equalTo( - "package org.javacs;\n" - + "\n" - + "import java.util.List;\n" - + "\n" - + "public class Example { void main() { } }")); - } - - @Test - public void noPackage() { - String before = - "import java.util.List;\n" + "\n" + "public class Example { void main() { } }"; - List<TextEdit> edits = addImport(before, "org.javacs", "Foo"); - String after = applyEdits(before, edits); - - assertThat( - after, - equalTo( - "import java.util.List;\n" - + "import org.javacs.Foo;\n" - + "\n" - + "public class Example { void main() { } }")); - } - - @Test - public void noPackageNoImports() { - String before = "public class Example { void main() { } }"; - List<TextEdit> edits = addImport(before, "org.javacs", "Foo"); - String after = applyEdits(before, edits); - - assertThat( - after, - equalTo( - "import org.javacs.Foo;\n" - + "\n" - + "public class Example { void main() { } }")); - } - - private List<TextEdit> addImport(String content, String packageName, String className) { - List<TextEdit> result = new ArrayList<>(); - JavacHolder compiler = - JavacHolder.create( - Collections.singleton(Paths.get("src/test/test-project/workspace/src")), - Collections.emptySet()); - BiConsumer<JavacTask, CompilationUnitTree> doRefactor = - (task, tree) -> { - List<TextEdit> edits = - new RefactorFile(task, tree).addImport(packageName, className); - - result.addAll(edits); - }; - - compiler.compileBatch( - Collections.singletonMap(FAKE_FILE, Optional.of(content)), doRefactor); - - return result; - } - - private String applyEdits(String before, List<TextEdit> edits) { - StringBuffer buffer = new StringBuffer(before); - - edits.stream().sorted(this::compareEdits).forEach(edit -> applyEdit(buffer, edit)); - - return buffer.toString(); - } - - private int compareEdits(TextEdit left, TextEdit right) { - int compareLines = - -Integer.compare( - left.getRange().getEnd().getLine(), right.getRange().getEnd().getLine()); - - if (compareLines != 0) return compareLines; - else - return -Integer.compare( - left.getRange().getStart().getCharacter(), - right.getRange().getEnd().getCharacter()); - } - - private void applyEdit(StringBuffer buffer, TextEdit edit) { - buffer.replace( - offset(edit.getRange().getStart(), buffer), - offset(edit.getRange().getEnd(), buffer), - edit.getNewText()); - } - - private int offset(Position pos, StringBuffer buffer) { - return (int) findOffset(buffer.toString(), pos.getLine(), pos.getCharacter()); - } - - public static long findOffset(String content, int targetLine, int targetCharacter) { - try (Reader in = new StringReader(content)) { - long offset = 0; - int line = 0; - int character = 0; - - while (line < targetLine) { - int next = in.read(); - - if (next < 0) return offset; - else { - offset++; - - if (next == '\n') line++; - } - } - - while (character < targetCharacter) { - int next = in.read(); - - if (next < 0) return offset; - else { - offset++; - character++; - } - } - - return offset; - } catch (IOException e) { - throw ShowMessageException.error(e.getMessage(), e); - } - } -} diff --git a/src/test/java/org/javacs/SearchTest.java b/src/test/java/org/javacs/SearchTest.java index b92b0e1..f99bba4 100644 --- a/src/test/java/org/javacs/SearchTest.java +++ b/src/test/java/org/javacs/SearchTest.java @@ -6,7 +6,6 @@ import static org.junit.Assert.assertThat; import com.google.common.base.Joiner; import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Set; @@ -23,10 +22,10 @@ public class SearchTest { private static final JavaLanguageServer server = LanguageServerFixture.getJavaLanguageServer(); @BeforeClass - public static void openSource() throws URISyntaxException, IOException { - URI uri = FindResource.uri("/org/javacs/example/AutocompleteBetweenLines.java"); - String textContent = Joiner.on("\n").join(Files.readAllLines(Paths.get(uri))); - TextDocumentItem document = new TextDocumentItem(); + public static void openSource() throws IOException { + var uri = FindResource.uri("/org/javacs/example/AutocompleteBetweenLines.java"); + var textContent = Joiner.on("\n").join(Files.readAllLines(Paths.get(uri))); + var document = new TextDocumentItem(); document.setUri(uri.toString()); document.setText(textContent); @@ -34,13 +33,14 @@ public class SearchTest { server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(document, null)); } - private static Set<String> searchWorkspace(String query) { + private static Set<String> searchWorkspace(String query, int limit) { try { return server.getWorkspaceService() .symbol(new WorkspaceSymbolParams(query)) .get() .stream() .map(result -> result.getName()) + .limit(limit) .collect(Collectors.toSet()); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); @@ -50,8 +50,7 @@ public class SearchTest { private static Set<String> searchFile(URI uri) { try { return server.getTextDocumentService() - .documentSymbol( - new DocumentSymbolParams(new TextDocumentIdentifier(uri.toString()))) + .documentSymbol(new DocumentSymbolParams(new TextDocumentIdentifier(uri.toString()))) .get() .stream() .map(result -> result.getName()) @@ -63,29 +62,29 @@ public class SearchTest { @Test public void all() { - Set<String> all = searchWorkspace(""); + var all = searchWorkspace("", 100); assertThat(all, not(empty())); } @Test public void searchClasses() { - Set<String> all = searchWorkspace("ABetweenLines"); + var all = searchWorkspace("ABetweenLines", Integer.MAX_VALUE); assertThat(all, hasItem("AutocompleteBetweenLines")); } @Test public void searchMethods() { - Set<String> all = searchWorkspace("mStatic"); + var all = searchWorkspace("mStatic", Integer.MAX_VALUE); assertThat(all, hasItem("methodStatic")); } @Test public void symbolsInFile() { - String path = "/org/javacs/example/AutocompleteMemberFixed.java"; - Set<String> all = searchFile(FindResource.uri(path)); + var path = "/org/javacs/example/AutocompleteMemberFixed.java"; + var all = searchFile(FindResource.uri(path)); assertThat( all, @@ -102,8 +101,8 @@ public class SearchTest { @Test public void explicitConstructor() { - String path = "/org/javacs/example/ReferenceConstructor.java"; - Set<String> all = searchFile(FindResource.uri(path)); + var path = "/org/javacs/example/ReferenceConstructor.java"; + var all = searchFile(FindResource.uri(path)); assertThat("includes explicit constructor", all, hasItem("ReferenceConstructor")); } diff --git a/src/test/java/org/javacs/SignatureHelpTest.java b/src/test/java/org/javacs/SignatureHelpTest.java index ee53d91..125e6fb 100644 --- a/src/test/java/org/javacs/SignatureHelpTest.java +++ b/src/test/java/org/javacs/SignatureHelpTest.java @@ -14,14 +14,14 @@ import org.junit.Test; public class SignatureHelpTest { @Test public void signatureHelp() throws IOException { - SignatureHelp help = doHelp("/org/javacs/example/SignatureHelp.java", 7, 36); + var help = doHelp("/org/javacs/example/SignatureHelp.java", 7, 36); assertThat(help.getSignatures(), hasSize(2)); } @Test public void partlyFilledIn() throws IOException { - SignatureHelp help = doHelp("/org/javacs/example/SignatureHelp.java", 8, 39); + var help = doHelp("/org/javacs/example/SignatureHelp.java", 8, 39); assertThat(help.getSignatures(), hasSize(2)); assertThat(help.getActiveSignature(), equalTo(1)); @@ -30,7 +30,7 @@ public class SignatureHelpTest { @Test public void constructor() throws IOException { - SignatureHelp help = doHelp("/org/javacs/example/SignatureHelp.java", 9, 27); + var help = doHelp("/org/javacs/example/SignatureHelp.java", 9, 27); assertThat(help.getSignatures(), hasSize(1)); assertThat(help.getSignatures().get(0).getLabel(), startsWith("SignatureHelp")); @@ -38,26 +38,27 @@ public class SignatureHelpTest { @Test public void platformConstructor() throws IOException { - SignatureHelp help = doHelp("/org/javacs/example/SignatureHelp.java", 10, 26); + var help = doHelp("/org/javacs/example/SignatureHelp.java", 10, 26); assertThat(help.getSignatures(), not(empty())); assertThat(help.getSignatures().get(0).getLabel(), startsWith("ArrayList")); - assertThat(help.getSignatures().get(0).getDocumentation(), not(nullValue())); + // TODO + // assertThat(help.getSignatures().get(0).getDocumentation(), not(nullValue())); } private static final JavaLanguageServer server = LanguageServerFixture.getJavaLanguageServer(); private SignatureHelp doHelp(String file, int row, int column) throws IOException { - TextDocumentIdentifier document = new TextDocumentIdentifier(); + var document = new TextDocumentIdentifier(); document.setUri(FindResource.uri(file).toString()); - Position position = new Position(); + var position = new Position(); position.setLine(row - 1); position.setCharacter(column - 1); - TextDocumentPositionParams p = new TextDocumentPositionParams(); + var p = new TextDocumentPositionParams(); p.setTextDocument(document); p.setPosition(position); diff --git a/src/test/java/org/javacs/SymbolIndexTest.java b/src/test/java/org/javacs/SymbolIndexTest.java deleted file mode 100644 index 285a5cb..0000000 --- a/src/test/java/org/javacs/SymbolIndexTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Optional; -import org.junit.Test; - -public class SymbolIndexTest { - private Path workspaceRoot = Paths.get("src/test/test-project/workspace"); - private SymbolIndex index = - new SymbolIndex(workspaceRoot, () -> Collections.emptySet(), __ -> Optional.empty()); - - @Test - public void workspaceSourcePath() { - assertThat(index.sourcePath(), contains(workspaceRoot.resolve("src").toAbsolutePath())); - } -} diff --git a/src/test/java/org/javacs/SymbolUnderCursorTest.java b/src/test/java/org/javacs/SymbolUnderCursorTest.java index 0f44f5c..e090834 100644 --- a/src/test/java/org/javacs/SymbolUnderCursorTest.java +++ b/src/test/java/org/javacs/SymbolUnderCursorTest.java @@ -1,9 +1,13 @@ package org.javacs; -import static org.junit.Assert.assertEquals; - -import java.util.Optional; -import javax.lang.model.element.Element; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.StringJoiner; +import java.util.concurrent.ExecutionException; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentPositionParams; import org.junit.Ignore; import org.junit.Test; @@ -11,85 +15,108 @@ public class SymbolUnderCursorTest { @Test public void classDeclaration() { - assertEquals( - "SymbolUnderCursor", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 3, 22)); + assertThat( + symbolAt("/org/javacs/example/SymbolUnderCursor.java", 3, 22), + containsString("org.javacs.example.SymbolUnderCursor")); } @Test public void fieldDeclaration() { - assertEquals("field", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 4, 22)); + assertThat(symbolAt("/org/javacs/example/SymbolUnderCursor.java", 4, 22), containsString("field")); } @Test public void methodDeclaration() { - assertEquals("method", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 6, 22)); + assertThat( + symbolAt("/org/javacs/example/SymbolUnderCursor.java", 6, 22), + containsString("method(String methodParameter)")); } @Test public void methodParameterDeclaration() { - assertEquals( - "methodParameter", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 6, 36)); + assertThat(symbolAt("/org/javacs/example/SymbolUnderCursor.java", 6, 36), containsString("methodParameter")); } @Test public void localVariableDeclaration() { - assertEquals( - "localVariable", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 7, 22)); + assertThat(symbolAt("/org/javacs/example/SymbolUnderCursor.java", 7, 22), containsString("localVariable")); } @Test public void constructorParameterDeclaration() { - assertEquals( - "constructorParameter", - symbolAt("/org/javacs/example/SymbolUnderCursor.java", 17, 46)); + assertThat( + symbolAt("/org/javacs/example/SymbolUnderCursor.java", 17, 46), containsString("constructorParameter")); } @Test public void classIdentifier() { - assertEquals( - "SymbolUnderCursor", - symbolAt("/org/javacs/example/SymbolUnderCursor.java", 12, 23)); + assertThat( + symbolAt("/org/javacs/example/SymbolUnderCursor.java", 12, 23), + containsString("org.javacs.example.SymbolUnderCursor")); } @Test public void fieldIdentifier() { - assertEquals("field", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 9, 27)); + assertThat(symbolAt("/org/javacs/example/SymbolUnderCursor.java", 9, 27), containsString("field")); } @Test public void methodIdentifier() { - assertEquals("method", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 12, 12)); + assertThat( + symbolAt("/org/javacs/example/SymbolUnderCursor.java", 12, 12), + containsString("method(String methodParameter)")); } @Test public void methodSelect() { - assertEquals("method", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 13, 17)); + assertThat( + symbolAt("/org/javacs/example/SymbolUnderCursor.java", 13, 17), + containsString("method(String methodParameter)")); } @Ignore // tree.sym is null @Test public void methodReference() { - assertEquals("method", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 14, 46)); + assertThat(symbolAt("/org/javacs/example/SymbolUnderCursor.java", 14, 65), containsString("method")); + } + + @Test + public void annotationUse() { + var found = symbolAt("/org/javacs/example/SymbolUnderCursor.java", 21, 8); + assertThat(found, containsString("@interface Override")); + assertThat(found, not(containsString("extends none"))); } @Test public void methodParameterReference() { - assertEquals( - "methodParameter", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 10, 32)); + assertThat(symbolAt("/org/javacs/example/SymbolUnderCursor.java", 10, 32), containsString("methodParameter")); } @Test public void localVariableReference() { - assertEquals( - "localVariable", symbolAt("/org/javacs/example/SymbolUnderCursor.java", 10, 16)); + assertThat(symbolAt("/org/javacs/example/SymbolUnderCursor.java", 10, 16), containsString("localVariable")); } - // Re-using the language server makes these tests go a lot faster, but it will potentially produce surprising output if things go wrong + // Re-using the language server makes these tests go a lot faster, but it will potentially produce surprising output + // if things go wrong private static final JavaLanguageServer server = LanguageServerFixture.getJavaLanguageServer(); private String symbolAt(String file, int line, int character) { - Optional<Element> symbol = server.findSymbol(FindResource.uri(file), line, character); - - return symbol.map(s -> s.getSimpleName().toString()).orElse(null); + var pos = + new TextDocumentPositionParams( + new TextDocumentIdentifier(FindResource.uri(file).toString()), + new Position(line - 1, character - 1)); + var result = new StringJoiner("\n"); + try { + server.getTextDocumentService() + .hover(pos) + .get() + .getContents() + .getLeft() + .forEach(hover -> result.add(hover.isLeft() ? hover.getLeft() : hover.getRight().getValue())); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + return result.toString(); } } diff --git a/src/test/java/org/javacs/TreePrunerTest.java b/src/test/java/org/javacs/TreePrunerTest.java deleted file mode 100644 index 0aa5e18..0000000 --- a/src/test/java/org/javacs/TreePrunerTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.equalToIgnoringWhiteSpace; -import static org.junit.Assert.assertThat; - -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; -import com.sun.tools.javac.api.JavacTool; -import java.io.IOException; -import java.net.URI; -import java.util.Collections; -import java.util.logging.Logger; -import javax.tools.JavaFileObject; -import org.junit.Test; - -public class TreePrunerTest { - private static final Logger LOG = Logger.getLogger("main"); - - private JavacTask task(String source) { - return JavacTool.create() - .getTask( - null, - null, - err -> LOG.warning(err.getMessage(null)), - Collections.emptyList(), - Collections.emptyList(), - Collections.singleton( - new StringFileObject(source, URI.create("FakePath.java")))); - } - - @Test - public void putSemicolonAfterCursor() throws IOException { - String source = - "public class Example {\n" + " void main() {\n" + " this.m\n" + " }\n" + "}\n"; - JavaFileObject after = - TreePruner.putSemicolonAfterCursor( - new StringFileObject(source, URI.create("Example.java")), 3, 11); - - assertThat( - after.getCharContent(true).toString(), - equalTo( - "public class Example {\n" - + " void main() {\n" - + " this.m;\n" - + " }\n" - + "}\n")); - } - - @Test - public void putSemicolonAtEndOfLine() throws IOException { - String source = - "public class Example {\n" - + " void main() {\n" - + " this.main\n" - + " }\n" - + "}\n"; - JavaFileObject after = - TreePruner.putSemicolonAfterCursor( - new StringFileObject(source, URI.create("Example.java")), 3, 11); - - assertThat( - after.getCharContent(true).toString(), - equalTo( - "public class Example {\n" - + " void main() {\n" - + " this.main;\n" - + " }\n" - + "}\n")); - } - - @Test - public void removeStatementsAfterCursor() throws IOException { - String source = - "public class Example {\n" - + " void main() {\n" - + " foo()\n" - + " bar()\n" - + " doh()\n" - + " }\n" - + "}\n"; - JavacTask task = task(source); - CompilationUnitTree tree = task.parse().iterator().next(); - - new TreePruner(task).removeStatementsAfterCursor(tree, 4, 6); - - assertThat( - tree.toString(), - equalToIgnoringWhiteSpace( - "\n" - + "public class Example {\n" - + " \n" - + " void main() {\n" - + " foo();\n" - + " bar();\n" - + " }\n" - + "}")); - } -} diff --git a/src/test/resources/BuildUpScope.java b/src/test/resources/BuildUpScope.java new file mode 100644 index 0000000..171d816 --- /dev/null +++ b/src/test/resources/BuildUpScope.java @@ -0,0 +1,8 @@ +class BuildUpScope { + void main() { + int a = 1; + int b = 2; + int c = 3; + } + void otherMethod() { } +}
\ No newline at end of file diff --git a/src/test/resources/ClassDoc.java b/src/test/resources/ClassDoc.java new file mode 100644 index 0000000..9106496 --- /dev/null +++ b/src/test/resources/ClassDoc.java @@ -0,0 +1,4 @@ +/** A great class */ +class ClassDoc { + +}
\ No newline at end of file diff --git a/src/test/resources/CompleteClass.java b/src/test/resources/CompleteClass.java new file mode 100644 index 0000000..5f16fb8 --- /dev/null +++ b/src/test/resources/CompleteClass.java @@ -0,0 +1,13 @@ +class CompleteClass { + void test() { + CompleteClass. + } + + static void staticMethod() { } + + void instanceMethod() { } + + static int staticField; + + int instanceField; +}
\ No newline at end of file diff --git a/src/test/resources/CompleteExpression.java b/src/test/resources/CompleteExpression.java new file mode 100644 index 0000000..9b9dd00 --- /dev/null +++ b/src/test/resources/CompleteExpression.java @@ -0,0 +1,13 @@ +class CompleteExpression { + void test() { + CompleteExpression.create(). + } + + static CompleteExpression create() { + return new CompleteExpression(); + } + + int instanceMethod() { + return 1; + } +}
\ No newline at end of file diff --git a/src/test/resources/CompleteIdentifiers.java b/src/test/resources/CompleteIdentifiers.java new file mode 100644 index 0000000..8ba5e39 --- /dev/null +++ b/src/test/resources/CompleteIdentifiers.java @@ -0,0 +1,16 @@ +class CompleteIdentifiers { + static int completeOuterStatic; + int completeOuterField; + + class CompleteInnerClass { + int completeInnerField; + + void completeOtherMethod() { + } + + void test(int completeParam) { + int completeLocal = 1; + complete + } + } +}
\ No newline at end of file diff --git a/src/test/resources/CompleteImports.java b/src/test/resources/CompleteImports.java new file mode 100644 index 0000000..1b0dc80 --- /dev/null +++ b/src/test/resources/CompleteImports.java @@ -0,0 +1 @@ +import java.util.
\ No newline at end of file diff --git a/src/test/resources/CompleteInMiddle.java b/src/test/resources/CompleteInMiddle.java new file mode 100644 index 0000000..981e71e --- /dev/null +++ b/src/test/resources/CompleteInMiddle.java @@ -0,0 +1,17 @@ +class CompleteInMiddle { + static int completeOuterStatic; + int completeOuterField; + + class CompleteInnerClass { + int completeInnerField; + + void completeOtherMethod() { + } + + void test(int completeParam) { + int completeLocal = 1; + complete + int localAfter = 2; + } + } +}
\ No newline at end of file diff --git a/src/test/resources/CompleteMembers.java b/src/test/resources/CompleteMembers.java new file mode 100644 index 0000000..168dc65 --- /dev/null +++ b/src/test/resources/CompleteMembers.java @@ -0,0 +1,17 @@ +class CompleteMembers { + void test(Sub param) { + param. + } + + class Super { + void superMethod() { + + } + } + + class Sub extends Super { + void subMethod() { + + } + } +}
\ No newline at end of file diff --git a/src/test/resources/GotoDefinition.java b/src/test/resources/GotoDefinition.java new file mode 100644 index 0000000..1f8866c --- /dev/null +++ b/src/test/resources/GotoDefinition.java @@ -0,0 +1,8 @@ +class GotoDefinition { + void test() { + goToHere(); + } + + void goToHere() { + } +}
\ No newline at end of file diff --git a/src/test/resources/HasError.java b/src/test/resources/HasError.java new file mode 100644 index 0000000..ad2fbe2 --- /dev/null +++ b/src/test/resources/HasError.java @@ -0,0 +1,5 @@ +class HasError { + void test() { + String x = 1; + } +}
\ No newline at end of file diff --git a/src/test/resources/HasImport.java b/src/test/resources/HasImport.java new file mode 100644 index 0000000..2b4f66e --- /dev/null +++ b/src/test/resources/HasImport.java @@ -0,0 +1,5 @@ +import java.util.List; + +class HasImport { + +}
\ No newline at end of file diff --git a/src/test/resources/HelloWorld.java b/src/test/resources/HelloWorld.java new file mode 100644 index 0000000..b86724f --- /dev/null +++ b/src/test/resources/HelloWorld.java @@ -0,0 +1,5 @@ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world!"); + } +}
\ No newline at end of file diff --git a/src/test/resources/LocalMethodDoc.java b/src/test/resources/LocalMethodDoc.java new file mode 100644 index 0000000..a240753 --- /dev/null +++ b/src/test/resources/LocalMethodDoc.java @@ -0,0 +1,12 @@ +class LocalMethodDoc { + void testMethod() { + targetMethod(1); + } + + /** + * A great method + * @param param A great param + */ + void targetMethod(int param) { + } +}
\ No newline at end of file diff --git a/src/test/resources/MissingImport.java b/src/test/resources/MissingImport.java new file mode 100644 index 0000000..79f0d92 --- /dev/null +++ b/src/test/resources/MissingImport.java @@ -0,0 +1,5 @@ +class MissingImport { + void test() { + List<String> x = null; + } +}
\ No newline at end of file diff --git a/src/test/resources/Overloads.java b/src/test/resources/Overloads.java new file mode 100644 index 0000000..e686971 --- /dev/null +++ b/src/test/resources/Overloads.java @@ -0,0 +1,8 @@ +class Overloads { + void test() { + print() + } + + void print(int i) { } + void print(String s) { } +}
\ No newline at end of file diff --git a/src/test/resources/PruneDot.java b/src/test/resources/PruneDot.java new file mode 100644 index 0000000..2ea5e1d --- /dev/null +++ b/src/test/resources/PruneDot.java @@ -0,0 +1,5 @@ +class PruneMiddle { + void test(String a) { + a. + } +}
\ No newline at end of file diff --git a/src/test/resources/PruneDot_erased.java b/src/test/resources/PruneDot_erased.java new file mode 100644 index 0000000..2ea5e1d --- /dev/null +++ b/src/test/resources/PruneDot_erased.java @@ -0,0 +1,5 @@ +class PruneMiddle { + void test(String a) { + a. + } +}
\ No newline at end of file diff --git a/src/test/resources/PruneMethods.java b/src/test/resources/PruneMethods.java new file mode 100644 index 0000000..474cee2 --- /dev/null +++ b/src/test/resources/PruneMethods.java @@ -0,0 +1,8 @@ +class PruneMethods { + void a() { + int a = 1; + } + void b() { + int b = 2; + } +}
\ No newline at end of file diff --git a/src/test/resources/PruneMethods_erased.java b/src/test/resources/PruneMethods_erased.java new file mode 100644 index 0000000..e3d1a4b --- /dev/null +++ b/src/test/resources/PruneMethods_erased.java @@ -0,0 +1,8 @@ +class PruneMethods { + void a() { + + } + void b() { + int b = 2; + } +}
\ No newline at end of file diff --git a/src/test/resources/PruneMiddle.java b/src/test/resources/PruneMiddle.java new file mode 100644 index 0000000..1916250 --- /dev/null +++ b/src/test/resources/PruneMiddle.java @@ -0,0 +1,7 @@ +class PruneMiddle { + void test() { + var a = "foo"; + a.l + var c = 3; + } +}
\ No newline at end of file diff --git a/src/test/resources/PruneMiddle_erased.java b/src/test/resources/PruneMiddle_erased.java new file mode 100644 index 0000000..5b208b3 --- /dev/null +++ b/src/test/resources/PruneMiddle_erased.java @@ -0,0 +1,7 @@ +class PruneMiddle { + void test() { + var a = "foo"; + a.l + + } +}
\ No newline at end of file diff --git a/src/test/resources/PruneToEndOfBlock.java b/src/test/resources/PruneToEndOfBlock.java new file mode 100644 index 0000000..9174589 --- /dev/null +++ b/src/test/resources/PruneToEndOfBlock.java @@ -0,0 +1,7 @@ +class PruneToEndOfBlock { + void test() { + int a = 1; + int b = 2; + int c = 3; + } +}
\ No newline at end of file diff --git a/src/test/resources/PruneToEndOfBlock_erased.java b/src/test/resources/PruneToEndOfBlock_erased.java new file mode 100644 index 0000000..fafee1f --- /dev/null +++ b/src/test/resources/PruneToEndOfBlock_erased.java @@ -0,0 +1,7 @@ +class PruneToEndOfBlock { + void test() { + int a = 1; + int b = 2; + + } +}
\ No newline at end of file diff --git a/src/test/test-project/bazel-temp/xyz/execroot/test/bazel-out/local-fastbuild/bin/bazel-bin b/src/test/test-project/bazel-temp/xyz/execroot/test/bazel-out/local-fastbuild/bin/bazel-bin new file mode 120000 index 0000000..86b13ae --- /dev/null +++ b/src/test/test-project/bazel-temp/xyz/execroot/test/bazel-out/local-fastbuild/bin/bazel-bin @@ -0,0 +1 @@ +bazel-bin
\ No newline at end of file diff --git a/src/test/test-project/bazel-workspace/module/placeholder.txt b/src/test/test-project/bazel-workspace/module/placeholder.txt new file mode 100644 index 0000000..a0b89c4 --- /dev/null +++ b/src/test/test-project/bazel-workspace/module/placeholder.txt @@ -0,0 +1 @@ +Make sure module dir gets checked into git
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/doimport/ImportThings.java b/src/test/test-project/workspace/src/org/javacs/doimport/ImportThings.java new file mode 100644 index 0000000..df6e96a --- /dev/null +++ b/src/test/test-project/workspace/src/org/javacs/doimport/ImportThings.java @@ -0,0 +1,8 @@ +package org.javacs.doimport; + +import java.util.List; +import java.io.*; + +class ImportThings { + +}
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteClasses.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteClasses.java index a92b404..bddf9f4 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteClasses.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteClasses.java @@ -2,7 +2,8 @@ package org.javacs.example; public class AutocompleteClasses { public static void test() { - S; + Fix; + Some; } public static class SomeInnerClass { diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteInners.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteInners.java index 608e1f2..33f4566 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteInners.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteInners.java @@ -8,7 +8,7 @@ public class AutocompleteInners { public void testReference() { new AutocompleteInners.I; - new I; + new Inner; } public void testEnum() { diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteMember.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteMember.java index 3c555e2..fc63d6f 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteMember.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteMember.java @@ -5,21 +5,21 @@ public class AutocompleteMember { this.; } - public static String fieldStatic; - public String fields; - public static String methodStatic() { + public static String testFieldStatic; + public String testFields; + public static String testMethodStatic() { return "foo"; } - public String methods() throws Exception { + public String testMethods() throws Exception { return "foo"; } - private static String fieldStaticPrivate; - private String fieldsPrivate; - private static String methodStaticPrivate() { + private static String testFieldStaticPrivate; + private String testFieldsPrivate; + private static String testMethodStaticPrivate() { return "foo"; } - private String methodsPrivate() { + private String testMethodsPrivate() { return "foo"; } }
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteMembers.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteMembers.java index ddc19c8..76431bc 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteMembers.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteMembers.java @@ -1,34 +1,34 @@ package org.javacs.example; public class AutocompleteMembers { - private String fields; - private static String fieldStatic; + private String testFields; + private static String testFieldStatic; { - s; // fields, fieldStatic, methods, methodStatic - this.s; // fields, methods - AutocompleteMembers.s; // fieldStatic, methodStatic - this::s; // methods - AutocompleteMembers::s; // methods, methodStatic + t; // testFields, testFieldStatic, testMethods, testMethodStatic + this.t; // testFields, testMethods + AutocompleteMembers.t; // testFieldStatic, testMethodStatic + this::t; // testMethods + AutocompleteMembers::t; // testMethods, testMethodStatic } static { - s; // fieldStatic - AutocompleteMembers.s; // fieldStatic - AutocompleteMembers::s; // methods, methodStatic + t; // testFieldStatic + AutocompleteMembers.t; // testFieldStatic + AutocompleteMembers::t; // testMethods, testMethodStatic } - private void methods(String arguments) { - s; // fields, fieldStatic, methods, methodStatic, arguments - this.s; // fields, methods - AutocompleteMembers.s; // fieldStatic, methodStatic - java.util.function.Supplier<String> test = this::s; // methods - java.util.function.Supplier<String> test = AutocompleteMembers::s; // methods, methodStatic + private void testMethods(String testArguments) { + t; // testFields, testFieldStatic, testMethods, testMethodStatic, testArguments + this.t; // testFields, testMethods + AutocompleteMembers.t; // testFieldStatic, testMethodStatic + java.util.function.Supplier<String> test = this::t; // testMethods + java.util.function.Supplier<String> test = AutocompleteMembers::t; // testMethods, testMethodStatic } - private static void methodStatic(String arguments) { - s; // fieldStatic, arguments - AutocompleteMembers.s; // fieldStatic - AutocompleteMembers::s; // methods, methodStatic + private static void testMethodStatic(String testArguments) { + t; // testFieldStatic, testArguments + AutocompleteMembers.t; // testFieldStatic + AutocompleteMembers::t; // testMethods, testMethodStatic } }
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOther.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOther.java index 8491f72..7a4b2a6 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOther.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOther.java @@ -3,7 +3,7 @@ package org.javacs.example; import java.util.Arrays; import java.util.concurrent public class AutocompleteOther { public void test() { new AutocompleteMember().; - A; + Auto; AutocompleteMember.; AutocompleteOther.class.; ArrayLis; diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOuter.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOuter.java index c7a4c0d..408b8a7 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOuter.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOuter.java @@ -1,21 +1,21 @@ package org.javacs.example; public class AutocompleteOuter { - public String fields; - public static String fieldStatic; + public String testFields; + public static String testFieldStatic; - public String methods() { } - public static String methodStatic() { } + public String testMethods() { } + public static String testMethodStatic() { } static class StaticInner { { - s + t } } class Inner { { - s + t } } }
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteReference.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteReference.java index 2ba5ca4..979fd6c 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteReference.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteReference.java @@ -11,12 +11,12 @@ public class AutocompleteReference { System.out.println(message.get()); } - private static String fieldStatic; - private String fields; - private static String methodStatic() { + private static String testFieldStatic; + private String testFields; + private static String testMethodStatic() { return "foo"; } - private String methods() { + private String testMethods() { return "foo"; } }
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteScopes.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteScopes.java index 2a2fa58..0e371d0 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteScopes.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteScopes.java @@ -1,188 +1,188 @@ package org.javacs.example; public class AutocompleteScopes { - static void outerStaticMethod() { } - void outerMethods() { } + static void testOuterStaticMethod() { } + void testOuterMethods() { } static class Super { - static void inheritedStaticMethod() { } - void inheritedMethods() { } + static void testInheritedStaticMethod() { } + void testInheritedMethods() { } } static class StaticSub extends Super { - void test(String arguments) { - int localVariables; - s; + void test(String testArguments) { + int testLocalVariables; + t; // Locals - // YES: localVariables, arguments + // YES: testLocalVariables, testArguments // // Static methods in enclosing scopes // YES: testStatic - // YES: outerStaticMethod + // YES: testOuterStaticMethod // // Virtual methods in enclosing scopes // NO: testInner // YES: test - // NO: outerMethods + // NO: testOuterMethods // // Inherited static methods - // YES: inheritedStaticMethod + // YES: testInheritedStaticMethod // // Inherited virtual methods - // YES: inheritedMethods + // YES: testInheritedMethods // // this/super in enclosing scopes // YES: this, super // YES: StaticSub.this, StaticSub.super - StaticSub.s; + StaticSub.; // NO: AutocompleteScopes.this, AutocompleteScopes.super - AutocompleteScopes.s; + AutocompleteScopes.; // NO: Super.this, Super.super - Super.s; + Super.; new Object() { void testInner() { - s; + t; // Locals - // YES: localVariables, arguments + // YES: testLocalVariables, testArguments // // Static methods in enclosing scopes // YES: testStatic - // YES: outerStaticMethod + // YES: testOuterStaticMethod // // Virtual methods in enclosing scopes // YES: testInner // YES: test - // NO: outerMethods + // NO: testOuterMethods // // Inherited static methods - // YES: inheritedStaticMethod + // YES: testInheritedStaticMethod // // Inherited virtual methods - // YES: inheritedMethods + // YES: testInheritedMethods // // this/super in enclosing scopes // YES: this, super // YES: StaticSub.this, StaticSub.super - StaticSub.s; + StaticSub.; // NO: AutocompleteScopes.this, AutocompleteScopes.super - AutocompleteScopes.s; + AutocompleteScopes.; // NO: Super.this, Super.super - Super.s; + Super.; } }; } - static void testStatic(String arguments) { - int localVariables; - s; + static void testStatic(String testArguments) { + int testLocalVariables; + t; // Locals - // YES: localVariables, arguments + // YES: testLocalVariables, testArguments // // Static methods in enclosing scopes // YES: testStatic - // YES: outerStaticMethod + // YES: testOuterStaticMethod // // Virtual methods in enclosing scopes // NO: testInner // NO: test - // NO: outerMethods + // NO: testOuterMethods // // Inherited static methods - // YES: inheritedStaticMethod + // YES: testInheritedStaticMethod // // Inherited virtual methods - // NO: inheritedMethods + // NO: testInheritedMethods // // this/super in enclosing scopes // NO: this, super // NO: StaticSub.this, StaticSub.super - StaticSub.s; + StaticSub.; // NO: AutocompleteScopes.this, AutocompleteScopes.super - AutocompleteScopes.s; + AutocompleteScopes.; // NO: Super.this, Super.super - Super.s; + Super.; new Object() { void testInner() { - s; + t; // Locals - // YES: localVariables, arguments + // YES: testLocalVariables, testArguments // // Static methods in enclosing scopes // YES: testStatic - // YES: outerStaticMethod + // YES: testOuterStaticMethod // // Virtual methods in enclosing scopes // YES: testInner // NO: test - // NO: outerMethods + // NO: testOuterMethods // // Inherited static methods - // YES: inheritedStaticMethod + // YES: testInheritedStaticMethod // // Inherited virtual methods - // NO: inheritedMethods + // NO: testInheritedMethods // // this/super in enclosing scopes // YES: this, super // NO: StaticSub.this, StaticSub.super - StaticSub.s; + StaticSub.; // NO: AutocompleteScopes.this, AutocompleteScopes.super - AutocompleteScopes.s; + AutocompleteScopes.; // NO: Super.this, Super.super - Super.s; + Super.; } }; } } class Sub extends Super { - void test(String arguments) { - int localVariables; - s; + void test(String testArguments) { + int testLocalVariables; + t; // Locals - // YES: localVariables, arguments + // YES: testLocalVariables, testArguments // // Methods in enclosing scopes // NO: testInner // YES: test - // YES: outerMethods, outerStaticMethod + // YES: testOuterMethods, testOuterStaticMethod // // Inherited methods - // YES: inheritedMethods, inheritedStaticMethod + // YES: testInheritedMethods, testInheritedStaticMethod // // this/super in enclosing scopes // YES: this, super // YES: Sub.this, Sub.super - Sub.s; + Sub.; // YES: AutocompleteScopes.this, AutocompleteScopes.super - AutocompleteScopes.s; + AutocompleteScopes.; // NO: Super.this, Super.super - Super.s; + Super.; new Object() { void testInner() { - s; + t; // Locals - // YES: localVariables, arguments + // YES: testLocalVariables, testArguments // // Methods in enclosing scopes // YES: testInner // YES: test - // YES: outerMethods, outerStaticMethod + // YES: testOuterMethods, testOuterStaticMethod // // Inherited methods - // YES: inheritedMethods, inheritedStaticMethod + // YES: testInheritedMethods, testInheritedStaticMethod // // this/super in enclosing scopes // YES: this, super // YES: Sub.this, Sub.super - Sub.s; + Sub.; // YES: AutocompleteScopes.this, AutocompleteScopes.super - AutocompleteScopes.s; + AutocompleteScopes.; // NO: Super.this, Super.super - Super.s; + Super.; } }; } diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticImport.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticImport.java index 4f382e4..e6670a5 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticImport.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticImport.java @@ -8,6 +8,6 @@ class AutocompleteStaticImport { void test() { emptyL; B; - staticFinal; + p; } }
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticMember.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticMember.java index 93191c1..b77f15d 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticMember.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticMember.java @@ -2,15 +2,15 @@ package org.javacs.example; public class AutocompleteStaticMember { public static void test() { - AutocompleteStaticMember. + AutocompleteStaticMember.test } - private static String fieldStatic; - private String field; - private static String methodStatic() { + private static String testFieldStatic; + private String testField; + private static String testMethodStatic() { return "foo"; } - private String method() { + private String testMethod() { return "foo"; } }
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticReference.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticReference.java index 1a9a0fc..2a280c1 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticReference.java +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteStaticReference.java @@ -4,19 +4,19 @@ import java.util.function.Supplier; public class AutocompleteStaticReference { public static void test() { - print(AutocompleteStaticReference::) + print(AutocompleteStaticReference::test) } private void print(Supplier<String> message) { System.out.println(message.get()); } - private static String fieldStatic; - private String field; - private static String methodStatic() { + private static String testFieldStatic; + private String testField; + private static String testMethodStatic() { return "foo"; } - private String methods() { + private String testMethod() { return "foo"; } }
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/Goto.java b/src/test/test-project/workspace/src/org/javacs/example/Goto.java index 2dd774f..3f4cb2f 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/Goto.java +++ b/src/test/test-project/workspace/src/org/javacs/example/Goto.java @@ -1,7 +1,7 @@ package org.javacs.example; public class Goto<Param> { - public static void test() { + public void test() { Object local; Runnable reference; GotoOther other; diff --git a/src/test/test-project/workspace/src/org/javacs/example/HasTest.java b/src/test/test-project/workspace/src/org/javacs/example/HasTest.java new file mode 100644 index 0000000..2bdd656 --- /dev/null +++ b/src/test/test-project/workspace/src/org/javacs/example/HasTest.java @@ -0,0 +1,10 @@ +package org.javacs.example; + +import org.junit.Test; + +public class HasTest { + @Test + public void testMethod() { + + } +}
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/OverloadedMethod.java b/src/test/test-project/workspace/src/org/javacs/example/OverloadedMethod.java new file mode 100644 index 0000000..a57edc4 --- /dev/null +++ b/src/test/test-project/workspace/src/org/javacs/example/OverloadedMethod.java @@ -0,0 +1,12 @@ +package org.javacs.example; + +class OverloadedMethod { + void overloaded() { } + void overloaded(int i) { } + void overloaded(String s) { } + + void testCompletion() { + over; + java.util.List.of; + } +}
\ No newline at end of file diff --git a/src/test/test-project/workspace/src/org/javacs/example/SymbolUnderCursor.java b/src/test/test-project/workspace/src/org/javacs/example/SymbolUnderCursor.java index 8ec75ec..26da058 100644 --- a/src/test/test-project/workspace/src/org/javacs/example/SymbolUnderCursor.java +++ b/src/test/test-project/workspace/src/org/javacs/example/SymbolUnderCursor.java @@ -11,10 +11,15 @@ public class SymbolUnderCursor { method(SymbolUnderCursor.class.getName()); this.method("foo"); - Function<String, String> m = this::method; + java.util.function.Function<String, String> m = this::method; } public SymbolUnderCursor(String constructorParameter) { } + + @Override + public void overrideMethod() { + + } }
\ No newline at end of file |