mirror of
https://github.com/openjdk/jdk.git
synced 2026-07-23 01:18:58 +00:00
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:
parent
3623246047
commit
99255c44eb
@ -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");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 '@':
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
310
test/langtools/jdk/jshell/BinaryToSourceCodeMappingTest.java
Normal file
310
test/langtools/jdk/jshell/BinaryToSourceCodeMappingTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user