8366691: JShell should support a more convenient completion

Reviewed-by: asotona
This commit is contained in:
Jan Lahoda 2025-11-12 07:14:45 +00:00
parent 6df78c4585
commit 76a0732ba5
8 changed files with 881 additions and 162 deletions

View File

@ -345,18 +345,21 @@ class ConsoleIOContext extends IOContext {
ConsoleIOContextTestSupport.willComputeCompletion();
int[] anchor = new int[] {-1};
List<Suggestion> suggestions;
List<String> doc;
List<AttributedString> doc;
boolean command = prefix.isEmpty() && text.startsWith("/");
if (command) {
suggestions = repl.commandCompletionSuggestions(text, cursor, anchor);
doc = repl.commandDocumentation(text, cursor, true);
doc = repl.commandDocumentation(text, cursor, true)
.stream()
.map(AttributedString::new)
.toList();
} else {
int prefixLength = prefix.length();
suggestions = repl.analysis.completionSuggestions(prefix + text, cursor + prefixLength, anchor);
anchor[0] -= prefixLength;
doc = repl.analysis.documentation(prefix + text, cursor + prefix.length(), false)
.stream()
.map(Documentation::signature)
.map(this::renderSignature)
.toList();
}
long smartCount = suggestions.stream().filter(Suggestion::matchesType).count();
@ -502,6 +505,41 @@ class ConsoleIOContext extends IOContext {
}
}
private AttributedString renderSignature(Documentation doc) {
int activeParamIndex = doc.activeParameterIndex();
String signature = doc.signature();
if (activeParamIndex == (-1)) {
return new AttributedString(signature);
}
int lparen = signature.indexOf('(');
int rparen = signature.indexOf(')', lparen);
if (lparen == (-1) || rparen == (-1)) {
return new AttributedString(signature);
}
AttributedStringBuilder result = new AttributedStringBuilder();
result.append(signature.substring(0, lparen + 1), AttributedStyle.DEFAULT);
String[] params = signature.substring(lparen + 1, rparen).split(", *");
String sep = "";
for (int i = 0; i < params.length; i++) {
result.append(sep);
result.append(params[i], i == activeParamIndex ? AttributedStyle.BOLD
: AttributedStyle.DEFAULT);
sep = ", ";
}
result.append(signature.substring(rparen), AttributedStyle.DEFAULT);
return result.toAttributedString();
}
private CompletionTask.Result doPrintFullDocumentation(List<CompletionTask> todo, List<String> doc, boolean command) {
if (doc != null && !doc.isEmpty()) {
Terminal term = in.getTerminal();
@ -722,9 +760,9 @@ class ConsoleIOContext extends IOContext {
private final class CommandSynopsisTask implements CompletionTask {
private final List<String> synopsis;
private final List<AttributedString> synopsis;
public CommandSynopsisTask(List<String> synposis) {
public CommandSynopsisTask(List<AttributedString> synposis) {
this.synopsis = synposis;
}
@ -738,6 +776,7 @@ class ConsoleIOContext extends IOContext {
// try {
in.getTerminal().writer().println();
in.getTerminal().writer().println(synopsis.stream()
.map(doc -> doc.toAnsi(in.getTerminal()))
.map(l -> l.replaceAll("\n", LINE_SEPARATOR))
.collect(Collectors.joining(LINE_SEPARATORS2)));
// } catch (IOException ex) {
@ -771,9 +810,9 @@ class ConsoleIOContext extends IOContext {
private final class ExpressionSignaturesTask implements CompletionTask {
private final List<String> doc;
private final List<AttributedString> doc;
public ExpressionSignaturesTask(List<String> doc) {
public ExpressionSignaturesTask(List<AttributedString> doc) {
this.doc = doc;
}
@ -786,7 +825,9 @@ class ConsoleIOContext extends IOContext {
public Result perform(String text, int cursor) throws IOException {
in.getTerminal().writer().println();
in.getTerminal().writer().println(repl.getResourceString("jshell.console.completion.current.signatures"));
in.getTerminal().writer().println(doc.stream().collect(Collectors.joining(LINE_SEPARATOR)));
in.getTerminal().writer().println(doc.stream()
.map(doc -> doc.toAnsi(in.getTerminal()))
.collect(Collectors.joining(LINE_SEPARATOR)));
return Result.FINISH;
}

View File

@ -118,7 +118,7 @@ public abstract class JavadocHelper implements AutoCloseable {
StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null);
try {
fm.setLocationFromPaths(StandardLocation.SOURCE_PATH, sourceLocations);
return new OnDemandJavadocHelper(mainTask, fm);
return new OnDemandJavadocHelper(mainTask, fm, sourceLocations);
} catch (IOException ex) {
try {
fm.close();
@ -135,6 +135,21 @@ public abstract class JavadocHelper implements AutoCloseable {
}
@Override
public void close() throws IOException {}
@Override
public String getResolvedDocComment(StoredElement forElement) throws IOException {
return null;
}
@Override
public StoredElement getHandle(Element forElement) {
return null;
}
@Override
public Collection<? extends Path> getSourceLocations() {
return List.of();
}
};
}
}
@ -147,6 +162,7 @@ public abstract class JavadocHelper implements AutoCloseable {
* @throws IOException if something goes wrong in the search
*/
public abstract String getResolvedDocComment(Element forElement) throws IOException;
public abstract String getResolvedDocComment(StoredElement forElement) throws IOException;
/**Returns an element representing the same given program element, but the returned element will
* be resolved from source, if it can be found. Returns the original element if the source for
@ -158,6 +174,9 @@ public abstract class JavadocHelper implements AutoCloseable {
*/
public abstract Element getSourceElement(Element forElement) throws IOException;
public abstract StoredElement getHandle(Element forElement);
public abstract Collection<? extends Path> getSourceLocations();
/**Closes the helper.
*
* @throws IOException if something foes wrong during the close
@ -165,16 +184,20 @@ public abstract class JavadocHelper implements AutoCloseable {
@Override
public abstract void close() throws IOException;
public record StoredElement(String module, String binaryName, String handle) {}
private static final class OnDemandJavadocHelper extends JavadocHelper {
private final JavacTask mainTask;
private final JavaFileManager baseFileManager;
private final StandardJavaFileManager fm;
private final Map<String, Pair<JavacTask, TreePath>> signature2Source = new HashMap<>();
private final Collection<? extends Path> sourceLocations;
private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm) {
private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm, Collection<? extends Path> sourceLocations) {
this.mainTask = mainTask;
this.baseFileManager = ((JavacTaskImpl) mainTask).getContext().get(JavaFileManager.class);
this.fm = fm;
this.sourceLocations = sourceLocations;
}
@Override
@ -187,6 +210,16 @@ public abstract class JavadocHelper implements AutoCloseable {
return getResolvedDocComment(sourceElement.fst, sourceElement.snd);
}
@Override
public String getResolvedDocComment(StoredElement forElement) throws IOException {
Pair<JavacTask, TreePath> sourceElement = getSourceElement(forElement);
if (sourceElement == null)
return null;
return getResolvedDocComment(sourceElement.fst, sourceElement.snd);
}
@Override
public Element getSourceElement(Element forElement) throws IOException {
Pair<JavacTask, TreePath> sourceElement = getSourceElement(mainTask, forElement);
@ -202,7 +235,30 @@ public abstract class JavadocHelper implements AutoCloseable {
return result;
}
private String getResolvedDocComment(JavacTask task, TreePath el) throws IOException {
@Override
public StoredElement getHandle(Element forElement) {
TypeElement type = topLevelType(forElement);
if (type == null)
return null;
Elements elements = mainTask.getElements();
ModuleElement module = elements.getModuleOf(type);
String moduleName = module == null || module.isUnnamed()
? null
: module.getQualifiedName().toString();
String binaryName = elements.getBinaryName(type).toString();
String handle = elementSignature(forElement);
return new StoredElement(moduleName, binaryName, handle);
}
@Override
public Collection<? extends Path> getSourceLocations() {
return sourceLocations;
}
private String getResolvedDocComment(JavacTask task, TreePath el) throws IOException {
DocTrees trees = DocTrees.instance(task);
Element element = trees.getElement(el);
String docComment = trees.getDocComment(el);
@ -634,7 +690,7 @@ public abstract class JavadocHelper implements AutoCloseable {
.filter(supMethod -> task.getElements().overrides(method, supMethod, type));
}
/* Find types from which methods in type may inherit javadoc, in the proper order.*/
/* Find types from which methods in binaryName may inherit javadoc, in the proper order.*/
private Stream<Element> superTypeForInheritDoc(JavacTask task, Element type) {
TypeElement clazz = (TypeElement) type;
Stream<Element> result = interfaces(clazz);
@ -701,6 +757,35 @@ public abstract class JavadocHelper implements AutoCloseable {
return exc != null ? exc.toString() : null;
}
private Pair<JavacTask, TreePath> getSourceElement(StoredElement el) throws IOException {
if (el == null) {
return null;
}
String handle = el.handle();
Pair<JavacTask, TreePath> cached = signature2Source.get(handle);
if (cached != null) {
return cached.fst != null ? cached : null;
}
Pair<JavacTask, CompilationUnitTree> source = findSource(el.module(), el.binaryName());
if (source == null)
return null;
fillElementCache(source.fst, source.snd);
cached = signature2Source.get(handle);
if (cached != null) {
return cached;
} else {
signature2Source.put(handle, Pair.of(null, null));
return null;
}
}
private Pair<JavacTask, TreePath> getSourceElement(JavacTask origin, Element el) throws IOException {
String handle = elementSignature(el);
Pair<JavacTask, TreePath> cached = signature2Source.get(handle);

View File

@ -28,6 +28,12 @@ package jdk.jshell;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
/**
* Provides analysis utilities for source code input.
@ -64,6 +70,18 @@ public abstract class SourceCodeAnalysis {
*/
public abstract List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);
/**
* Compute possible follow-ups for the given input.
* Uses information from the current {@code JShell} state, including
* type information, to filter the suggestions.
* @param input the user input, so far
* @param cursor the current position of the cursors in the given {@code input} text
* @param convertor convert the given {@linkplain ElementSuggestion} to a custom completion suggestions.
* @return list of candidate continuations of the given input.
* @since 26
*/
public abstract <S> List<S> completionSuggestions(String input, int cursor, ElementSuggestionConvertor<S> convertor);
/**
* Compute documentation for the given user's input. Multiple {@code Documentation} objects may
* be returned when multiple elements match the user's input (like for overloaded methods).
@ -315,6 +333,135 @@ public abstract class SourceCodeAnalysis {
boolean matchesType();
}
/**
* A description of an {@linkplain Element} that is a possible continuation of
* a given snippet.
*
* @apiNote Instances of this interface and instances of the returned {@linkplain Elements}
* should only be used and held during the execution of the
* {@link #completionSuggestions(java.lang.String, int, jdk.jshell.SourceCodeAnalysis.ElementSuggestionConvertor) }
* method. Their use outside of the context of the method is not supported and
* the effect is undefined.
*
* @since 26
*/
public sealed interface ElementSuggestion permits SourceCodeAnalysisImpl.ElementSuggestionImpl {
/**
* {@return a possible continuation {@linkplain Element}, or {@code null}
* if this item does not represent an {@linkplain Element}.}
*/
Element element();
/**
* {@return a possible continuation keyword, or {@code null}
* if this item does not represent a keyword.}
*/
String keyword();
/**
* {@return {@code true} if this {@linkplain Element}'s type fits into
* the context.}
*
* Typically used when the type of the element fits the expected type.
*/
boolean matchesType();
/**
* {@return the offset in the original snippet at which point this {@linkplain Element}
* should be inserted.}
*/
int anchor();
/**
* {@return a {@linkplain Supplier} for the javadoc documentation for this Element.}
*
* @apiNote The instance returned from this method is safe to hold for extended
* periods of time, and can be called outside of the context of the
* {@link #completionSuggestions(java.lang.String, int, jdk.jshell.SourceCodeAnalysis.ElementSuggestionConvertor) } method.
*/
Supplier<String> documentation();
}
/**
* Permit access to completion state.
*
* @since 26
*/
public sealed interface CompletionState permits SourceCodeAnalysisImpl.CompletionStateImpl {
/**
* {@return true if the given element is available using the simple name at
* the place of the cursor.}
*
* @param el {@linkplain Element} to check
*/
public boolean availableUsingSimpleName(Element el);
/**
* {@return flags describing the overall completion context.}
*/
public Set<CompletionContext> completionContext();
/**
* {@return if the context is a qualified expression
* (i.e. {@link CompletionContext#QUALIFIED} is set),
* the type of the selector expression; {@code null} otherwise.}
*/
public TypeMirror selectorType();
/**
* {@return an implementation of some utility methods for
* operating on elements}
*/
Elements elementUtils();
/**
* {@return an implementation of some utility methods for
* operating on types}
*/
Types typeUtils();
}
/**
* Various flags describing the context in which the completion happens.
*
* @since 26
*/
public enum CompletionContext {
/**
* The context is inside annotation attributes.
*/
ANNOTATION_ATTRIBUTE,
/**
* Parentheses should not be filled for methods and constructor
* in the current context.
*
* Typically used in the import or method reference contexts.
*/
NO_PAREN,
/**
* Interpret {@link ElementKind#ANNOTATION_TYPE}s as annotation uses. Typically means
* they should be prefixed with {@code @}.
*/
TYPES_AS_ANNOTATIONS,
/**
* The context is in a qualified expression (like member access). Simple
* names only should be used.
*/
QUALIFIED,
;
}
/**
* A convertor from a list of {@linkplain ElementSuggestion} to a list
* of custom target completion items.
*
* @param <S> a custom target completion type.
* @since 26
*/
public interface ElementSuggestionConvertor<S> {
/**
* Convert a list of {@linkplain ElementSuggestion} to a list
* of custom completion items.
*
* @param state the state of the completion
* @param suggestions the input suggestions
* @return the converted suggestions
*/
public List<S> convert(CompletionState state, List<? extends ElementSuggestion> suggestions);
}
/**
* A documentation for a candidate for continuation of the given user's input.
*/
@ -333,6 +480,18 @@ public abstract class SourceCodeAnalysis {
* @return the javadoc, or null if not found or not requested
*/
String javadoc();
/**
* If this {@code Documentation} is created for a method invocation,
* return the current parameter index.
*
* @implNote the default implementation returns {@code -1}
* @return the active parameter index, or {@code -1} if not available
* @since 26
*/
default int activeParameterIndex() {
return -1;
}
}
/**

View File

@ -114,11 +114,13 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -152,8 +154,13 @@ import static jdk.jshell.SourceCodeAnalysis.Completeness.DEFINITELY_INCOMPLETE;
import static jdk.jshell.TreeDissector.printType;
import static java.util.stream.Collectors.joining;
import static javax.lang.model.element.ElementKind.CONSTRUCTOR;
import static javax.lang.model.element.ElementKind.MODULE;
import static javax.lang.model.element.ElementKind.PACKAGE;
import javax.lang.model.type.IntersectionType;
import javax.lang.model.util.Elements;
import jdk.internal.shellsupport.doc.JavadocHelper.StoredElement;
/**
* The concrete implementation of SourceCodeAnalysis.
@ -278,18 +285,76 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
@Override
public List<Suggestion> completionSuggestions(String code, int cursor, int[] anchor) {
suspendIndexing();
ElementSuggestionConvertor<Suggestion> convertor = (state, suggestions) -> {
Set<String> haveParams = suggestions.stream()
.map(s -> s.element())
.filter(el -> el != null)
.filter(IS_CONSTRUCTOR.or(IS_METHOD))
.filter(c -> !((ExecutableElement)c).getParameters().isEmpty())
.map(this::simpleContinuationName)
.collect(toSet());
List<Suggestion> result = new ArrayList<>();
for (ElementSuggestion s : suggestions) {
Element el = s.element();
if (el != null) {
String continuation = continuationName(state, el);
switch (el.getKind()) {
case CONSTRUCTOR, METHOD -> {
if (state.completionContext().contains(CompletionContext.ANNOTATION_ATTRIBUTE)) {
continuation += " = ";
} else if (!state.completionContext().contains(CompletionContext.NO_PAREN)) {
// add trailing open or matched parenthesis, as approriate:
continuation += haveParams.contains(continuation) ? "(" : "()";
}
}
case ANNOTATION_TYPE -> {
if (state.completionContext().contains(CompletionContext.TYPES_AS_ANNOTATIONS)) {
boolean hasAnyAttributes =
ElementFilter.methodsIn(el.getEnclosedElements())
.stream()
.anyMatch(attribute -> attribute.getParameters().isEmpty());
String paren = hasAnyAttributes ? "(" : "";
continuation = "@" + continuation + paren;
}
}
case PACKAGE ->
// add trailing dot to package names
continuation += ".";
}
result.add(new SuggestionImpl(continuation, s.matchesType()));
} else if (s.keyword() != null) {
result.add(new SuggestionImpl(s.keyword(), s.matchesType()));
}
anchor[0] = s.anchor();
}
Collections.sort(result, Comparator.comparing(Suggestion::continuation));
return result;
};
try {
return completionSuggestionsImpl(code, cursor, anchor);
return completionSuggestions(code, cursor, convertor);
} catch (Throwable exc) {
proc.debug(exc, "Exception thrown in SourceCodeAnalysisImpl.completionSuggestions");
return Collections.emptyList();
}
}
@Override
public <Suggestion> List<Suggestion> completionSuggestions(String code, int cursor, ElementSuggestionConvertor<Suggestion> convertor) {
suspendIndexing();
try {
return completionSuggestionsImpl(code, cursor, convertor);
} finally {
resumeIndexing();
}
}
private List<Suggestion> completionSuggestionsImpl(String code, int cursor, int[] anchor) {
private <Suggestion> List<Suggestion> completionSuggestionsImpl(String code, int cursor, ElementSuggestionConvertor<Suggestion> suggestionConvertor) {
code = code.substring(0, cursor);
Matcher m = JAVA_IDENTIFIER.matcher(code);
String identifier = "";
@ -302,31 +367,27 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
}
OuterWrap codeWrap = wrapCodeForCompletion(code, cursor, true);
String[] requiredPrefix = new String[] {identifier};
return computeSuggestions(codeWrap, code, cursor, requiredPrefix, anchor).stream()
.filter(s -> filteringText(s).startsWith(requiredPrefix[0]) && !s.continuation().equals(REPL_DOESNOTMATTER_CLASS_NAME))
.sorted(Comparator.comparing(Suggestion::continuation))
.toList();
return computeSuggestions(codeWrap, code, cursor, identifier, suggestionConvertor);
}
private static String filteringText(Suggestion suggestion) {
return suggestion instanceof SuggestionImpl impl
? impl.filteringText
: suggestion.continuation();
}
private static List<String> COMPLETION_EXTRA_PARAMETERS = List.of("-parameters");
private List<Suggestion> computeSuggestions(OuterWrap code, String inputCode, int cursor, String[] requiredPrefix, int[] anchor) {
return proc.taskFactory.analyze(code, at -> {
private <Suggestion> List<Suggestion> computeSuggestions(OuterWrap code, String inputCode, int cursor, String prefix, ElementSuggestionConvertor<Suggestion> suggestionConvertor) {
return proc.taskFactory.analyze(code, COMPLETION_EXTRA_PARAMETERS, at -> {
try (JavadocHelper javadoc = JavadocHelper.create(at.task, findSources())) {
SourcePositions sp = at.trees().getSourcePositions();
CompilationUnitTree topLevel = at.firstCuTree();
List<Suggestion> result = new ArrayList<>();
TreePath tp = pathFor(topLevel, sp, code, cursor);
if (tp != null) {
List<ElementSuggestion> result = new ArrayList<>();
Scope scope = at.trees().getScope(tp);
Collection<? extends Element> scopeContent = scopeContent(at, scope, IDENTITY);
Set<CompletionContext> completionContext = EnumSet.noneOf(CompletionContext.class);
Predicate<Element> accessibility = createAccessibilityFilter(at, tp);
Predicate<Element> smartTypeFilter;
Predicate<Element> smartFilter;
Iterable<TypeMirror> targetTypes = findTargetType(at, tp);
TypeMirror selectorType = null;
if (targetTypes != null) {
if (tp.getLeaf().getKind() == Kind.MEMBER_REFERENCE) {
Types types = at.getTypes();
@ -386,24 +447,24 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
}
switch (tp.getLeaf().getKind()) {
case MEMBER_REFERENCE, MEMBER_SELECT: {
completionContext.add(CompletionContext.QUALIFIED);
javax.lang.model.element.Name identifier;
ExpressionTree expression;
Function<Boolean, String> paren;
if (tp.getLeaf().getKind() == Kind.MEMBER_SELECT) {
MemberSelectTree mst = (MemberSelectTree)tp.getLeaf();
identifier = mst.getIdentifier();
expression = mst.getExpression();
paren = DEFAULT_PAREN;
} else {
MemberReferenceTree mst = (MemberReferenceTree)tp.getLeaf();
identifier = mst.getName();
expression = mst.getQualifierExpression();
paren = NO_PAREN;
completionContext.add(CompletionContext.NO_PAREN);
}
if (identifier.contentEquals("*"))
break;
TreePath exprPath = new TreePath(tp, expression);
TypeMirror site = at.trees().getTypeMirror(exprPath);
selectorType = at.trees().getTypeMirror(exprPath);
boolean staticOnly = isStaticContext(at, exprPath);
ImportTree it = findImport(tp);
@ -413,17 +474,14 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
? ((MemberSelectTree) it.getQualifiedIdentifier()).getExpression().toString() + "."
: "";
addModuleElements(at, qualifiedPrefix, result);
addModuleElements(at, javadoc, selectStart, qualifiedPrefix + prefix, result);
requiredPrefix[0] = qualifiedPrefix + requiredPrefix[0];
anchor[0] = selectStart;
return result;
break;
}
boolean isImport = it != null;
List<? extends Element> members = membersOf(at, site, staticOnly && !isImport && tp.getLeaf().getKind() == Kind.MEMBER_SELECT);
List<? extends Element> members = membersOf(at, selectorType, staticOnly && !isImport && tp.getLeaf().getKind() == Kind.MEMBER_SELECT);
Predicate<Element> filter = accessibility;
if (isNewClass(tp)) { // new xxx.|
@ -434,7 +492,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
}
return true;
});
addElements(membersOf(at, members), constructorFilter, smartFilter, result);
addElements(javadoc, membersOf(at, members), constructorFilter, smartFilter, cursor, prefix, result);
filter = filter.and(IS_PACKAGE);
} else if (isThrowsClause(tp)) {
@ -442,7 +500,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
filter = filter.and(IS_PACKAGE.or(IS_CLASS).or(IS_INTERFACE));
smartFilter = IS_PACKAGE.negate().and(smartTypeFilter);
} else if (isImport) {
paren = NO_PAREN;
completionContext.add(CompletionContext.NO_PAREN);
if (!it.isStatic()) {
filter = filter.and(IS_PACKAGE.or(IS_CLASS).or(IS_INTERFACE));
}
@ -452,7 +510,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
filter = filter.and(staticOnly ? STATIC_ONLY : INSTANCE_ONLY);
addElements(members, filter, smartFilter, paren, result);
addElements(javadoc, members, filter, smartFilter, cursor, prefix, result);
break;
}
case IDENTIFIER:
@ -466,35 +524,43 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
if (enclosingExpression != null) { // expr.new IDENT|
TypeMirror site = at.trees().getTypeMirror(new TreePath(tp, enclosingExpression));
filter = filter.and(el -> el.getEnclosingElement().getKind() == ElementKind.CLASS && !el.getEnclosingElement().getModifiers().contains(Modifier.STATIC));
addElements(membersOf(at, membersOf(at, site, false)), filter, smartFilter, result);
addElements(javadoc, membersOf(at, membersOf(at, site, false)), filter, smartFilter, cursor, prefix, result);
} else {
addScopeElements(at, scope, listEnclosed, filter, smartFilter, result);
addScopeElements(at, javadoc, scope, listEnclosed, filter, smartFilter, cursor, prefix, result);
}
break;
}
if (isThrowsClause(tp)) {
Predicate<Element> accept = accessibility.and(STATIC_ONLY)
.and(IS_PACKAGE.or(IS_CLASS).or(IS_INTERFACE));
addScopeElements(at, scope, IDENTITY, accept, IS_PACKAGE.negate().and(smartTypeFilter), result);
addElements(javadoc, scopeContent, accept, IS_PACKAGE.negate().and(smartTypeFilter), cursor, prefix, result);
break;
}
if (isAnnotation(tp)) {
completionContext.add(CompletionContext.TYPES_AS_ANNOTATIONS);
if (getAnnotationAttributeNameOrNull(tp.getParentPath(), true) != null) {
//nested annotation
result = completionSuggestionsImpl(inputCode, cursor - 1, anchor);
requiredPrefix[0] = "@" + requiredPrefix[0];
return result;
return completionSuggestionsImpl(inputCode, cursor - 1, (state, items) -> {
CompletionState newState = new CompletionStateImpl(((CompletionStateImpl) state).scopeContent, completionContext, state.selectorType(), state.elementUtils(), state.typeUtils());
return suggestionConvertor.convert(newState,
items.stream()
.filter(s -> s.element().getKind() == ElementKind.ANNOTATION_TYPE)
.filter(s -> s.element().getSimpleName().toString().startsWith(prefix))
.map(s -> new ElementSuggestionImpl(s.element(), s.keyword(), s.matchesType(), s.anchor(), s.documentation()))
.<ElementSuggestion>toList());
});
}
Predicate<Element> accept = accessibility.and(STATIC_ONLY)
.and(IS_PACKAGE.or(IS_CLASS).or(IS_INTERFACE));
addScopeElements(at, scope, IDENTITY, accept, IS_PACKAGE.negate().and(smartTypeFilter), result);
addElements(javadoc, scopeContent, accept, IS_PACKAGE.negate().and(smartTypeFilter), cursor - 1, prefix, result);
break;
}
ImportTree it = findImport(tp);
if (it != null) {
if (it.isModule()) {
addModuleElements(at, "", result);
addModuleElements(at, javadoc, cursor, prefix, result);
} else {
// the context of the identifier is an import, look for
// package names that start with the identifier.
@ -503,20 +569,20 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
// JShell to change to use the default package, and that
// change is done, then this should use some variation
// of membersOf(at, at.getElements().getPackageElement("").asType(), false)
addElements(listPackages(at, ""),
addElements(javadoc, listPackages(at, ""),
it.isStatic()
? STATIC_ONLY.and(accessibility)
: accessibility,
smartFilter, result);
smartFilter, cursor, prefix, result);
result.add(new SuggestionImpl("module ", false));
result.add(new ElementSuggestionImpl(null, "module ", false, cursor, () -> null)); //TODO: better javadoc?
}
}
break;
case CLASS: {
Predicate<Element> accept = accessibility.and(IS_TYPE);
addScopeElements(at, scope, IDENTITY, accept, smartFilter, result);
addElements(primitivesOrVoid(at), TRUE, smartFilter, result);
addElements(javadoc, scopeContent, accept, smartFilter, cursor, prefix, result);
addElements(javadoc, primitivesOrVoid(at), TRUE, smartFilter, cursor, prefix, result);
break;
}
case BLOCK:
@ -553,6 +619,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
accept = accept.and(IS_TYPE);
}
} else if (tp.getParentPath().getLeaf().getKind() == Kind.ANNOTATION) {
completionContext.add(CompletionContext.ANNOTATION_ATTRIBUTE);
AnnotationTree annotation = (AnnotationTree) tp.getParentPath().getLeaf();
Element annotationType = at.trees().getElement(tp.getParentPath());
Set<String> present = annotation.getArguments()
@ -563,15 +631,18 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
.filter(var -> var.getKind() == Kind.IDENTIFIER)
.map(var -> ((IdentifierTree) var).getName().toString())
.collect(Collectors.toSet());
addElements(ElementFilter.methodsIn(annotationType.getEnclosedElements()), el -> !present.contains(el.getSimpleName().toString()), TRUE, _ -> " = ", result);
addElements(javadoc, ElementFilter.methodsIn(annotationType.getEnclosedElements()), el -> !present.contains(el.getSimpleName().toString()), TRUE, cursor, prefix, /*_ -> " = ", */result);
break;
} else if (getAnnotationAttributeNameOrNull(tp, true) instanceof String attributeName) {
completionContext.add(CompletionContext.ANNOTATION_ATTRIBUTE);
Element annotationType = tp.getParentPath().getParentPath().getLeaf().getKind() == Kind.ANNOTATION
? at.trees().getElement(tp.getParentPath().getParentPath())
: at.trees().getElement(tp.getParentPath().getParentPath().getParentPath());
if (sp.getEndPosition(topLevel, tp.getParentPath().getLeaf()) == (-1)) {
//synthetic 'value':
addElements(ElementFilter.methodsIn(annotationType.getEnclosedElements()), TRUE, TRUE, _ -> " = ", result);
//TODO: filter out existing:
addElements(javadoc, ElementFilter.methodsIn(annotationType.getEnclosedElements()), TRUE, TRUE, cursor, prefix, result);
boolean hasValue = findAnnotationAttributeIfAny(annotationType, "value").isPresent();
if (!hasValue) {
break;
@ -588,29 +659,18 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
if (relevantAttributeType.getKind() == TypeKind.DECLARED &&
at.getTypes().asElement(relevantAttributeType) instanceof Element attributeTypeEl) {
if (attributeTypeEl.getKind() == ElementKind.ANNOTATION_TYPE) {
boolean hasAnyAttributes =
ElementFilter.methodsIn(attributeTypeEl.getEnclosedElements())
.stream()
.anyMatch(attribute -> attribute.getParameters().isEmpty());
String paren = hasAnyAttributes ? "(" : "";
String name = scopeContent(at, scope, IDENTITY).contains(attributeTypeEl)
? attributeTypeEl.getSimpleName().toString() //simple name ought to be enough:
: ((TypeElement) attributeTypeEl).getQualifiedName().toString();
result.add(new SuggestionImpl("@" + name + paren, true));
completionContext.add(CompletionContext.TYPES_AS_ANNOTATIONS);
addElements(javadoc, List.of(attributeTypeEl), TRUE, TRUE, cursor, prefix, result);
break;
} else if (attributeTypeEl.getKind() == ElementKind.ENUM) {
String typeName = scopeContent(at, scope, IDENTITY).contains(attributeTypeEl)
? attributeTypeEl.getSimpleName().toString() //simple name ought to be enough:
: ((TypeElement) attributeTypeEl).getQualifiedName().toString();
result.add(new SuggestionImpl(typeName, true));
result.addAll(ElementFilter.fieldsIn(attributeTypeEl.getEnclosedElements())
.stream()
.filter(e -> e.getKind() == ElementKind.ENUM_CONSTANT)
.map(c -> new SuggestionImpl(scopeContent(at, scope, IDENTITY).contains(c)
? c.getSimpleName().toString()
: typeName + "." + c.getSimpleName(), c.getSimpleName().toString(),
true))
.toList());
List<Element> elements = new ArrayList<>();
elements.add(attributeTypeEl);
ElementFilter.fieldsIn(attributeTypeEl.getEnclosedElements())
.stream()
.filter(e -> e.getKind() == ElementKind.ENUM_CONSTANT)
.forEach(elements::add);
addElements(javadoc, elements, TRUE, TRUE, cursor, prefix, result);
break;
}
}
@ -625,7 +685,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
insertPrimitiveTypes = false;
}
addScopeElements(at, scope, IDENTITY, accept, smartFilter, result);
addElements(javadoc, scopeContent, accept, smartFilter, cursor, prefix, result);
if (insertPrimitiveTypes) {
Tree parent = tp.getParentPath().getLeaf();
@ -637,22 +697,32 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
case TYPE_PARAMETER, CLASS, INTERFACE, ENUM, RECORD -> FALSE;
default -> TRUE;
};
addElements(primitivesOrVoid(at), accept, smartFilter, result);
addElements(javadoc, primitivesOrVoid(at), accept, smartFilter, cursor, prefix, result);
}
boolean hasBooleanSmartType = targetTypes != null &&
StreamSupport.stream(targetTypes.spliterator(), false)
.anyMatch(tm -> tm.getKind() == TypeKind.BOOLEAN);
if (hasBooleanSmartType) {
result.add(new SuggestionImpl("true", true));
result.add(new SuggestionImpl("false", true));
for (String booleanKeyword : new String[] {"false", "true"}) {
if (booleanKeyword.startsWith(prefix)) {
result.add(new ElementSuggestionImpl(null, booleanKeyword, true, cursor, () -> null));
}
}
}
break;
}
}
CompletionState completionState = new CompletionStateImpl(scopeContent, completionContext, selectorType, at.getElements(), at.getTypes());
return suggestionConvertor.convert(completionState, result);
}
anchor[0] = cursor;
return result;
} catch (IOException ex) {
//TODO:
ex.printStackTrace();
}
return Collections.emptyList();
});
}
@ -1162,59 +1232,74 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
IS_PACKAGE.test(encl);
};
private final Function<Element, Iterable<? extends Element>> IDENTITY = Collections::singletonList;
private final Function<Boolean, String> DEFAULT_PAREN = hasParams -> hasParams ? "(" : "()";
private final Function<Boolean, String> NO_PAREN = hasParams -> "";
private void addElements(Iterable<? extends Element> elements, Predicate<Element> accept, Predicate<Element> smart, List<Suggestion> result) {
addElements(elements, accept, smart, DEFAULT_PAREN, result);
}
private void addElements(Iterable<? extends Element> elements, Predicate<Element> accept, Predicate<Element> smart, Function<Boolean, String> paren, List<Suggestion> result) {
Set<String> hasParams = Util.stream(elements)
.filter(accept)
.filter(IS_CONSTRUCTOR.or(IS_METHOD))
.filter(c -> !((ExecutableElement)c).getParameters().isEmpty())
.map(this::simpleName)
.collect(toSet());
private void addElements(JavadocHelper javadoc, Iterable<? extends Element> elements, Predicate<Element> accept, Predicate<Element> smart, int anchor, String prefix, List<ElementSuggestion> result) {
for (Element c : elements) {
if (!accept.test(c))
if (!accept.test(c) || !simpleContinuationName(c).startsWith(prefix))
continue;
if (c.getKind() == ElementKind.METHOD &&
c.getSimpleName().contentEquals(Util.DOIT_METHOD_NAME) &&
((ExecutableElement) c).getParameters().isEmpty()) {
continue;
}
String simpleName = simpleName(c);
switch (c.getKind()) {
case CONSTRUCTOR:
case METHOD:
// add trailing open or matched parenthesis, as approriate
simpleName += paren.apply(hasParams.contains(simpleName));
break;
case PACKAGE:
// add trailing dot to package names
simpleName += ".";
break;
}
result.add(new SuggestionImpl(simpleName, smart.test(c)));
StoredElement stored = javadoc.getHandle(c);
Collection<? extends Path> sourceLocations = javadoc.getSourceLocations();
result.add(new ElementSuggestionImpl(c, null, smart.test(c), anchor, () -> {
return proc.taskFactory.analyze(proc.outerMap.wrapInTrialClass(Wrap.methodWrap(";")), task -> {
try (JavadocHelper nestedJavadoc = JavadocHelper.create(task.task, sourceLocations)) {
return nestedJavadoc.getResolvedDocComment(stored);
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
});
}));
}
}
private void addModuleElements(AnalyzeTask at,
private void addModuleElements(AnalyzeTask at, JavadocHelper javadoc, int anchor,
String prefix,
List<Suggestion> result) {
List<ElementSuggestion> result) {
for (ModuleElement me : at.getElements().getAllModuleElements()) {
if (!me.getQualifiedName().toString().startsWith(prefix)) {
continue;
}
result.add(new SuggestionImpl(me.getQualifiedName().toString(),
false));
result.add(new ElementSuggestionImpl(me, null, false, anchor, () -> null)); //TODO: better javadoc!
}
}
private String simpleName(Element el) {
return el.getKind() == ElementKind.CONSTRUCTOR ? el.getEnclosingElement().getSimpleName().toString()
: el.getSimpleName().toString();
private String simpleContinuationName(Element el) {
return switch (el.getKind()) {
case CONSTRUCTOR -> el.getEnclosingElement().getSimpleName().toString();
case MODULE -> ((ModuleElement) el).getQualifiedName().toString();
default -> el.getSimpleName().toString();
};
}
private String continuationName(CompletionState state, Element el) {
if (state.completionContext().contains(CompletionContext.QUALIFIED)) {
return simpleContinuationName(el);
} else if (state.availableUsingSimpleName(el)) {
return el.getSimpleName().toString();
} else {
return (switch (el.getKind()) {
case PACKAGE -> ((PackageElement) el).getQualifiedName();
case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD ->
primitiveLikeClass(el)
? el.getSimpleName()
: continuationName(state, el.getEnclosingElement()) + "." + el.getSimpleName();
case ENUM_CONSTANT, FIELD, METHOD ->
el.getModifiers().contains(Modifier.STATIC)
? continuationName(state, el.getEnclosingElement()) + "." + el.getSimpleName()
: el.getSimpleName();
default -> simpleContinuationName(el);
}).toString();
}
}
private boolean primitiveLikeClass(Element el) {
return el.asType().getKind().isPrimitive() ||
el.asType().getKind() == TypeKind.VOID;
}
private List<? extends Element> membersOf(AnalyzeTask at, TypeMirror site, boolean shouldGenerateDotClassItem) {
@ -1625,8 +1710,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
};
}
private void addScopeElements(AnalyzeTask at, Scope scope, Function<Element, Iterable<? extends Element>> elementConvertor, Predicate<Element> filter, Predicate<Element> smartFilter, List<Suggestion> result) {
addElements(scopeContent(at, scope, elementConvertor), filter, smartFilter, result);
private void addScopeElements(AnalyzeTask at, JavadocHelper javadoc, Scope scope, Function<Element, Iterable<? extends Element>> elementConvertor, Predicate<Element> filter, Predicate<Element> smartFilter, int anchor, String prefix, List<ElementSuggestion> result) {
addElements(javadoc, scopeContent(at, scope, elementConvertor), filter, smartFilter, anchor, prefix, result);
}
private Iterable<Pair<ExecutableElement, ExecutableType>> methodCandidates(AnalyzeTask at, TreePath invocation) {
@ -1759,6 +1844,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
Stream<Element> elements;
Iterable<Pair<ExecutableElement, ExecutableType>> candidates;
List<? extends ExpressionTree> arguments;
int parameterIndex = -1;
if (tp.getLeaf().getKind() == Kind.METHOD_INVOCATION || tp.getLeaf().getKind() == Kind.NEW_CLASS) {
if (tp.getLeaf().getKind() == Kind.METHOD_INVOCATION) {
@ -1783,6 +1869,10 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
}
elements = Util.stream(candidates).map(method -> method.fst);
if (prevPath != null) {
parameterIndex = arguments.indexOf(prevPath.getLeaf());
}
} else if (tp.getLeaf().getKind() == Kind.IDENTIFIER || tp.getLeaf().getKind() == Kind.MEMBER_SELECT) {
Element el = at.trees().getElement(tp);
@ -1820,7 +1910,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
List<Documentation> result = Collections.emptyList();
try (JavadocHelper helper = JavadocHelper.create(at.task, findSources())) {
result = elements.map(el -> constructDocumentation(at, helper, el, computeJavadoc))
int parameterIndexFin = parameterIndex;
result = elements.map(el -> constructDocumentation(at, helper, el, parameterIndexFin, computeJavadoc))
.filter(Objects::nonNull)
.toList();
} catch (IOException ex) {
@ -1831,7 +1922,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
});
}
private Documentation constructDocumentation(AnalyzeTask at, JavadocHelper helper, Element el, boolean computeJavadoc) {
private Documentation constructDocumentation(AnalyzeTask at, JavadocHelper helper, Element el, int parameterIndex, boolean computeJavadoc) {
String javadoc = null;
try {
if (hasSyntheticParameterNames(el)) {
@ -1844,7 +1935,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
proc.debug(ex, "SourceCodeAnalysisImpl.element2String(..., " + el + ")");
}
String signature = Util.expunge(elementHeader(at, el, !hasSyntheticParameterNames(el), true));
return new DocumentationImpl(signature, javadoc);
return new DocumentationImpl(signature, javadoc, parameterIndex);
}
public void close() {
@ -1857,27 +1948,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
}
}
private static final class DocumentationImpl implements Documentation {
private final String signature;
private final String javadoc;
public DocumentationImpl(String signature, String javadoc) {
this.signature = signature;
this.javadoc = javadoc;
}
@Override
public String signature() {
return signature;
}
@Override
public String javadoc() {
return javadoc;
}
}
private record DocumentationImpl(String signature, String javadoc, int activeParameterIndex) implements Documentation {}
private boolean isEmptyArgumentsContext(List<? extends ExpressionTree> arguments) {
if (arguments.size() == 1) {
@ -2520,7 +2591,6 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
private static class SuggestionImpl implements Suggestion {
private final String continuation;
private final String filteringText;
private final boolean matchesType;
/**
@ -2530,19 +2600,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
* @param matchesType does the candidate match the target type
*/
public SuggestionImpl(String continuation, boolean matchesType) {
this(continuation, continuation, matchesType);
}
/**
* Create a {@code Suggestion} instance.
*
* @param continuation a candidate continuation of the user's input
* @param filteringText a text that should be used for filtering
* @param matchesType does the candidate match the target type
*/
public SuggestionImpl(String continuation, String filteringText, boolean matchesType) {
this.continuation = continuation;
this.filteringText = filteringText;
this.matchesType = matchesType;
}
@ -2620,4 +2678,53 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
}
}
record ElementSuggestionImpl(Element element, String keyword, boolean matchesType, int anchor, Supplier<String> documentation) implements ElementSuggestion {
}
final static class CompletionStateImpl implements CompletionState {
private final Collection<? extends Element> scopeContent;
private final Set<CompletionContext> completionContext;
private final TypeMirror selectorType;
private final Elements elementUtils;
private final Types typeUtils;
public CompletionStateImpl(Collection<? extends Element> scopeContent,
Set<CompletionContext> completionContext,
TypeMirror selectorType,
Elements elementUtils,
Types typeUtils) {
this.scopeContent = scopeContent;
this.completionContext = completionContext;
this.selectorType = selectorType;
this.elementUtils = elementUtils;
this.typeUtils = typeUtils;
}
@Override
public boolean availableUsingSimpleName(Element el) {
return scopeContent.contains(el);
}
@Override
public Set<CompletionContext> completionContext() {
return completionContext;
}
@Override
public TypeMirror selectorType() {
return selectorType;
}
@Override
public Elements elementUtils() {
return elementUtils;
}
@Override
public Types typeUtils() {
return typeUtils;
}
}
}

View File

@ -0,0 +1,303 @@
/*
* Copyright (c) 2025, 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.
*
* 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.
*/
/*
* @test
* @bug 8366691
* @summary Test JShell Completion API
* @library /tools/lib
* @modules jdk.compiler/com.sun.tools.javac.api
* jdk.compiler/com.sun.tools.javac.main
* jdk.jdeps/com.sun.tools.javap
* jdk.jshell/jdk.jshell:open
* @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask
* @build KullaTesting TestingInputStream Compiler
* @run junit CompletionAPITest
*/
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.QualifiedNameable;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import jdk.jshell.SourceCodeAnalysis.CompletionContext;
import jdk.jshell.SourceCodeAnalysis.CompletionState;
import jdk.jshell.SourceCodeAnalysis.ElementSuggestion;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class CompletionAPITest extends KullaTesting {
private static final long TIMEOUT = 2_000;
@Test
public void testAPI() {
waitIndexingFinished();
assertEval("String str = \"\";");
List<String> actual;
actual = completionSuggestions("str.", (state, suggestions) -> {
assertEquals(EnumSet.of(CompletionContext.QUALIFIED), state.completionContext());
});
assertTrue(actual.contains("java.lang.String.length()"), String.valueOf(actual));
actual = completionSuggestions("java.lang.", (state, suggestions) -> {
assertEquals(EnumSet.of(CompletionContext.QUALIFIED), state.completionContext());
});
assertTrue(actual.contains("java.lang.String"), String.valueOf(actual));
actual = completionSuggestions("java.", (state, suggestions) -> {
assertEquals(EnumSet.of(CompletionContext.QUALIFIED), state.completionContext());
});
assertTrue(actual.contains("java.lang"), String.valueOf(actual));
assertEval("@interface Ann2 { }");
assertEval("@interface Ann1 { Ann2 value(); }");
actual = completionSuggestions("@Ann", (state, suggestions) -> {
assertEquals(EnumSet.of(CompletionContext.TYPES_AS_ANNOTATIONS), state.completionContext());
});
assertTrue(actual.containsAll(Set.of("Ann1", "Ann2")), String.valueOf(actual));
actual = completionSuggestions("@Ann1(", (state, suggestions) -> {
assertEquals(EnumSet.of(CompletionContext.ANNOTATION_ATTRIBUTE,
CompletionContext.TYPES_AS_ANNOTATIONS),
state.completionContext());
});
assertTrue(actual.contains("Ann2"), String.valueOf(actual));
actual = completionSuggestions("import static java.lang.String.", (state, suggestions) -> {
assertEquals(EnumSet.of(CompletionContext.QUALIFIED, CompletionContext.NO_PAREN), state.completionContext());
});
assertTrue(actual.contains("java.lang.String.valueOf(int arg0)"), String.valueOf(actual));
actual = completionSuggestions("java.util.function.IntFunction<String> f = String::", (state, suggestions) -> {
assertEquals(EnumSet.of(CompletionContext.QUALIFIED, CompletionContext.NO_PAREN), state.completionContext());
});
assertTrue(actual.contains("java.lang.String.valueOf(int arg0)"), String.valueOf(actual));
actual = completionSuggestions("str.^len", (state, suggestions) -> {
assertEquals(EnumSet.of(CompletionContext.QUALIFIED), state.completionContext());
});
assertTrue(actual.contains("java.lang.String.length()"), String.valueOf(actual));
actual = completionSuggestions("^@Depr", (state, suggestions) -> {
assertEquals(EnumSet.of(CompletionContext.TYPES_AS_ANNOTATIONS), state.completionContext());
});
assertTrue(actual.contains("java.lang.Deprecated"), String.valueOf(actual));
assertEval("import java.util.*;");
actual = completionSuggestions("^ArrayL", (state, suggestions) -> {
TypeElement arrayList =
suggestions.stream()
.filter(el -> el.element() != null)
.map(el -> el.element())
.filter(el -> el.getKind() == ElementKind.CLASS)
.map(el -> (TypeElement) el)
.filter(el -> el.getQualifiedName().contentEquals("java.util.ArrayList"))
.findAny()
.orElseThrow();
assertTrue(state.availableUsingSimpleName(arrayList));
assertEquals(EnumSet.noneOf(CompletionContext.class), state.completionContext());
});
assertTrue(actual.contains("java.util.ArrayList"), String.valueOf(actual));
completionSuggestions("(new java.util.ArrayList<String>()).", (state, suggestions) -> {
List<String> elsWithTypes =
suggestions.stream()
.filter(el -> el.element() != null)
.map(el -> el.element())
.filter(el -> el.getKind() == ElementKind.METHOD)
.map(el -> el.getSimpleName() + state.typeUtils()
.asMemberOf((DeclaredType) state.selectorType(), el)
.toString())
.toList();
assertTrue(elsWithTypes.contains("add(java.lang.String)boolean"));
});
}
@Test
public void testDocumentation() {
waitIndexingFinished();
Path classes = prepareZip();
getState().addToClasspath(classes.toString());
AtomicReference<Supplier<String>> documentation = new AtomicReference<>();
AtomicReference<Reference<Element>> clazz = new AtomicReference<>();
completionSuggestions("jshelltest.JShellTest", (state, suggestions) -> {
ElementSuggestion test =
suggestions.stream()
.filter(el -> el.element() != null)
.filter(el -> el.element().getKind() == ElementKind.CLASS)
.filter(el -> ((TypeElement) el.element()).getQualifiedName().contentEquals("jshelltest.JShellTest"))
.findAny()
.orElseThrow();
documentation.set(test.documentation());
clazz.set(new WeakReference<>(test.element()));
});
//throw away the JavacTaskPool, so that the cached javac instances are dropped:
getState().addToClasspath("undefined");
long start = System.currentTimeMillis();
while (clazz.get().get() != null && (System.currentTimeMillis() - start) < TIMEOUT) {
System.gc();
}
assertNull(clazz.get().get());
assertEquals("JShellTest 0 ", documentation.get().get());
}
@Test
public void testSignature() {
waitIndexingFinished();
assertEval("void test(int i) {}");
assertEval("void test(int i, int j) {}");
assertSignature("test(|", true, "void test(int i):0", "void test(int i, int j):0");
assertSignature("test(0, |", true, "void test(int i, int j):1");
}
private List<String> completionSuggestions(String input,
BiConsumer<CompletionState, List<? extends ElementSuggestion>> validator) {
int expectedAnchor = input.indexOf('^');
if (expectedAnchor != (-1)) {
input = input.substring(0, expectedAnchor) + input.substring(expectedAnchor + 1);
}
AtomicInteger mergedAnchor = new AtomicInteger(-1);
List<String> result = getAnalysis().completionSuggestions(input, input.length(), (state, suggestions) -> {
validator.accept(state, suggestions);
if (expectedAnchor != (-1)) {
for (ElementSuggestion sugg : suggestions) {
if (mergedAnchor.get() == (-1)) {
mergedAnchor.set(sugg.anchor());
} else {
assertEquals(mergedAnchor.get(), sugg.anchor());
}
}
}
return suggestions.stream()
.map(this::convertElement)
.toList();
});
if (expectedAnchor != (-1)) {
assertEquals(expectedAnchor, mergedAnchor.get());
}
return result;
}
private String convertElement(ElementSuggestion suggestion) {
if (suggestion.keyword() != null) {
return suggestion.keyword();
}
Element el = suggestion.element();
if (el.getKind().isClass() || el.getKind().isInterface() || el.getKind() == ElementKind.PACKAGE) {
String qualifiedName = ((QualifiedNameable) el).getQualifiedName().toString();
if (qualifiedName.startsWith("REPL.$JShell$")) {
String[] parts = qualifiedName.split("\\.", 3);
return parts[2];
} else {
return qualifiedName;
}
} else if (el.getKind().isField()) {
return ((QualifiedNameable) el.getEnclosingElement()).getQualifiedName().toString() +
"." +
el.getSimpleName();
} else if (el.getKind() == ElementKind.CONSTRUCTOR || el.getKind() == ElementKind.METHOD) {
String name = el.getKind() == ElementKind.CONSTRUCTOR ? "" : "." + el.getSimpleName();
ExecutableElement method = (ExecutableElement) el;
return ((QualifiedNameable) el.getEnclosingElement()).getQualifiedName().toString() +
name +
method.getParameters()
.stream()
.map(var -> var.asType().toString() + " " + var.getSimpleName())
.collect(Collectors.joining(", ", "(", ")"));
} else {
return el.getSimpleName().toString();
}
}
private Path prepareZip() {
String clazz =
"package jshelltest;\n" +
"/**JShellTest 0" +
" */\n" +
"public class JShellTest {\n" +
"}\n";
Path srcZip = Paths.get("src.zip");
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(srcZip))) {
out.putNextEntry(new JarEntry("jshelltest/JShellTest.java"));
out.write(clazz.getBytes());
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
compiler.compile(clazz);
try {
Field availableSources = Class.forName("jdk.jshell.SourceCodeAnalysisImpl").getDeclaredField("availableSourcesOverride");
availableSources.setAccessible(true);
availableSources.set(null, Arrays.asList(srcZip));
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | ClassNotFoundException ex) {
throw new IllegalStateException(ex);
}
return compiler.getClassDir();
}
//where:
private final Compiler compiler = new Compiler();
static {
try {
//disable reading of paramater names, to improve stability:
Class<?> analysisClass = Class.forName("jdk.jshell.SourceCodeAnalysisImpl");
Field params = analysisClass.getDeclaredField("COMPLETION_EXTRA_PARAMETERS");
params.setAccessible(true);
params.set(null, List.of());
} catch (ReflectiveOperationException ex) {
throw new IllegalStateException(ex);
}
}
}

View File

@ -44,6 +44,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.jar.JarEntry;
@ -901,7 +902,7 @@ public class CompletionSuggestionTest extends KullaTesting {
@Test
public void testAnnotation() {
assertCompletion("@Deprec|", "Deprecated");
assertCompletion("@Deprec|", "@Deprecated(");
assertCompletion("@Deprecated(|", "forRemoval = ", "since = ");
assertCompletion("@Deprecated(forRemoval = |", true, "false", "true");
assertCompletion("@Deprecated(forRemoval = true, |", "since = ");
@ -951,4 +952,16 @@ public class CompletionSuggestionTest extends KullaTesting {
assertCompletion("class S { public int length() { return 0; } } new S().len|", true, "length()");
assertSignature("void f() { } f(|", "void f()");
}
static {
try {
//disable reading of paramater names, to improve stability:
Class<?> analysisClass = Class.forName("jdk.jshell.SourceCodeAnalysisImpl");
Field params = analysisClass.getDeclaredField("COMPLETION_EXTRA_PARAMETERS");
params.setAccessible(true);
params.set(null, List.of());
} catch (ReflectiveOperationException ex) {
throw new IllegalStateException(ex);
}
}
}

View File

@ -44,6 +44,7 @@ import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -969,11 +970,21 @@ public class KullaTesting {
}
public void assertSignature(String code, String... expected) {
assertSignature(code, false, expected);
}
public void assertSignature(String code, boolean includeActive, String... expected) {
int cursor = code.indexOf('|');
code = code.replace("|", "");
assertTrue(cursor > -1, "'|' expected, but not found in: " + code);
List<Documentation> documentation = getAnalysis().documentation(code, cursor, false);
Set<String> docSet = documentation.stream().map(doc -> doc.signature()).collect(Collectors.toSet());
Function<Documentation, String> convert;
if (includeActive) {
convert = doc -> doc.signature() + ":" + doc.activeParameterIndex();
} else {
convert = doc -> doc.signature();
}
Set<String> docSet = documentation.stream().map(convert).collect(Collectors.toSet());
Set<String> expectedSet = Stream.of(expected).collect(Collectors.toSet());
assertEquals(expectedSet, docSet, "Input: " + code);
}

View File

@ -103,8 +103,8 @@ public class ToolTabSnippetTest extends UITesting {
inputSink.write("(" + TAB);
waitOutput(out, "\\(\n" +
resource("jshell.console.completion.current.signatures") + "\n" +
"JShellTest\\(String str\\)\n" +
"JShellTest\\(String str, int i\\)\n" +
"JShellTest\\(\\u001B\\[1mString str\\u001B\\[0m\\)\n" +
"JShellTest\\(\\u001B\\[1mString str\\u001B\\[0m, int i\\)\n" +
"\n" +
resource("jshell.console.see.documentation") +
REDRAW_PROMPT + "new JShellTest\\(");
@ -138,8 +138,8 @@ public class ToolTabSnippetTest extends UITesting {
"str \n" +
"\n" +
resource("jshell.console.completion.current.signatures") + "\n" +
"JShellTest\\(String str\\)\n" +
"JShellTest\\(String str, int i\\)\n" +
"JShellTest\\(\\u001B\\[1mString str\\u001B\\[0m\\)\n" +
"JShellTest\\(\\u001B\\[1mString str\\u001B\\[0m, int i\\)\n" +
"\n" +
resource("jshell.console.see.documentation") +
REDRAW_PROMPT + "new JShellTest\\(");