diff --git a/src/jdk.jshell/share/classes/jdk/internal/shellsupport/doc/JavadocHelper.java b/src/jdk.jshell/share/classes/jdk/internal/shellsupport/doc/JavadocHelper.java index e69f32097b28b..099aba269685a 100644 --- a/src/jdk.jshell/share/classes/jdk/internal/shellsupport/doc/JavadocHelper.java +++ b/src/jdk.jshell/share/classes/jdk/internal/shellsupport/doc/JavadocHelper.java @@ -118,7 +118,7 @@ public static JavadocHelper create(JavacTask mainTask, Collection getSourceLocations() { + return List.of(); + } }; } } @@ -147,6 +162,7 @@ public void close() throws IOException {} * @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 void close() throws IOException {} */ public abstract Element getSourceElement(Element forElement) throws IOException; + public abstract StoredElement getHandle(Element forElement); + public abstract Collection getSourceLocations(); + /**Closes the helper. * * @throws IOException if something foes wrong during the close @@ -165,16 +184,20 @@ public void close() throws IOException {} @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> signature2Source = new HashMap<>(); + private final Collection sourceLocations; - private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm) { + private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm, Collection sourceLocations) { this.mainTask = mainTask; this.baseFileManager = ((JavacTaskImpl) mainTask).getContext().get(JavaFileManager.class); this.fm = fm; + this.sourceLocations = sourceLocations; } @Override @@ -187,6 +210,16 @@ public String getResolvedDocComment(Element forElement) throws IOException { return getResolvedDocComment(sourceElement.fst, sourceElement.snd); } + @Override + public String getResolvedDocComment(StoredElement forElement) throws IOException { + Pair sourceElement = getSourceElement(forElement); + + if (sourceElement == null) + return null; + + return getResolvedDocComment(sourceElement.fst, sourceElement.snd); + } + @Override public Element getSourceElement(Element forElement) throws IOException { Pair sourceElement = getSourceElement(mainTask, forElement); @@ -202,7 +235,30 @@ public Element getSourceElement(Element forElement) throws IOException { 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 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 @@ private Stream superMethodsForInheritDoc(JavacTask task, .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 superTypeForInheritDoc(JavacTask task, Element type) { TypeElement clazz = (TypeElement) type; Stream result = interfaces(clazz); @@ -701,6 +757,35 @@ private String getThrownException(JavacTask task, TreePath rootOn, DocCommentTre return exc != null ? exc.toString() : null; } + private Pair getSourceElement(StoredElement el) throws IOException { + if (el == null) { + return null; + } + + String handle = el.handle(); + Pair cached = signature2Source.get(handle); + + if (cached != null) { + return cached.fst != null ? cached : null; + } + + Pair 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 getSourceElement(JavacTask origin, Element el) throws IOException { String handle = elementSignature(el); Pair cached = signature2Source.get(handle); diff --git a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java index 10da664cd03f1..f01edaa850abe 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java @@ -28,6 +28,12 @@ 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 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 List completionSuggestions(String input, int cursor, ElementSuggestionConvertor 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 interface Suggestion { 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 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(); + /** + * {@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 a custom target completion type. + * @since 26 + */ + public interface ElementSuggestionConvertor { + /** + * 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 convert(CompletionState state, List suggestions); + } + /** * A documentation for a candidate for continuation of the given user's input. */ diff --git a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java index 19bad110a5d6e..5daa8187d6620 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java @@ -114,11 +114,13 @@ 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.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 @@ private Tree.Kind guessKind(String code, boolean[] moduleImport) { @Override public List completionSuggestions(String code, int cursor, int[] anchor) { - suspendIndexing(); + ElementSuggestionConvertor convertor = (state, suggestions) -> { + Set 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 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 List completionSuggestions(String code, int cursor, ElementSuggestionConvertor convertor) { + suspendIndexing(); + try { + return completionSuggestionsImpl(code, cursor, convertor); } finally { resumeIndexing(); } } - private List completionSuggestionsImpl(String code, int cursor, int[] anchor) { + private List completionSuggestionsImpl(String code, int cursor, ElementSuggestionConvertor suggestionConvertor) { code = code.substring(0, cursor); Matcher m = JAVA_IDENTIFIER.matcher(code); String identifier = ""; @@ -302,31 +367,27 @@ private List completionSuggestionsImpl(String code, int cursor, int[ } 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 COMPLETION_EXTRA_PARAMETERS = List.of("-parameters"); - private List computeSuggestions(OuterWrap code, String inputCode, int cursor, String[] requiredPrefix, int[] anchor) { - return proc.taskFactory.analyze(code, at -> { + private List computeSuggestions(OuterWrap code, String inputCode, int cursor, String prefix, ElementSuggestionConvertor 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 result = new ArrayList<>(); TreePath tp = pathFor(topLevel, sp, code, cursor); if (tp != null) { + List result = new ArrayList<>(); Scope scope = at.trees().getScope(tp); + Collection scopeContent = scopeContent(at, scope, IDENTITY); + Set completionContext = EnumSet.noneOf(CompletionContext.class); Predicate accessibility = createAccessibilityFilter(at, tp); Predicate smartTypeFilter; Predicate smartFilter; Iterable 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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in } switch (tp.getLeaf().getKind()) { case MEMBER_REFERENCE, MEMBER_SELECT: { + completionContext.add(CompletionContext.QUALIFIED); + javax.lang.model.element.Name identifier; ExpressionTree expression; - Function 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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in ? ((MemberSelectTree) it.getQualifiedIdentifier()).getExpression().toString() + "." : ""; - addModuleElements(at, qualifiedPrefix, result); - - requiredPrefix[0] = qualifiedPrefix + requiredPrefix[0]; - anchor[0] = selectStart; + addModuleElements(at, javadoc, selectStart, qualifiedPrefix + prefix, result); - return result; + break; } boolean isImport = it != null; - List members = membersOf(at, site, staticOnly && !isImport && tp.getLeaf().getKind() == Kind.MEMBER_SELECT); + List members = membersOf(at, selectorType, staticOnly && !isImport && tp.getLeaf().getKind() == Kind.MEMBER_SELECT); Predicate filter = accessibility; if (isNewClass(tp)) { // new xxx.| @@ -434,7 +492,7 @@ private List computeSuggestions(OuterWrap code, String inputCode, in } 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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in 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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in 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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in 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 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())) + .toList()); + }); } Predicate 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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in // 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 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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in 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 present = annotation.getArguments() @@ -563,15 +631,18 @@ private List computeSuggestions(OuterWrap code, String inputCode, in .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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in 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 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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in 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 @@ private List computeSuggestions(OuterWrap code, String inputCode, in 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 @@ private boolean isPermittedAnnotationAttributeFieldType(AnalyzeTask at, TypeMirr IS_PACKAGE.test(encl); }; private final Function> IDENTITY = Collections::singletonList; - private final Function DEFAULT_PAREN = hasParams -> hasParams ? "(" : "()"; - private final Function NO_PAREN = hasParams -> ""; - - private void addElements(Iterable elements, Predicate accept, Predicate smart, List result) { - addElements(elements, accept, smart, DEFAULT_PAREN, result); - } - private void addElements(Iterable elements, Predicate accept, Predicate smart, Function paren, List result) { - Set 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 elements, Predicate accept, Predicate smart, int anchor, String prefix, List 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 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 result) { + List 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 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 String simpleName(Element el) { - return el.getKind() == ElementKind.CONSTRUCTOR ? el.getEnclosingElement().getSimpleName().toString() - : el.getSimpleName().toString(); + private boolean primitiveLikeClass(Element el) { + return el.asType().getKind().isPrimitive() || + el.asType().getKind() == TypeKind.VOID; } private List membersOf(AnalyzeTask at, TypeMirror site, boolean shouldGenerateDotClassItem) { @@ -1625,8 +1710,8 @@ private TypeMirror resultTypeOf(Element el) { }; } - private void addScopeElements(AnalyzeTask at, Scope scope, Function> elementConvertor, Predicate filter, Predicate smartFilter, List result) { - addElements(scopeContent(at, scope, elementConvertor), filter, smartFilter, result); + private void addScopeElements(AnalyzeTask at, JavadocHelper javadoc, Scope scope, Function> elementConvertor, Predicate filter, Predicate smartFilter, int anchor, String prefix, List result) { + addElements(javadoc, scopeContent(at, scope, elementConvertor), filter, smartFilter, anchor, prefix, result); } private Iterable> methodCandidates(AnalyzeTask at, TreePath invocation) { @@ -2520,7 +2605,6 @@ private static String whitespaces(String input, int offset) { private static class SuggestionImpl implements Suggestion { private final String continuation; - private final String filteringText; private final boolean matchesType; /** @@ -2530,19 +2614,7 @@ private static class SuggestionImpl implements Suggestion { * @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 +2692,53 @@ public String source() { } } + record ElementSuggestionImpl(Element element, String keyword, boolean matchesType, int anchor, Supplier documentation) implements ElementSuggestion { + } + + final static class CompletionStateImpl implements CompletionState { + + private final Collection scopeContent; + private final Set completionContext; + private final TypeMirror selectorType; + private final Elements elementUtils; + private final Types typeUtils; + + public CompletionStateImpl(Collection scopeContent, + Set 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() { + return completionContext; + } + + @Override + public TypeMirror selectorType() { + return selectorType; + } + + @Override + public Elements elementUtils() { + return elementUtils; + } + + @Override + public Types typeUtils() { + return typeUtils; + } + + } } diff --git a/test/langtools/jdk/jshell/CompletionAPITest.java b/test/langtools/jdk/jshell/CompletionAPITest.java new file mode 100644 index 0000000000000..3705dc7e0120a --- /dev/null +++ b/test/langtools/jdk/jshell/CompletionAPITest.java @@ -0,0 +1,293 @@ +/* + * 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 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 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()).", (state, suggestions) -> { + List 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> documentation = new AtomicReference<>(); + AtomicReference> 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()); + } + + private List completionSuggestions(String input, + BiConsumer> validator) { + int expectedAnchor = input.indexOf('^'); + + if (expectedAnchor != (-1)) { + input = input.substring(0, expectedAnchor) + input.substring(expectedAnchor + 1); + } + + AtomicInteger mergedAnchor = new AtomicInteger(-1); + + List 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); + } + } +} diff --git a/test/langtools/jdk/jshell/CompletionSuggestionTest.java b/test/langtools/jdk/jshell/CompletionSuggestionTest.java index 7907fa4d027ee..7d739efddc631 100644 --- a/test/langtools/jdk/jshell/CompletionSuggestionTest.java +++ b/test/langtools/jdk/jshell/CompletionSuggestionTest.java @@ -44,6 +44,7 @@ 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 void testCustomClassPathIndexing() { @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 void testMultiSnippet() { 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); + } + } }