8186876: JShell: access javadoc for user/library classes

Co-authored-by: Alex Buckley <abuckley@openjdk.org>
Reviewed-by: vromero, asotona
This commit is contained in:
Jan Lahoda 2026-07-20 13:02:39 +00:00
parent 3623246047
commit 99255c44eb
20 changed files with 1069 additions and 239 deletions

View File

@ -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");

View File

@ -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);
}

View File

@ -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<? extends Path> sourceLocations) {
public static JavadocHelper create(JavacTask mainTask, Collection<? extends Path> sourceLocations,
Function<String, String> 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<? extends Path> 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<? extends Path> 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<? extends VariableElement> 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<String, Pair<JavacTask, TreePath>> signature2Source = new HashMap<>();
private final Collection<? extends Path> sourceLocations;
private final Function<String, String> extraBinaryName2FullSource;
private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm, Collection<? extends Path> sourceLocations) {
private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm,
Function<String, String> 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<? extends Path> 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<? extends VariableElement> 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<JavaFileObject> jfos = Arrays.asList(jfo);
JavaFileManager patchFM = moduleName != null

View File

@ -200,10 +200,11 @@ class Eval {
* @return usually a singleton list of Snippet, but may be empty or multiple
*/
private List<Snippet> 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<? extends Tree> 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<String> 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<Snippet> 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<Snippet> 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);

View File

@ -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<String> extraRemoteVMOptions;
final List<String> extraCompilerOptions;
final Function<StandardJavaFileManager, StandardJavaFileManager> fileManagerMapping;
final Function<Path, Collection<? extends Path>> 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<String,String> executionControlParameters;
String executionControlSpec;
Function<StandardJavaFileManager, StandardJavaFileManager> fileManagerMapping;
Function<Path, Collection<? extends Path>> 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.
*
* <p>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.
*
* <p>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.
*
* <p>The results of calling {@code binarySourceMapping} may be cached, and the
* same file may or may not be queried again.
*
* <p>The result of calling the {@code binarySourceMapping} may be {@code null},
* which will be treated the same way as an empty collection.
*
* <p>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<Path, Collection<? extends Path>> 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

View File

@ -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<String> ignoredModifiers) {
@ -84,10 +87,15 @@ class MaskCommentsAndModifiers {
}
MaskCommentsAndModifiers(String s, boolean maskModifiers, Set<String> ignoredModifiers) {
this(s, maskModifiers, ignoredModifiers, true);
}
MaskCommentsAndModifiers(String s, boolean maskModifiers, Set<String> 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 '@':

View File

@ -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.

View File

@ -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<String, String> binaryName2SnipperOuterWrap;
private final List<AutoCloseable> closeables = new ArrayList<>();
private final Map<Path, ClassIndex> 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 <Suggestion> List<Suggestion> computeSuggestions(OuterWrap code, String inputCode, int cursor, String prefix, ElementSuggestionConvertor<Suggestion> suggestionConvertor) {
return proc.taskFactory.analyze(code, COMPLETION_EXTRA_PARAMETERS, at -> {
try (JavadocHelper javadoc = JavadocHelper.create(at.task, findSources())) {
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<Element, Iterable<? extends Element>> IDENTITY = Collections::singletonList;
private void addElements(JavadocHelper javadoc, Iterable<? extends Element> elements, Predicate<Element> accept, Predicate<Element> smart, int anchor, String prefix, List<ElementSuggestion> result) {
private void addElements(JavadocConfig javadoc, Iterable<? extends Element> elements, Predicate<Element> accept, Predicate<Element> smart, int anchor, String prefix, List<ElementSuggestion> result) {
for (Element c : elements) {
if (!accept.test(c) || !simpleContinuationName(c).startsWith(prefix))
continue;
@ -1237,10 +1245,11 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
continue;
}
StoredElement stored = javadoc.getHandle(c);
Collection<? extends Path> sourceLocations = javadoc.getSourceLocations();
Collection<? extends Path> 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<ElementSuggestion> 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<Element, Iterable<? extends Element>> elementConvertor, Predicate<Element> filter, Predicate<Element> smartFilter, int anchor, String prefix, List<ElementSuggestion> result) {
private void addScopeElements(AnalyzeTask at, JavadocConfig javadoc, Scope scope, Function<Element, Iterable<? extends Element>> elementConvertor, Predicate<Element> filter, Predicate<Element> smartFilter, int anchor, String prefix, List<ElementSuggestion> result) {
addElements(javadoc, scopeContent(at, scope, elementConvertor), filter, smartFilter, anchor, prefix, result);
}
@ -1903,7 +1914,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
List<Documentation> 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<AutoCloseable> 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<Path> availableSourcesOverride; //for tests
private List<Path> availableSources;
private CloseableSources findSources(Collection<? extends Path> forPaths) {
List<AutoCloseable> toClose = new ArrayList<>();
private List<Path> findSources() {
if (availableSources != null) {
return availableSources;
try {
List<Path> sourcePaths = new ArrayList<>();
sourcePaths.addAll(jdkSources());
if (proc.binarySourceMapping != null) {
for (Path binaryPath : forPaths) {
Iterable<? extends Path> 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<Path> jdkSourcesOverride; //for tests
private List<Path> jdkSources;
private synchronized List<Path> jdkSources() {
if (jdkSources != null) {
return jdkSources;
}
List<Path> 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<Path> 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<Path> sources, List<AutoCloseable> 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<Path> paths = proc.taskFactory.parse("", task -> {
MemoryFileManager fm = proc.taskFactory.fileManager();
Collection<Path> _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<Path> paths = getAllPaths();
Map<Path, ClassIndex> newIndexes = new HashMap<>();
@ -2323,6 +2361,50 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
}
}
private Collection<Path> allPaths;
private Collection<Path> getAllPaths() {
int originalClasspathVersion;
synchronized (currentIndexes) {
if (allPaths != null) {
return allPaths;
}
originalClasspathVersion = classpathVersion;
}
Collection<Path> paths = proc.taskFactory.parse("", task -> {
MemoryFileManager fm = proc.taskFactory.fileManager();
Collection<Path> _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<Path> paths) {
Iterable<? extends Path> 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<? extends Path> binaryPaths;
public JavadocConfig(JavacTask mainTask, Collection<? extends Path> 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<? extends Path> getBinaryPaths() {
if (binaryPaths == null) {
throw new IllegalStateException("Already closed.");
}
return binaryPaths;
}
@Override
public void close() {
mainTask = null;
binaryPaths = null;
}
}
}

View File

@ -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<Object> 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));
}
}

View File

@ -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 `<directory>/lib.jar`
does not contain the source, then `<directory>/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

View File

@ -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<ExportsDirective> 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<? extends Element> content =
ed.getPackage().getEnclosedElements();
for (TypeElement clazz : ElementFilter.typesIn(content)) {

View File

@ -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);
}
}

View File

@ -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<Supplier<String>> 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<JShell.Builder> 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<Path> 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);
}
}
}

View File

@ -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);

View File

@ -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<JShell.Builder> bc) {
super.setUp(bc.andThen(b -> b.binarySourceMapping(p -> compiler.getClassDir().equals(p) ? List.of(srcZip) : null)));
}
static {
try {

View File

@ -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<Path> 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:

View File

@ -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<Path> 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);
}
}
}

View File

@ -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();

View File

@ -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")

View File

@ -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<Path> 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);
}
}
}