From 99255c44ebabfa160a2a47c52f623a6209e2c3cf Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 20 Jul 2026 13:02:39 +0000 Subject: [PATCH] 8186876: JShell: access javadoc for user/library classes Co-authored-by: Alex Buckley Reviewed-by: vromero, asotona --- .../jshell/tool/ConsoleIOContext.java | 1 + .../jdk/internal/jshell/tool/JShellTool.java | 17 +- .../shellsupport/doc/JavadocHelper.java | 175 +++++----- .../share/classes/jdk/jshell/Eval.java | 40 ++- .../share/classes/jdk/jshell/JShell.java | 39 ++- .../jdk/jshell/MaskCommentsAndModifiers.java | 70 +++- .../jdk/jshell/SourceCodeAnalysis.java | 8 + .../jdk/jshell/SourceCodeAnalysisImpl.java | 214 +++++++++--- .../share/classes/jdk/jshell/Wrap.java | 17 +- src/jdk.jshell/share/man/jshell.md | 23 +- .../shellsupport/doc/JavadocHelperTest.java | 4 +- test/langtools/jdk/jshell/AnalysisTest.java | 9 + .../jshell/BinaryToSourceCodeMappingTest.java | 310 ++++++++++++++++++ .../jdk/jshell/CompletenessTest.java | 24 ++ .../jdk/jshell/CompletionAPITest.java | 17 +- .../jdk/jshell/CompletionSuggestionTest.java | 27 +- test/langtools/jdk/jshell/JavadocTest.java | 175 +++++++++- test/langtools/jdk/jshell/KullaTesting.java | 5 +- test/langtools/jdk/jshell/ToolSimpleTest.java | 2 + .../jdk/jshell/ToolTabSnippetTest.java | 131 ++++++-- 20 files changed, 1069 insertions(+), 239 deletions(-) create mode 100644 test/langtools/jdk/jshell/BinaryToSourceCodeMappingTest.java diff --git a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java index eba81b4be07..c5cbb238251 100644 --- a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java +++ b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java @@ -1308,6 +1308,7 @@ class ConsoleIOContext extends IOContext { case COMPLETE: case COMPLETE_WITH_SEMI: case CONSIDERED_INCOMPLETE: + case PREFIX: break; case EMPTY: return reject(repl, "jshell.console.empty"); diff --git a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java index 9a030f7e46b..196d51685c5 100644 --- a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java +++ b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, 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 @@ -1130,7 +1130,20 @@ public class JShellTool implements MessageHandler { ? currentNameSpace.tid(sn) : errorNamespace.tid(sn)) .remoteVMOptions(options.remoteVmOptions()) - .compilerOptions(options.compilerOptions()); + .compilerOptions(options.compilerOptions()) + .binarySourceMapping(binary -> { + String binaryPath = binary.toString(); + if (binaryPath.toLowerCase(Locale.ROOT).endsWith(".jar")) { + String sourceCandidatePath = + binaryPath.substring(0, binaryPath.length() - ".jar".length()) + "-sources.jar"; + Path sourceCandidate = Path.of(sourceCandidatePath); + + if (Files.exists(sourceCandidate)) { + return List.of(binary, sourceCandidate); + } + } + return List.of(binary); + }); if (executionControlSpec != null) { builder.executionEngine(executionControlSpec); } 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 112808420bc..6db40093863 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2026, 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 @@ -96,6 +96,7 @@ import com.sun.tools.javac.util.Assert; import com.sun.tools.javac.util.DefinedBy; import com.sun.tools.javac.util.DefinedBy.Api; import com.sun.tools.javac.util.Pair; +import java.util.function.Function; import jdk.internal.org.commonmark.ext.gfm.tables.TablesExtension; import jdk.internal.org.commonmark.node.Node; @@ -114,11 +115,12 @@ public abstract class JavadocHelper implements AutoCloseable { * @param sourceLocations paths where source files should be searched * @return a JavadocHelper */ - public static JavadocHelper create(JavacTask mainTask, Collection sourceLocations) { + public static JavadocHelper create(JavacTask mainTask, Collection sourceLocations, + Function extraBinaryName2FullSource) { StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null); try { fm.setLocationFromPaths(StandardLocation.SOURCE_PATH, sourceLocations); - return new OnDemandJavadocHelper(mainTask, fm, sourceLocations); + return new OnDemandJavadocHelper(mainTask, fm, extraBinaryName2FullSource); } catch (IOException ex) { try { fm.close(); @@ -140,16 +142,6 @@ public abstract class JavadocHelper implements AutoCloseable { public String getResolvedDocComment(StoredElement forElement) throws IOException { return null; } - - @Override - public StoredElement getHandle(Element forElement) { - return null; - } - - @Override - public Collection getSourceLocations() { - return List.of(); - } }; } } @@ -174,9 +166,6 @@ public abstract class JavadocHelper implements AutoCloseable { */ 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 @@ -184,20 +173,86 @@ public abstract class JavadocHelper implements AutoCloseable { @Override public abstract void close() throws IOException; - public record StoredElement(String module, String binaryName, String handle) {} + public record StoredElement(String module, String binaryName, String handle) { + public static StoredElement getStoredElement(JavacTask mainTask, 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); + } + + } + + private static String elementSignature(Element el) { + switch (el.getKind()) { + case ANNOTATION_TYPE: case CLASS: case ENUM: case INTERFACE: case RECORD: + return ((TypeElement) el).getQualifiedName().toString(); + case FIELD: + return elementSignature(el.getEnclosingElement()) + "." + el.getSimpleName() + ":" + el.asType(); + case ENUM_CONSTANT: + return elementSignature(el.getEnclosingElement()) + "." + el.getSimpleName(); + case EXCEPTION_PARAMETER: case LOCAL_VARIABLE: case PARAMETER: case RESOURCE_VARIABLE: + return el.getSimpleName() + ":" + el.asType(); + case CONSTRUCTOR: case METHOD: + StringBuilder header = new StringBuilder(); + header.append(elementSignature(el.getEnclosingElement())); + if (el.getKind() == ElementKind.METHOD) { + header.append("."); + header.append(el.getSimpleName()); + } + header.append("("); + String sep = ""; + ExecutableElement method = (ExecutableElement) el; + for (Iterator i = method.getParameters().iterator(); i.hasNext();) { + VariableElement p = i.next(); + header.append(sep); + header.append(p.asType()); + sep = ", "; + } + header.append(")"); + return header.toString(); + case PACKAGE, STATIC_INIT, INSTANCE_INIT, TYPE_PARAMETER, + OTHER, MODULE, RECORD_COMPONENT, BINDING_VARIABLE: + return el.toString(); + default: + throw Assert.error(el.getKind().name()); + } + } + + private static TypeElement topLevelType(Element el) { + if (el.getKind() == ElementKind.PACKAGE) + return null; + + while (el != null && el.getEnclosingElement().getKind() != ElementKind.PACKAGE) { + el = el.getEnclosingElement(); + } + + return el != null && (el.getKind().isClass() || el.getKind().isInterface()) ? (TypeElement) el : null; + } 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 final Function extraBinaryName2FullSource; - private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm, Collection sourceLocations) { + private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm, + Function extraBinaryName2FullSource) { this.mainTask = mainTask; this.baseFileManager = ((JavacTaskImpl) mainTask).getContext().get(JavaFileManager.class); this.fm = fm; - this.sourceLocations = sourceLocations; + this.extraBinaryName2FullSource = extraBinaryName2FullSource; } @Override @@ -235,29 +290,6 @@ public abstract class JavadocHelper implements AutoCloseable { return result; } - @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); @@ -822,52 +854,6 @@ public abstract class JavadocHelper implements AutoCloseable { } } //where: - private String elementSignature(Element el) { - switch (el.getKind()) { - case ANNOTATION_TYPE: case CLASS: case ENUM: case INTERFACE: case RECORD: - return ((TypeElement) el).getQualifiedName().toString(); - case FIELD: - return elementSignature(el.getEnclosingElement()) + "." + el.getSimpleName() + ":" + el.asType(); - case ENUM_CONSTANT: - return elementSignature(el.getEnclosingElement()) + "." + el.getSimpleName(); - case EXCEPTION_PARAMETER: case LOCAL_VARIABLE: case PARAMETER: case RESOURCE_VARIABLE: - return el.getSimpleName() + ":" + el.asType(); - case CONSTRUCTOR: case METHOD: - StringBuilder header = new StringBuilder(); - header.append(elementSignature(el.getEnclosingElement())); - if (el.getKind() == ElementKind.METHOD) { - header.append("."); - header.append(el.getSimpleName()); - } - header.append("("); - String sep = ""; - ExecutableElement method = (ExecutableElement) el; - for (Iterator i = method.getParameters().iterator(); i.hasNext();) { - VariableElement p = i.next(); - header.append(sep); - header.append(p.asType()); - sep = ", "; - } - header.append(")"); - return header.toString(); - case PACKAGE, STATIC_INIT, INSTANCE_INIT, TYPE_PARAMETER, - OTHER, MODULE, RECORD_COMPONENT, BINDING_VARIABLE: - return el.toString(); - default: - throw Assert.error(el.getKind().name()); - } - } - - private TypeElement topLevelType(Element el) { - if (el.getKind() == ElementKind.PACKAGE) - return null; - - while (el != null && el.getEnclosingElement().getKind() != ElementKind.PACKAGE) { - el = el.getEnclosingElement(); - } - - return el != null && (el.getKind().isClass() || el.getKind().isInterface()) ? (TypeElement) el : null; - } private void fillElementCache(JavacTask task, CompilationUnitTree cut) throws IOException { Trees trees = Trees.instance(task); @@ -907,8 +893,15 @@ public abstract class JavadocHelper implements AutoCloseable { binaryName, JavaFileObject.Kind.SOURCE); - if (jfo == null) - return null; + if (jfo == null) { + String fullSource = extraBinaryName2FullSource.apply(binaryName); + + if (fullSource == null) { + return null; + } + + jfo = SimpleJavaFileObject.forSource(URI.create("mem://Temp.java"), fullSource); + } List jfos = Arrays.asList(jfo); JavaFileManager patchFM = moduleName != null diff --git a/src/jdk.jshell/share/classes/jdk/jshell/Eval.java b/src/jdk.jshell/share/classes/jdk/jshell/Eval.java index 70f3b70fd66..e2026eda08a 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/Eval.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/Eval.java @@ -200,10 +200,11 @@ class Eval { * @return usually a singleton list of Snippet, but may be empty or multiple */ private List sourceToSnippets(String userSource) { - String compileSource = Util.trimEnd(new MaskCommentsAndModifiers(userSource, false).cleared()); - if (compileSource.length() == 0) { + String emptyCheck = Util.trimEnd(new MaskCommentsAndModifiers(userSource, false, true).cleared()); + if (emptyCheck.isEmpty()) { return Collections.emptyList(); } + String compileSource = Util.trimEnd(new MaskCommentsAndModifiers(userSource, false, false).cleared()); return state.taskFactory.parse(compileSource, pt -> { List units = pt.units(); if (units.isEmpty()) { @@ -221,7 +222,7 @@ class Eval { } // Erase illegal/ignored modifiers - String compileSourceInt = new MaskCommentsAndModifiers(compileSource, true).cleared(); + String compileSourceInt = new MaskCommentsAndModifiers(compileSource, true, false).cleared(); state.debug(DBG_GEN, "Kind: %s -- %s\n", unitTree.getKind(), unitTree); return switch (unitTree.getKind()) { @@ -337,6 +338,7 @@ class Eval { Set anonymousClasses = Collections.emptySet(); StringBuilder sbBrackets = new StringBuilder(); Tree baseType = vt.getType(); + int variableDeclarationStart = dis.getStartPosition(vt); if (vt.getType() != null && vt.getType().getKind() != Tree.Kind.VAR_TYPE) { tds.scan(baseType); // Not dependent on initializer fullTypeName = displayType = typeName = EvalPretty.prettyExpr((JCTree) vt.getType(), false); @@ -350,7 +352,7 @@ class Eval { } else { DiagList dl = trialCompile(Wrap.methodWrap(compileSource)); if (dl.hasErrors()) { - return compileFailResult(dl, userSource, kindOfTree(unitTree)); + return compileFailResult(dl, userSource, kindOfTree(unitTree), variableDeclarationStart); } Tree init = vt.getInitializer(); if (init != null) { @@ -424,7 +426,10 @@ class Eval { wname = new Wrap.RangeWrap(compileSource, rname); } } - Wrap guts = Wrap.varWrap(compileSource, typeWrap, sbBrackets.toString(), wname, + Wrap prefixWrap = variableDeclarationStart == 0 + ? null + : Wrap.rangeWrap(compileSource, new Range(0, variableDeclarationStart)); + Wrap guts = Wrap.varWrap(compileSource, prefixWrap, typeWrap, sbBrackets.toString(), wname, winit, enhancedDesugaring, anonDeclareWrap); DiagList modDiag = modifierDiagnostics(vt.getModifiers(), dis, true); Snippet snip = new VarSnippet(state.keyMap.keyForVariable(name), userSource, guts, @@ -730,7 +735,7 @@ class Eval { // Corralling Wrap corralled = new Corraller(dis, key.index(), compileSource).corralType(klassTree); - Wrap guts = Wrap.classMemberWrap(compileSource); + Wrap guts = Wrap.classMemberWrap(compileSource, dis.getStartPosition(klassTree)); Snippet snip = new TypeDeclSnippet(key, userSource, guts, name, snippetKind, corralled, tds.declareReferences(), tds.bodyReferences(), modDiag); @@ -776,6 +781,7 @@ class Eval { final MethodTree mt = (MethodTree) unitTree; //String name = userReadableName(mt.getName(), compileSource); final String name = mt.getName().toString(); + int methodDeclarationStart = dis.getStartPosition(mt); if (objectMethods.contains(name)) { // The name matches a method on Object, short of an overhaul, this // fails, see 8187137. Generate a descriptive error message @@ -790,7 +796,7 @@ class Eval { ? possibleStart // something wrong, punt : possibleStart + offset; - return compileFailResult(new DiagList(objectMethodNameDiag(name, start)), userSource, Kind.METHOD); + return compileFailResult(new DiagList(objectMethodNameDiag(name, start)), userSource, Kind.METHOD, methodDeclarationStart); } String parameterTypes = mt.getParameters() @@ -800,7 +806,7 @@ class Eval { Tree returnType = mt.getReturnType(); DiagList modDiag = modifierDiagnostics(mt.getModifiers(), dis, false); if (modDiag.hasErrors()) { - return compileFailResult(modDiag, userSource, Kind.METHOD); + return compileFailResult(modDiag, userSource, Kind.METHOD, methodDeclarationStart); } MethodKey key = state.keyMap.keyForMethod(name, parameterTypes); @@ -822,7 +828,7 @@ class Eval { } else { // normal method corralled = new Corraller(dis, key.index(), compileSource).corralMethod(mt); - guts = Wrap.classMemberWrap(compileSource); + guts = Wrap.classMemberWrap(compileSource, methodDeclarationStart); unresolvedSelf = null; } Range typeRange = dis.treeToRange(returnType); @@ -874,19 +880,31 @@ class Eval { * @return a rejected snippet */ private List compileFailResult(DiagList diags, String userSource, Kind probableKind) { + return compileFailResult(diags, userSource, probableKind, 0); + } + + /** + * The snippet has failed, return with the rejected snippet + * + * @param diags the failure diagnostics + * @param userSource the incoming bad user source + * @param declarationStart the start offset of the declaration (if any) + * @return a rejected snippet + */ + private List compileFailResult(DiagList diags, String userSource, Kind probableKind, int declarationStart) { ErroneousKey key = state.keyMap.keyForErroneous(); Snippet snip = new ErroneousSnippet(key, userSource, null, probableKind, SubKind.UNKNOWN_SUBKIND); snip.setFailed(diags); // Install wrapper for query by SourceCodeAnalysis.wrapper - String compileSource = Util.trimEnd(new MaskCommentsAndModifiers(userSource, true).cleared()); + String compileSource = Util.trimEnd(new MaskCommentsAndModifiers(userSource, true, false).cleared()); OuterWrap outer = switch (probableKind) { case IMPORT -> state.outerMap.wrapImport(Wrap.simpleWrap(compileSource), snip); case EXPRESSION -> state.outerMap.wrapInTrialClass(Wrap.methodReturnWrap(compileSource)); case VAR, TYPE_DECL, - METHOD -> state.outerMap.wrapInTrialClass(Wrap.classMemberWrap(compileSource)); + METHOD -> state.outerMap.wrapInTrialClass(Wrap.classMemberWrap(compileSource, declarationStart)); default -> state.outerMap.wrapInTrialClass(Wrap.methodWrap(compileSource)); }; snip.setOuterWrap(outer); diff --git a/src/jdk.jshell/share/classes/jdk/jshell/JShell.java b/src/jdk.jshell/share/classes/jdk/jshell/JShell.java index 336746047dc..535e4df02fb 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/JShell.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/JShell.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, 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 @@ -31,9 +31,11 @@ import java.io.InputStream; import java.io.InterruptedIOException; import java.io.PrintStream; import java.net.InetAddress; +import java.nio.file.Path; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -100,6 +102,7 @@ public class JShell implements AutoCloseable { final List extraRemoteVMOptions; final List extraCompilerOptions; final Function fileManagerMapping; + final Function> binarySourceMapping; private int nextKeyIndex = 1; @@ -125,6 +128,7 @@ public class JShell implements AutoCloseable { this.extraRemoteVMOptions = b.extraRemoteVMOptions; this.extraCompilerOptions = b.extraCompilerOptions; this.fileManagerMapping = b.fileManagerMapping; + this.binarySourceMapping = b.binarySourceMapping; try { if (b.executionControlProvider != null) { executionControl = b.executionControlProvider.generate(new ExecutionEnvImpl(), @@ -183,6 +187,7 @@ public class JShell implements AutoCloseable { Map executionControlParameters; String executionControlSpec; Function fileManagerMapping; + Function> binarySourceMapping; Builder() { } @@ -414,6 +419,38 @@ public class JShell implements AutoCloseable { return this; } + /** + * Add a mapping from roots containing classfiles to roots containing + * their corresponding sources. + * + *

The given {@code binarySourceMapping} will be called for various paths + * used by JShell. Some of them may be distinct from roots specified in classpath + * and module path. + * + *

When looking for a source for a binary class or interface, the roots + * containing sources are searched in the given order, and once + * a source file corresponding to the given class or interface is found, + * the rest of the roots containing sources will be ignored. + * + *

The results of calling {@code binarySourceMapping} may be cached, and the + * same file may or may not be queried again. + * + *

The result of calling the {@code binarySourceMapping} may be {@code null}, + * which will be treated the same way as an empty collection. + * + *

If the result value is of a type that is {@link AutoCloseable}, then it will + * be closed when not needed anymore. + * + * @param binarySourceMapping the binary to source mapper, or {@code null} if none. + * @return the {@code Builder} instance (for use in chained + * initialization) + * @since 28 + */ + public Builder binarySourceMapping(Function> binarySourceMapping) { + this.binarySourceMapping = binarySourceMapping; + return this; + } + /** * Builds a JShell state engine. This is the entry-point to all JShell * functionality. This creates a remote process for execution. It is diff --git a/src/jdk.jshell/share/classes/jdk/jshell/MaskCommentsAndModifiers.java b/src/jdk.jshell/share/classes/jdk/jshell/MaskCommentsAndModifiers.java index becdb60129a..858c6a37509 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/MaskCommentsAndModifiers.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/MaskCommentsAndModifiers.java @@ -72,11 +72,14 @@ class MaskCommentsAndModifiers { // initial modifier section private boolean maskModifiers; + //should documentation comments be masked? + private boolean maskDocComments; + // Does the string end with an unclosed '/*' style comment? private boolean openToken = false; - MaskCommentsAndModifiers(String s, boolean maskModifiers) { - this(s, maskModifiers, IGNORED_MODIFIERS); + MaskCommentsAndModifiers(String s, boolean maskModifiers, boolean maskDocComments) { + this(s, maskModifiers, IGNORED_MODIFIERS, maskDocComments); } MaskCommentsAndModifiers(String s, Set ignoredModifiers) { @@ -84,10 +87,15 @@ class MaskCommentsAndModifiers { } MaskCommentsAndModifiers(String s, boolean maskModifiers, Set ignoredModifiers) { + this(s, maskModifiers, ignoredModifiers, true); + } + + MaskCommentsAndModifiers(String s, boolean maskModifiers, Set ignoredModifiers, boolean maskDocComments) { this.str = s; this.length = s.length(); this.maskModifiers = maskModifiers; this.ignoredModifiers = ignoredModifiers; + this.maskDocComments = maskDocComments; read(); while (c >= 0) { next(); @@ -154,6 +162,14 @@ class MaskCommentsAndModifiers { } } + private void writeMaskOrNotMask(int ch, boolean mask) { + if (mask) { + writeMask(ch); + } else { + write(ch); + } + } + @SuppressWarnings("fallthrough") private void next() { switch (c) { @@ -199,30 +215,50 @@ class MaskCommentsAndModifiers { case '/': read(); switch (c) { - case '*': - writeMask('/'); - writeMask(c); - int prevc = 0; - while (read() >= 0 && (c != '/' || prevc != '*')) { + case '*' -> { + read(); + boolean mask; + if (maskDocComments || c != '*') { + writeMask("/*"); writeMask(c); + mask = true; + } else { + read(); + if (c == '/') { + //special case: /**/ + writeMask("/**/"); + openToken = false; + break; + } + write("/**"); + write(c); + mask = false; //do not mask javadoc comments + } + + int prevc = c; + while (read() >= 0 && (c != '/' || prevc != '*')) { + writeMaskOrNotMask(c, mask); prevc = c; } - writeMask(c); + writeMaskOrNotMask(c, mask); openToken = c < 0; - break; - case '/': - writeMask('/'); - writeMask(c); + } + case '/' -> { + read(); + boolean mask = maskDocComments || c != '/'; //do not mask javadoc comments + writeMaskOrNotMask('/', mask); + writeMaskOrNotMask('/', mask); + writeMaskOrNotMask(c, mask); while (read() >= 0 && c != '\n' && c != '\r') { - writeMask(c); + writeMaskOrNotMask(c, mask); } - writeMask(c); - break; - default: + writeMaskOrNotMask(c, mask); + } + default -> { maskModifiers = false; write('/'); unread(); - break; + } } break; case '@': diff --git a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java index 91bdde888fa..f78828e1c10 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java @@ -273,6 +273,14 @@ public abstract class SourceCodeAnalysis { */ CONSIDERED_INCOMPLETE(false), + /** + * Snippet is likely a prefix of a real snippet. Typically, the snippet + * is a documentation comment, after which it is expected that a declaration + * will follow. + * + * @since 28 + */ + PREFIX(false), /** * An empty input. diff --git a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java index 215b412003d..b6146fb9588 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java @@ -46,6 +46,7 @@ import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.TypeParameterTree; import com.sun.source.tree.VariableTree; import com.sun.source.tree.YieldTree; +import com.sun.source.util.JavacTask; import com.sun.source.util.SourcePositions; import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; @@ -173,6 +174,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { private final JShell proc; private final CompletenessAnalyzer ca; + private final Function binaryName2SnipperOuterWrap; private final List closeables = new ArrayList<>(); private final Map currentIndexes = new HashMap<>(); private int indexVersion; @@ -184,6 +186,14 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { this.proc = proc; this.ca = new CompletenessAnalyzer(proc); + this.binaryName2SnipperOuterWrap = binaryName -> { + return proc.snippets() + .filter(s -> binaryName.equals(s.classFullName())) + .map(s -> s.outerWrap().wrapped()) + .findAny() + .orElse(null); + }; + int cpVersion = classpathVersion = 1; INDEXER.submit(() -> refreshIndexes(cpVersion)); @@ -191,7 +201,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { @Override public CompletionInfo analyzeCompletion(String srcInput) { - MaskCommentsAndModifiers mcm = new MaskCommentsAndModifiers(srcInput, false); + MaskCommentsAndModifiers mcm = new MaskCommentsAndModifiers(srcInput, false, true); if (mcm.endsWithOpenToken()) { proc.debug(DBG_COMPA, "Incomplete (open comment): %s\n", srcInput); return new CompletionInfoImpl(DEFINITELY_INCOMPLETE, null, srcInput + '\n'); @@ -199,8 +209,9 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { String cleared = mcm.cleared(); String trimmedInput = Util.trimEnd(cleared); if (trimmedInput.isEmpty()) { - // Just comment or empty - return new CompletionInfoImpl(Completeness.EMPTY, srcInput, ""); + // Just comment, empty, or javadoc prefix + boolean hasDocumentation = !Util.trimEnd(new MaskCommentsAndModifiers(srcInput, false, false).cleared()).isEmpty(); + return new CompletionInfoImpl(hasDocumentation ? Completeness.PREFIX : Completeness.EMPTY, srcInput, ""); } CaInfo info = ca.scan(trimmedInput); Completeness status = info.status; @@ -369,7 +380,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { 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())) { + try (JavadocConfig javadoc = new JavadocConfig(at.task, getAllPaths())) { SourcePositions sp = at.trees().getSourcePositions(); CompilationUnitTree topLevel = at.firstCuTree(); TreePath tp = pathFor(topLevel, sp, code, cursor); @@ -713,9 +724,6 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { return suggestionConvertor.convert(completionState, result); } - } catch (IOException ex) { - //TODO: - ex.printStackTrace(); } return Collections.emptyList(); }); @@ -805,7 +813,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { //TODO: OuterWrap duplicated OuterWrap codeWrap = switch (guessKind(snippet)) { case IMPORT -> proc.outerMap.wrapImport(Wrap.simpleWrap(snippet + "any.any"), null); - case CLASS, METHOD -> proc.outerMap.wrapInTrialClass(Wrap.classMemberWrap(snippet)); + case CLASS, METHOD -> proc.outerMap.wrapInTrialClass(Wrap.classMemberWrap(snippet, 0)); default -> proc.outerMap.wrapInTrialClass(Wrap.methodWrap(snippet)); }; String wrappedCode = codeWrap.wrapped(); @@ -1227,7 +1235,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { }; private final Function> IDENTITY = Collections::singletonList; - private void addElements(JavadocHelper javadoc, Iterable elements, Predicate accept, Predicate smart, int anchor, String prefix, List result) { + private void addElements(JavadocConfig javadoc, Iterable elements, Predicate accept, Predicate smart, int anchor, String prefix, List result) { for (Element c : elements) { if (!accept.test(c) || !simpleContinuationName(c).startsWith(prefix)) continue; @@ -1237,10 +1245,11 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { continue; } StoredElement stored = javadoc.getHandle(c); - Collection sourceLocations = javadoc.getSourceLocations(); + Collection binaryPaths = javadoc.getBinaryPaths(); 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)) { + try (CloseableSources closeableSources = findSources(binaryPaths); + JavadocHelper nestedJavadoc = JavadocHelper.create(task.task, closeableSources.sources(), binaryName2SnipperOuterWrap)) { return nestedJavadoc.getResolvedDocComment(stored); } catch (IOException ex) { ex.printStackTrace(); @@ -1251,7 +1260,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { } } - private void addModuleElements(AnalyzeTask at, JavadocHelper javadoc, int anchor, + private void addModuleElements(AnalyzeTask at, JavadocConfig javadoc, int anchor, String prefix, List result) { for (ModuleElement me : at.getElements().getAllModuleElements()) { @@ -1413,6 +1422,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { int cpVersion = ++classpathVersion; INDEXER.submit(() -> refreshIndexes(cpVersion)); + + allPaths = null; } } @@ -1704,7 +1715,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { }; } - private void addScopeElements(AnalyzeTask at, JavadocHelper javadoc, Scope scope, Function> elementConvertor, Predicate filter, Predicate smartFilter, int anchor, String prefix, List result) { + private void addScopeElements(AnalyzeTask at, JavadocConfig 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); } @@ -1903,7 +1914,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { List result = Collections.emptyList(); - try (JavadocHelper helper = JavadocHelper.create(at.task, findSources())) { + try (CloseableSources sources = findSources(getAllPaths()); + JavadocHelper helper = JavadocHelper.create(at.task, sources.sources(), binaryName2SnipperOuterWrap)) { int parameterIndexFin = parameterIndex; result = elements.map(el -> constructDocumentation(at, helper, el, parameterIndexFin, computeJavadoc)) .filter(Objects::nonNull) @@ -1933,7 +1945,11 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { } public void close() { - for (AutoCloseable closeable : closeables) { + close(closeables); + } + + private void close(List toClose) { + for (AutoCloseable closeable : toClose) { try { closeable.close(); } catch (Exception ex) { @@ -1966,17 +1982,51 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { .allMatch(param -> param.getSimpleName().toString().startsWith("arg")); } - private static List availableSourcesOverride; //for tests - private List availableSources; + private CloseableSources findSources(Collection forPaths) { + List toClose = new ArrayList<>(); - private List findSources() { - if (availableSources != null) { - return availableSources; + try { + List sourcePaths = new ArrayList<>(); + + sourcePaths.addAll(jdkSources()); + + if (proc.binarySourceMapping != null) { + for (Path binaryPath : forPaths) { + Iterable mappedSources = proc.binarySourceMapping.apply(binaryPath); + + if (mappedSources != null) { + if (mappedSources instanceof AutoCloseable closeable) { + toClose.add(closeable); + } + + mappedSources.forEach(sourcePaths::add); + } + } + } + + CloseableSources result = new CloseableSources(this, sourcePaths, toClose); + + toClose = List.of(); + + return result; + } finally { + close(toClose); } - if (availableSourcesOverride != null) { - return availableSources = availableSourcesOverride; + } + + private static List jdkSourcesOverride; //for tests + private List jdkSources; + + private synchronized List jdkSources() { + if (jdkSources != null) { + return jdkSources; } - List result = new ArrayList<>(); + if (jdkSourcesOverride != null) { + return jdkSources = jdkSourcesOverride; + } + + jdkSources = new ArrayList<>(); + Path home = Paths.get(System.getProperty("java.home")); Path srcZip = home.resolve("lib").resolve("src.zip"); if (!Files.isReadable(srcZip)) @@ -1991,13 +2041,13 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { if (Files.exists(root.resolve("java/lang/Object.java".replace("/", zipFO.getSeparator())))) { //non-modular format: - result.add(srcZip); + jdkSources.add(srcZip); } else if (Files.exists(root.resolve("java.base/java/lang/Object.java".replace("/", zipFO.getSeparator())))) { //modular format: try (DirectoryStream ds = Files.newDirectoryStream(root)) { for (Path p : ds) { if (Files.isDirectory(p)) { - result.add(p); + jdkSources.add(p); } } } @@ -2011,18 +2061,21 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { if (keepOpen) { closeables.add(zipFO); } else { - try { - zipFO.close(); - } catch (IOException ex) { - proc.debug(ex, "SourceCodeAnalysisImpl.findSources()"); - } + close(List.of(zipFO)); } } } } - return availableSources = result; + + return jdkSources; } + record CloseableSources(SourceCodeAnalysisImpl analysis, List sources, List toClose) implements AutoCloseable { + @Override + public void close() { + analysis.close(toClose()); + } + } private Element getOriginalEnclosingElement(Element el) { if (el instanceof Symbol s) el = s.baseSymbol(); return el.getEnclosingElement(); @@ -2186,7 +2239,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { case IMPORT: return new QualifiedNames(Collections.emptyList(), -1, true, false); case METHOD: - codeWrap = proc.outerMap.wrapInTrialClass(Wrap.classMemberWrap(codeFin)); + codeWrap = proc.outerMap.wrapInTrialClass(Wrap.classMemberWrap(codeFin, 0)); break; default: codeWrap = proc.outerMap.wrapInTrialClass(Wrap.methodWrap(codeFin)); @@ -2269,22 +2322,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { //update indexes, either initially or after a classpath change: private void refreshIndexes(int version) { try { - Collection paths = proc.taskFactory.parse("", task -> { - MemoryFileManager fm = proc.taskFactory.fileManager(); - Collection _paths = new ArrayList<>(); - try { - appendPaths(fm, StandardLocation.PLATFORM_CLASS_PATH, _paths); - appendPaths(fm, StandardLocation.CLASS_PATH, _paths); - appendPaths(fm, StandardLocation.SOURCE_PATH, _paths); - appendModulePaths(fm, StandardLocation.SYSTEM_MODULES, _paths); - appendModulePaths(fm, StandardLocation.UPGRADE_MODULE_PATH, _paths); - appendModulePaths(fm, StandardLocation.MODULE_PATH, _paths); - return _paths; - } catch (Exception ex) { - proc.debug(ex, "SourceCodeAnalysisImpl.refreshIndexes(" + version + ")"); - return List.of(); - } - }); + Collection paths = getAllPaths(); Map newIndexes = new HashMap<>(); @@ -2323,6 +2361,50 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { } } + private Collection allPaths; + private Collection getAllPaths() { + int originalClasspathVersion; + + synchronized (currentIndexes) { + if (allPaths != null) { + return allPaths; + } + + originalClasspathVersion = classpathVersion; + } + + Collection paths = proc.taskFactory.parse("", task -> { + MemoryFileManager fm = proc.taskFactory.fileManager(); + Collection _paths = new ArrayList<>(); + try { + appendPaths(fm, StandardLocation.PLATFORM_CLASS_PATH, _paths); + appendPaths(fm, StandardLocation.CLASS_PATH, _paths); + appendPaths(fm, StandardLocation.SOURCE_PATH, _paths); + appendModulePaths(fm, StandardLocation.SYSTEM_MODULES, _paths); + appendModulePaths(fm, StandardLocation.UPGRADE_MODULE_PATH, _paths); + appendModulePaths(fm, StandardLocation.MODULE_PATH, _paths); + return _paths; + } catch (Exception ex) { + proc.debug(ex, "SourceCodeAnalysisImpl.getAllPaths(" + originalClasspathVersion + ")"); + return List.of(); + } + }); + + synchronized (currentIndexes) { + if (originalClasspathVersion != classpathVersion) { + paths = null; + } else { + allPaths = paths; + } + } + + if (paths == null) { + paths = getAllPaths(); + } + + return paths; + } + private void appendPaths(MemoryFileManager fm, Location loc, Collection paths) { Iterable locationPaths = fm.getLocationAsPaths(loc); if (locationPaths == null) @@ -2536,10 +2618,10 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { lastImportIsModuleImport = moduleImport[0]; } case CLASS, METHOD -> { - pendingWrap = declarationWrap = Wrap.classMemberWrap(whitespaces(code, startOffset) + current); + pendingWrap = declarationWrap = Wrap.classMemberWrap(whitespaces(code, startOffset) + current, 0); } case VARIABLE -> { - declarationWrap = Wrap.classMemberWrap(whitespaces(code, startOffset) + current); + declarationWrap = Wrap.classMemberWrap(whitespaces(code, startOffset) + current, 0); pendingWrap = Wrap.methodWrap(whitespaces(code, startOffset) + current); } default -> { @@ -2721,4 +2803,36 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { } } + + private static class JavadocConfig implements AutoCloseable { + private JavacTask mainTask; + private Collection binaryPaths; + + public JavadocConfig(JavacTask mainTask, Collection binaryPaths) { + this.mainTask = mainTask; + this.binaryPaths = binaryPaths; + } + + public StoredElement getHandle(Element el) { + if (mainTask == null) { + throw new IllegalStateException("Already closed."); + } + + return StoredElement.getStoredElement(mainTask, el); + } + + public Collection getBinaryPaths() { + if (binaryPaths == null) { + throw new IllegalStateException("Already closed."); + } + + return binaryPaths; + } + + @Override + public void close() { + mainTask = null; + binaryPaths = null; + } + } } diff --git a/src/jdk.jshell/share/classes/jdk/jshell/Wrap.java b/src/jdk.jshell/share/classes/jdk/jshell/Wrap.java index 990dbd8bece..b5d0592be38 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/Wrap.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/Wrap.java @@ -87,11 +87,11 @@ abstract class Wrap implements GeneralWrap { * @return a Wrap that declares the given variable, potentially with * an initialization method */ - public static Wrap varWrap(String source, Wrap wtype, String brackets, + public static Wrap varWrap(String source, Wrap prefix, Wrap wtype, String brackets, Wrap wname, Wrap winit, boolean enhanced, Wrap anonDeclareWrap) { List components = new ArrayList<>(); - components.add(new VarDeclareWrap(wtype, brackets, wname)); + components.add(new VarDeclareWrap(prefix, wtype, brackets, wname)); Wrap wmeth; if (winit == null) { @@ -164,9 +164,10 @@ abstract class Wrap implements GeneralWrap { return new RangeWrap(source, range); } - public static Wrap classMemberWrap(String source) { - Wrap w = new NoWrap(source); - return new CompoundWrap(" public static\n ", w); + public static Wrap classMemberWrap(String source, int modifierInsertPos) { + Wrap prefix = modifierInsertPos == 0 ? null : new RangeWrap(source, new Range(0, modifierInsertPos)); + Wrap suffix = new RangeWrap(source, new Range(modifierInsertPos, source.length())); + return new CompoundWrap(prefix, " public static ", suffix); } private static int countLines(String s) { @@ -257,6 +258,8 @@ abstract class Wrap implements GeneralWrap { snlnLast = w.lastSnippetLine(); } sb.append(w.wrapped()); + } else if (o == null) { + //permit missing delegates } else { throw new InternalError("Bad object in CommoundWrap: " + o); } @@ -558,8 +561,8 @@ abstract class Wrap implements GeneralWrap { private static class VarDeclareWrap extends CompoundWrap { - VarDeclareWrap(Wrap wtype, String brackets, Wrap wname) { - super(" public static ", wtype, brackets + " ", wname, semi(wname)); + VarDeclareWrap(Wrap prefix, Wrap wtype, String brackets, Wrap wname) { + super(prefix, " public static ", wtype, brackets + " ", wname, semi(wname)); } } diff --git a/src/jdk.jshell/share/man/jshell.md b/src/jdk.jshell/share/man/jshell.md index 62237f1447f..71381ad094f 100644 --- a/src/jdk.jshell/share/man/jshell.md +++ b/src/jdk.jshell/share/man/jshell.md @@ -1,5 +1,5 @@ --- -# Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2017, 2026, 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 @@ -772,6 +772,27 @@ JShell. item can't be determined from what was entered, then possible options are provided. + Use the Tab key after entering the name of a class, interface, method, or + field to see the description of the class, interface, method, or field. + The description is based on the javadoc comment for the declaration of the + class, interface, method, or field, when sources for the declaration are + available. + + The sources for the JDK classes are looked up in the JDK's sources, if they + can be found in the JDK installation. The sources may need to be added to + the JDK installation separately. + + The sources of classes, interfaces, methods, and fields declared on the class path + or module path are looked up in the entry on the class path or module path which + contains the declaration. If the entry is a JAR file and it does not contain the source, + then the source is looked up in a JAR file with the same path and name but with `.jar` + replaced by `-sources.jar`, if such a file exists. For example, if `/lib.jar` + does not contain the source, then `/lib-sources.jar` is searched if it exists. + + When searching for sources, JShell expects source files to be co-located with class files. + That is, the source for a given class or interface must be in a file with the same path and name + as the class file for the corresponding top-level class or interface, with `.class` replaced by `.java`. + When entering a method call, use the Tab key after the method call's opening parenthesis to see the parameters for the method. If the method has more than one signature, then all signatures are displayed. Pressing the diff --git a/test/langtools/jdk/internal/shellsupport/doc/JavadocHelperTest.java b/test/langtools/jdk/internal/shellsupport/doc/JavadocHelperTest.java index 4f739d38d3d..6bd8ddf510a 100644 --- a/test/langtools/jdk/internal/shellsupport/doc/JavadocHelperTest.java +++ b/test/langtools/jdk/internal/shellsupport/doc/JavadocHelperTest.java @@ -516,7 +516,7 @@ public class JavadocHelperTest { Element el = getElement.apply(task); - try (JavadocHelper helper = JavadocHelper.create(task, Arrays.asList(srcZip))) { + try (JavadocHelper helper = JavadocHelper.create(task, Arrays.asList(srcZip), _ -> null)) { String javadoc = helper.getResolvedDocComment(el); assertEquals(expectedJavadoc, javadoc); @@ -608,7 +608,7 @@ public class JavadocHelperTest { List exports = ElementFilter.exportsIn(me.getDirectives()); for (ExportsDirective ed : exports) { - try (JavadocHelper helper = JavadocHelper.create(task, sources)) { + try (JavadocHelper helper = JavadocHelper.create(task, sources, _ -> null)) { List content = ed.getPackage().getEnclosedElements(); for (TypeElement clazz : ElementFilter.typesIn(content)) { diff --git a/test/langtools/jdk/jshell/AnalysisTest.java b/test/langtools/jdk/jshell/AnalysisTest.java index 478301f3fd5..3e22523e0b7 100644 --- a/test/langtools/jdk/jshell/AnalysisTest.java +++ b/test/langtools/jdk/jshell/AnalysisTest.java @@ -47,6 +47,7 @@ public class AnalysisTest extends KullaTesting { assertAnalyze("/*zoo*/int x=3 ;/*test*/", "/*zoo*/int x=3 ;/*test*/", "", true); assertAnalyze("int x=3 /*;*/", "int x=3; /*;*/", "", true); assertAnalyze("void m5() {} /*hgjghj*/", "void m5() {} /*hgjghj*/", "", true); + assertAnalyze("void m5() {} /**hgjghj*/", "void m5() {} /**hgjghj*/", "", true); assertAnalyze("int ff; int v /*hgjghj*/", "int ff;", " int v /*hgjghj*/", true); } @@ -65,4 +66,12 @@ public class AnalysisTest extends KullaTesting { assertAnalyze("/*zoo*/45;/*test*/", "/*zoo*/45;/*test*/", "", true); assertAnalyze("45/*;*/", "45/*;*/", "", true); } + + @Test + public void testOnlyComments() { + assertAnalyze("/*test*/", "/*test*/", "", false); + assertAnalyze("/**test*/", "/**test*/", "", false); + assertAnalyze("//test", "//test", "", false); + assertAnalyze("///test", "///test", "", false); + } } diff --git a/test/langtools/jdk/jshell/BinaryToSourceCodeMappingTest.java b/test/langtools/jdk/jshell/BinaryToSourceCodeMappingTest.java new file mode 100644 index 00000000000..b7ff55b3fd2 --- /dev/null +++ b/test/langtools/jdk/jshell/BinaryToSourceCodeMappingTest.java @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2026, 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 8186876 + * @summary Test binary to source mapping in completion + * @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/othervm/timeout=480 BinaryToSourceCodeMappingTest + */ + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Field; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import jdk.jshell.JShell; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryToSourceCodeMappingTest extends KullaTesting { + + private static Path classesDir; + private static Path transientClassesDir; + private static Path srcDir; + private static Path srcZip; + private static Path srcZipWithNestedPath; + private static Path brokenSrcZip; + private final TestInfo testInfo; + private final AtomicBoolean closeCalled = new AtomicBoolean(); + + public BinaryToSourceCodeMappingTest(TestInfo info) { + this.testInfo = info; + } + + @Test + public void testNull() { + assertJavadoc("test.inner.Test|", + """ + test.inner.Test + null"""); + } + + @Test + public void testReturnsNull() { + assertJavadoc("test.inner.Test|", + """ + test.inner.Test + null"""); + } + + @Test + public void testSourcesAsDirectory() { + assertJavadoc("test.inner.Test|", + """ + test.inner.Test + Class javadoc."""); + assertJavadoc("test.inner.Test.test(|", + """ + void test.inner.Test.test(int i) + Test method. + @param i param"""); + } + + @Test + public void testSourcesAsZip() { + assertJavadoc("test.inner.Test|", + """ + test.inner.Test + Class javadoc."""); + assertJavadoc("test.inner.Test.test(|", + """ + void test.inner.Test.test(int i) + Test method. + @param i param"""); + } + + @Test + public void testSourcesDuplicate() { + //src.zip and broken-src.zip both available, + //only the first one should be used: + assertJavadoc("test.inner.Test|", + """ + test.inner.Test + Class javadoc."""); + assertJavadoc("test.inner.Test.test(|", + """ + void test.inner.Test.test(int i) + Test method. + @param i param"""); + } + + @Test + public void testSourcesAsZipNested() { + assertJavadoc("test.inner.Test|", + """ + test.inner.Test + Class javadoc."""); + assertJavadoc("test.inner.Test.test(|", + """ + void test.inner.Test.test(int i) + Test method. + @param i param"""); + } + + @Test + public void testClassPathModification() { + assertJavadoc("test.inner.Test|", + """ + test.inner.Test + null"""); + getState().addToClasspath(classesDir.toString()); + assertFalse(closeCalled.get()); + assertJavadoc("test.inner.Test|", + """ + test.inner.Test + Class javadoc."""); + assertTrue(closeCalled.get()); + closeCalled.set(false); + } + + @Test + public void testClosingAPIUse() { + getState().addToClasspath(classesDir.toString()); + String input = "test.inner.Test"; + AtomicReference> documentation = new AtomicReference<>(); + + getAnalysis().completionSuggestions(input, input.length(), (state, suggestions) -> { + Assertions.assertEquals(1, suggestions.size()); + documentation.set(suggestions.get(0).documentation()); + return List.of(""); + }); + //holding the documentation supplier, and using it later/outside of the convertor is OK; + //will open the sources, and should also close them: + assertFalse(closeCalled.get()); + Assertions.assertEquals("Class javadoc.", documentation.get().get()); + assertTrue(closeCalled.get()); + closeCalled.set(false); + } + + @Override + public void setUp(Consumer bc) { + super.setUp(bc.andThen(b -> { + b.binarySourceMapping(switch (testInfo.getTestMethod().orElseThrow().getName()) { + case "testNull" -> null; + case "testReturnsNull" -> _ -> null; + case "testSourcesAsDirectory" -> p -> classesDir.equals(p) ? List.of(srcDir) : List.of(); + case "testSourcesAsZip" -> + p -> classesDir.equals(p) ? List.of(srcZip) : List.of(); + case "testSourcesDuplicate" -> + p -> classesDir.equals(p) ? List.of(srcZip, brokenSrcZip) : List.of(); + case "testClassPathModification", "testSourcesAsZipNested", "testClosingAPIUse" -> p -> { + if (!classesDir.equals(p)) { + return List.of(); + } + try { + FileSystem withNested = FileSystems.newFileSystem(srcZipWithNestedPath); + class CloseableIterable extends ArrayList implements AutoCloseable { + public CloseableIterable() { + super(List.of(withNested.getPath("root"))); + } + @Override + public void close() throws Exception { + withNested.close(); + closeCalled.set(true); + } + } + + return new CloseableIterable(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }; + default -> throw new AssertionError(); + }); + })); + if ("testClassPathModification".equals(testInfo.getTestMethod().orElseThrow().getName())) { + addToClasspath(transientClassesDir); + } else { + addToClasspath(classesDir); + } + } + + @BeforeAll + public static void beforeAll() { + Compiler compiler = new Compiler(); + srcDir = Paths.get("src").toAbsolutePath(); + Path testOutDir = Paths.get("classes"); + String input = + """ + package test.inner; + ///Class javadoc. + public class Test { + ///Test method. + ///@param i param + public static void test(int i) { + } + } + """; + Path srcFile = srcDir.resolve("test").resolve("inner").resolve("Test.java"); + try { + Files.createDirectories(srcFile.getParent()); + try (Writer w = Files.newBufferedWriter(srcFile)) { + w.append(input); + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } + compiler.compile(testOutDir, input); + classesDir = compiler.getPath(testOutDir); + Path transientClasses = Paths.get("transientClasses"); + compiler.compile(transientClasses, input); + transientClassesDir = compiler.getPath(transientClasses); + srcZip = Paths.get("src.zip"); + compiler.jar(srcDir, srcZip, srcDir.relativize(srcFile).toString()); + srcZipWithNestedPath = Paths.get("srcWithNestedPath.zip"); + Path srcDir2 = Paths.get("src2").toAbsolutePath(); + Path srcFile2 = srcDir2.resolve("root").resolve("test").resolve("inner").resolve("Test.java"); + try { + Files.createDirectories(srcFile2.getParent()); + try (Writer w = Files.newBufferedWriter(srcFile2)) { + w.append(input); + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } + compiler.jar(srcDir2, srcZipWithNestedPath, srcDir2.relativize(srcFile2).toString()); + + brokenSrcZip = Paths.get("broken-src.zip"); + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(brokenSrcZip))) { + out.putNextEntry(new JarEntry("test/inner/Test.java")); + out.write(""" + package test.inner; + ///broken + public class Test { + ///broken + ///@param i broken + public static void test(int i) { + } + } + """.getBytes()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + @Override + protected void tearDownDone() { + switch(testInfo.getTestMethod().orElseThrow().getName()) { + case "testSourcesAsZipNested" -> assertTrue(closeCalled.get()); + case "testClassPathModification", "testClosingAPIUse" -> assertFalse(closeCalled.get()); + } + super.tearDownDone(); + } + + 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/CompletenessTest.java b/test/langtools/jdk/jshell/CompletenessTest.java index 1d57aef8210..f938a74d8a0 100644 --- a/test/langtools/jdk/jshell/CompletenessTest.java +++ b/test/langtools/jdk/jshell/CompletenessTest.java @@ -229,6 +229,19 @@ public class CompletenessTest extends KullaTesting { "void t(int i) { int v = switch (i) { case 0 -> throw new IllegalStateException();", }; + static final String[] empty = new String[] { + " ", + "/*comment*/", + "/**/", + "//", + }; + + static final String[] prefix = new String[] { + "/**comment*/", + "/***/", + "///comment", + }; + static final String[] unknown = new String[] { "new ;", "\"", @@ -259,6 +272,7 @@ public class CompletenessTest extends KullaTesting { break; case EMPTY: + case PREFIX: case COMPLETE: case UNKNOWN: augSrc = source; @@ -301,6 +315,16 @@ public class CompletenessTest extends KullaTesting { assertStatus(definitely_incomplete, DEFINITELY_INCOMPLETE); } + @Test + public void test_prefix() { + assertStatus(prefix, PREFIX); + } + + @Test + public void test_empty() { + assertStatus(empty, EMPTY); + } + @Test public void test_unknown() { assertStatus(definitely_incomplete, DEFINITELY_INCOMPLETE); diff --git a/test/langtools/jdk/jshell/CompletionAPITest.java b/test/langtools/jdk/jshell/CompletionAPITest.java index 932482ce6dc..f5bc205c208 100644 --- a/test/langtools/jdk/jshell/CompletionAPITest.java +++ b/test/langtools/jdk/jshell/CompletionAPITest.java @@ -49,6 +49,7 @@ 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.Consumer; import java.util.function.Supplier; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; @@ -59,6 +60,7 @@ 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.JShell; import jdk.jshell.SourceCodeAnalysis.CompletionContext; import jdk.jshell.SourceCodeAnalysis.CompletionState; import jdk.jshell.SourceCodeAnalysis.ElementSuggestion; @@ -266,8 +268,6 @@ public class CompletionAPITest extends KullaTesting { "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()); @@ -277,18 +277,15 @@ public class CompletionAPITest extends KullaTesting { 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(); + private final Path srcZip = Paths.get("src.zip"); + + public void setUp(Consumer bc) { + super.setUp(bc.andThen(b -> b.binarySourceMapping(p -> compiler.getClassDir().equals(p) ? List.of(srcZip) : null))); + } static { try { diff --git a/test/langtools/jdk/jshell/CompletionSuggestionTest.java b/test/langtools/jdk/jshell/CompletionSuggestionTest.java index 7a960258cc1..44f21c4675e 100644 --- a/test/langtools/jdk/jshell/CompletionSuggestionTest.java +++ b/test/langtools/jdk/jshell/CompletionSuggestionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, 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 @@ -782,13 +782,13 @@ public class CompletionSuggestionTest extends KullaTesting { throw new IllegalStateException(ex); } - try { - Field availableSources = getAnalysis().getClass().getDeclaredField("availableSources"); - availableSources.setAccessible(true); - availableSources.set(getAnalysis(), Arrays.asList(srcZip)); - } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException ex) { - throw new IllegalStateException(ex); - } + setJDKSourcesOverride(List.of(srcZip)); + } + + @Override + public void tearDown() { + setJDKSourcesOverride(null); + super.tearDown(); } private void dontReadParameterNamesFromClassFile() throws Exception { @@ -953,6 +953,17 @@ public class CompletionSuggestionTest extends KullaTesting { assertSignature("void f() { } f(|", "void f()"); } + private static void setJDKSourcesOverride(List paths) throws IllegalStateException { + try { + //to ensure test stability, don't use JDK's src.zip: + Field availableSources = Class.forName("jdk.jshell.SourceCodeAnalysisImpl").getDeclaredField("jdkSourcesOverride"); + availableSources.setAccessible(true); + availableSources.set(null, paths); + } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | ClassNotFoundException ex) { + throw new IllegalStateException(ex); + } + } + static { try { //disable reading of paramater names, to improve stability: diff --git a/test/langtools/jdk/jshell/JavadocTest.java b/test/langtools/jdk/jshell/JavadocTest.java index 463c23a4f31..3f25d1f11c5 100644 --- a/test/langtools/jdk/jshell/JavadocTest.java +++ b/test/langtools/jdk/jshell/JavadocTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, 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 @@ -39,9 +39,10 @@ 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.List; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; +import jdk.jshell.Snippet.Status; import org.junit.jupiter.api.Test; @@ -98,13 +99,7 @@ public class JavadocTest extends KullaTesting { compiler.compile(clazz); - try { - Field availableSources = getAnalysis().getClass().getDeclaredField("availableSources"); - availableSources.setAccessible(true); - availableSources.set(getAnalysis(), Arrays.asList(srcZip)); - } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException ex) { - throw new IllegalStateException(ex); - } + setJDKSourcesOverride(List.of(srcZip)); addToClasspath(compiler.getClassDir()); } @@ -147,13 +142,169 @@ public class JavadocTest extends KullaTesting { throw new IllegalStateException(ex); } + setJDKSourcesOverride(List.of(srcZip)); + } + + @Test + public void testJavadocFromSnippets() { + assertEval(""" + /**SnippetClazz top level.*/ + class SnippetClazz { + /**Method javadoc.*/ + public static void test() {} + /**SnippetClazzNested nested.*/ + static class SnippetClazzNested { + /**Nested method javadoc.*/ + public static void nestedMethod() {} + } + } + """); + assertEval(""" + /**topLevel method.*/ + void topLevel(SnippetClazz clazz) {} + """); + assertEval(""" + /**topLevel variable.*/ + int variable = 0; + """); + assertJavadoc("SnippetClazz|", + """ + SnippetClazz + SnippetClazz top level."""); + assertJavadoc("SnippetClazz.SnippetClazzNested|", + """ + SnippetClazz.SnippetClazzNested + SnippetClazzNested nested."""); + assertJavadoc("SnippetClazz.test(|", + """ + void SnippetClazz.test() + Method javadoc."""); + assertJavadoc("SnippetClazz.SnippetClazzNested.nestedMethod(|", + """ + void SnippetClazz.SnippetClazzNested.nestedMethod() + Nested method javadoc."""); + assertJavadoc("topLevel(|", + """ + void topLevel(SnippetClazz clazz) + topLevel method."""); + assertJavadoc("variable|", + """ + variable:int + topLevel variable."""); + } + + @Test + public void testJavadocFromSnippetsMarkdown() { + assertEval(""" + ///SnippetClazz top level. + class SnippetClazz { + ///Method javadoc. + public static void test() {} + ///SnippetClazzNested nested. + static class SnippetClazzNested { + ///Nested method javadoc. + public static void nestedMethod() {} + } + } + """); + assertEval(""" + ///topLevel method. + public static void topLevel(SnippetClazz clazz) {} + """); + assertEval(""" + ///topLevel variable. + int variable = 0; + """); + assertJavadoc("SnippetClazz|", + """ + SnippetClazz + SnippetClazz top level."""); + assertJavadoc("SnippetClazz.SnippetClazzNested|", + """ + SnippetClazz.SnippetClazzNested + SnippetClazzNested nested."""); + assertJavadoc("SnippetClazz.test(|", + """ + void SnippetClazz.test() + Method javadoc."""); + assertJavadoc("SnippetClazz.SnippetClazzNested.nestedMethod(|", + """ + void SnippetClazz.SnippetClazzNested.nestedMethod() + Nested method javadoc."""); + assertJavadoc("topLevel(|", + """ + void topLevel(SnippetClazz clazz) + topLevel method."""); + assertJavadoc("variable|", + """ + variable:int + topLevel variable."""); + } + + @Test + public void testOverrideSnippet() { + var originalClass = + classKey(assertEval(""" + ///bad javadoc + class SnippetClazz { + } + """)); + assertEval(""" + ///SnippetClazz top level. + class SnippetClazz implements java.io.Serializable { + } + """, + ste(MAIN_SNIPPET, Status.VALID, Status.VALID, true, null), + ste(originalClass, Status.VALID, Status.OVERWRITTEN, false, MAIN_SNIPPET)); + var originalMethod= + methodKey(assertEval(""" + ///bad javadoc + public static void topLevel(SnippetClazz clazz) {} + """)); + assertEval(""" + ///topLevel method. + public static String topLevel(SnippetClazz clazz) { return ""; } + """, + ste(MAIN_SNIPPET, Status.VALID, Status.VALID, true, null), + ste(originalMethod, Status.VALID, Status.OVERWRITTEN, false, MAIN_SNIPPET)); + var originalVar = + varKey(assertEval(""" + ///bad javadoc + int variable = 0; + """)); + assertEval(""" + ///topLevel variable. + String variable = ""; + """, + ste(MAIN_SNIPPET, Status.VALID, Status.VALID, true, null), + ste(originalVar, Status.VALID, Status.OVERWRITTEN, false, MAIN_SNIPPET)); + assertJavadoc("SnippetClazz|", + """ + SnippetClazz + SnippetClazz top level."""); + assertJavadoc("topLevel(|", + """ + String topLevel(SnippetClazz clazz) + topLevel method."""); + assertJavadoc("variable|", + """ + variable:java.lang.String + topLevel variable."""); + } + + @Override + public void tearDown() { + setJDKSourcesOverride(null); + super.tearDown(); + } + + private void setJDKSourcesOverride(List override) { try { - Field availableSources = getAnalysis().getClass().getDeclaredField("availableSources"); + Field availableSources = getAnalysis().getClass().getDeclaredField("jdkSourcesOverride"); availableSources.setAccessible(true); - availableSources.set(getAnalysis(), Arrays.asList(srcZip)); + availableSources.set(getAnalysis(), override); } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException ex) { throw new IllegalStateException(ex); } } - } diff --git a/test/langtools/jdk/jshell/KullaTesting.java b/test/langtools/jdk/jshell/KullaTesting.java index 8b62ce69a4c..c9e531cd84c 100644 --- a/test/langtools/jdk/jshell/KullaTesting.java +++ b/test/langtools/jdk/jshell/KullaTesting.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, 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 @@ -210,8 +210,11 @@ public class KullaTesting { analysis = null; allSnippets = null; idToSnippet = null; + tearDownDone(); } + protected void tearDownDone() {} + public ClassLoader createAndRunFromModule(String moduleName, Path modPath) { ModuleFinder finder = ModuleFinder.of(modPath); ModuleLayer parent = ModuleLayer.boot(); diff --git a/test/langtools/jdk/jshell/ToolSimpleTest.java b/test/langtools/jdk/jshell/ToolSimpleTest.java index 39ce248a521..dc792137663 100644 --- a/test/langtools/jdk/jshell/ToolSimpleTest.java +++ b/test/langtools/jdk/jshell/ToolSimpleTest.java @@ -73,6 +73,8 @@ public class ToolSimpleTest extends ReplToolTesting { test( (a) -> assertCommand(a, "int z = /* blah", ""), (a) -> assertCommand(a, "baz */ 5", "z ==> 5"), + (a) -> assertCommand(a, "/* hoge ", ""), + (a) -> assertCommand(a, "baz **/", ""), (a) -> assertCommand(a, "/** hoge ", ""), (a) -> assertCommand(a, "baz **/", ""), (a) -> assertCommand(a, "int v", "v ==> 0") diff --git a/test/langtools/jdk/jshell/ToolTabSnippetTest.java b/test/langtools/jdk/jshell/ToolTabSnippetTest.java index 112208c40b3..3eb709f52f4 100644 --- a/test/langtools/jdk/jshell/ToolTabSnippetTest.java +++ b/test/langtools/jdk/jshell/ToolTabSnippetTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2026, 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 @@ -38,17 +38,22 @@ */ import java.io.IOException; +import java.io.InputStream; 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.List; import java.util.concurrent.CountDownLatch; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import jdk.internal.jshell.tool.ConsoleIOContextTestSupport; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class ToolTabSnippetTest extends UITesting { @@ -56,9 +61,10 @@ public class ToolTabSnippetTest extends UITesting { super(true); } - @Test - public void testExpression() throws Exception { - Path classes = prepareZip(); + @ParameterizedTest + @ValueSource(booleans={false, true}) + public void testExpression(boolean createCombinedJar) throws Exception { + Path classes = prepareZip(createCombinedJar); doRunTest((inputSink, out) -> { inputSink.write("/env -class-path " + classes.toString() + "\n"); waitOutput(out, resource("jshell.msg.set.restore") + "\n\\u001B\\[\\?2004h" + PROMPT); @@ -278,7 +284,7 @@ public class ToolTabSnippetTest extends UITesting { }); } - private Path prepareZip() { + private Path prepareZip(boolean createCombinedJar) { String clazz1 = "package jshelltest;\n" + "/**JShellTest 0" + @@ -308,28 +314,80 @@ public class ToolTabSnippetTest extends UITesting { " public JShellTestAux(String str, int i) { }\n" + "}\n"; - Path srcZip = Paths.get("src.zip"); + if (createCombinedJar) { + Path combinedJar = Paths.get("combined.jar"); - try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(srcZip))) { - out.putNextEntry(new JarEntry("jshelltest/JShellTest.java")); - out.write(clazz1.getBytes()); - out.putNextEntry(new JarEntry("jshelltest/JShellTestAux.java")); - out.write(clazz2.getBytes()); - } catch (IOException ex) { - throw new IllegalStateException(ex); + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(combinedJar))) { + out.putNextEntry(new JarEntry("jshelltest/JShellTest.java")); + out.write(clazz1.getBytes()); + out.putNextEntry(new JarEntry("jshelltest/JShellTestAux.java")); + out.write(clazz2.getBytes()); + + compiler.compile(clazz1, clazz2); + + for (String clazz : new String[] {"jshelltest/JShellTest.class", "jshelltest/JShellTestAux.class"}) { + out.putNextEntry(new JarEntry(clazz)); + + try (InputStream in = Files.newInputStream(compiler.getClassDir().resolve(clazz))) { + in.transferTo(out); + } + } + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + + //create a broken combined-sources.jar, which should not be used: + Path srcZip = Paths.get("combined-sources.jar"); + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(srcZip))) { + out.putNextEntry(new JarEntry("jshelltest/JShellTest.java")); + out.write(""" + package jshelltest; + /** wrong */ + public class JShellTest { + /** wrong + */ + public JShellTest(String str) {} + /**JShellTest 2 */ + public JShellTest(String str, int i) {} + } + """.getBytes()); + + out.putNextEntry(new JarEntry("jshelltest/JShellTestAux.java")); + out.write(""" + package jshelltest; + /** wrong */ + public class JShellTestAux { + /** wrong */ + public JShellTestAux(String str) { } + /** wrong */ + public JShellTestAux(String str, int i) { } + } + """.getBytes()); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + + return combinedJar; + } else { + Path srcZip = Paths.get("test-sources.jar"); + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(srcZip))) { + out.putNextEntry(new JarEntry("jshelltest/JShellTest.java")); + out.write(clazz1.getBytes()); + out.putNextEntry(new JarEntry("jshelltest/JShellTestAux.java")); + out.write(clazz2.getBytes()); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + + compiler.compile(clazz1, clazz2); + + Path binaryJar = Paths.get("test.jar"); + compiler.jar(compiler.getClassDir(), binaryJar, "jshelltest/JShellTest.class", "jshelltest/JShellTestAux.class"); + + return binaryJar; } - - compiler.compile(clazz1, clazz2); - - 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(); @@ -364,4 +422,25 @@ public class ToolTabSnippetTest extends UITesting { waitOutput(out, "CL\\u001B\\[2Djava.lang.annotation.RetentionPolicy.CLASS \\u0008"); }); } + + @BeforeAll + public static void noJDKSources() { + setJDKSourcesOverride(List.of()); + } + + @AfterAll + public static void restoreJDKSources() { + setJDKSourcesOverride(null); + } + + private static void setJDKSourcesOverride(List paths) throws IllegalStateException { + try { + //to ensure test stability, don't use JDK's src.zip: + Field availableSources = Class.forName("jdk.jshell.SourceCodeAnalysisImpl").getDeclaredField("jdkSourcesOverride"); + availableSources.setAccessible(true); + availableSources.set(null, paths); + } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | ClassNotFoundException ex) { + throw new IllegalStateException(ex); + } + } }