8182270: JShell API: Tools need snippet information without evaluating snippet

8166334: jshell tool: shortcut: expression/statement to method

Reviewed-by: jlahoda
This commit is contained in:
Robert Field 2017-08-16 18:42:11 -07:00
parent 1dd6fa2fb6
commit 6573d94b58
12 changed files with 495 additions and 19 deletions

View File

@ -64,6 +64,11 @@ import jdk.internal.jline.internal.NonBlockingInputStream;
import jdk.internal.jshell.tool.StopDetectingInputStream.State;
import jdk.internal.misc.Signal;
import jdk.internal.misc.Signal.Handler;
import jdk.jshell.ExpressionSnippet;
import jdk.jshell.Snippet;
import jdk.jshell.Snippet.SubKind;
import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
import jdk.jshell.VarSnippet;
class ConsoleIOContext extends IOContext {
@ -916,6 +921,111 @@ class ConsoleIOContext extends IOContext {
return new FixResult(fixes, null);
}
},
new FixComputer('m', false) { //compute "Introduce method" Fix:
private void performToMethod(ConsoleReader in, String type, String code) throws IOException {
in.redrawLine();
if (!code.trim().endsWith(";")) {
in.putString(";");
}
in.putString(" }");
in.setCursorPosition(0);
String afterCursor = type.equals("void")
? "() { "
: "() { return ";
in.putString(type + " " + afterCursor);
// position the cursor where the method name should be entered (before parens)
in.setCursorPosition(in.getCursorBuffer().cursor - afterCursor.length());
in.flush();
}
private FixResult reject(JShellTool repl, String messageKey) {
return new FixResult(Collections.emptyList(), repl.messageFormat(messageKey));
}
@Override
public FixResult compute(JShellTool repl, String code, int cursor) {
final String codeToCursor = code.substring(0, cursor);
final String type;
final CompletionInfo ci = repl.analysis.analyzeCompletion(codeToCursor);
if (!ci.remaining().isEmpty()) {
return reject(repl, "jshell.console.exprstmt");
}
switch (ci.completeness()) {
case COMPLETE:
case COMPLETE_WITH_SEMI:
case CONSIDERED_INCOMPLETE:
break;
case EMPTY:
return reject(repl, "jshell.console.empty");
case DEFINITELY_INCOMPLETE:
case UNKNOWN:
default:
return reject(repl, "jshell.console.erroneous");
}
List<Snippet> snl = repl.analysis.sourceToSnippets(ci.source());
if (snl.size() != 1) {
return reject(repl, "jshell.console.erroneous");
}
Snippet sn = snl.get(0);
switch (sn.kind()) {
case EXPRESSION:
type = ((ExpressionSnippet) sn).typeName();
break;
case STATEMENT:
type = "void";
break;
case VAR:
if (sn.subKind() != SubKind.TEMP_VAR_EXPRESSION_SUBKIND) {
// only valid var is an expression turned into a temp var
return reject(repl, "jshell.console.exprstmt");
}
type = ((VarSnippet) sn).typeName();
break;
case IMPORT:
case METHOD:
case TYPE_DECL:
return reject(repl, "jshell.console.exprstmt");
case ERRONEOUS:
default:
return reject(repl, "jshell.console.erroneous");
}
List<Fix> fixes = new ArrayList<>();
fixes.add(new Fix() {
@Override
public String displayName() {
return repl.messageFormat("jshell.console.create.method");
}
@Override
public void perform(ConsoleReader in) throws IOException {
performToMethod(in, type, codeToCursor);
}
});
int idx = type.lastIndexOf(".");
if (idx > 0) {
String stype = type.substring(idx + 1);
QualifiedNames res = repl.analysis.listQualifiedNames(stype, stype.length());
if (res.isUpToDate() && res.getNames().contains(type)
&& !res.isResolvable()) {
fixes.add(new Fix() {
@Override
public String displayName() {
return "import: " + type + ". " +
repl.messageFormat("jshell.console.create.method");
}
@Override
public void perform(ConsoleReader in) throws IOException {
repl.processCompleteSource("import " + type + ";");
in.println("Imported: " + type);
performToMethod(in, stype, codeToCursor);
}
});
}
}
return new FixResult(fixes, null);
}
},
new FixComputer('i', true) { //compute "Add import" Fixes:
@Override
public FixResult compute(JShellTool repl, String code, int cursor) {

View File

@ -174,10 +174,19 @@ jshell.console.do.nothing = Do nothing
jshell.console.choice = Choice: \
jshell.console.create.variable = Create variable
jshell.console.create.method = Create method
jshell.console.resolvable = \nThe identifier is resolvable in this context.
jshell.console.no.candidate = \nNo candidate fully qualified names found to import.
jshell.console.incomplete = \nResults may be incomplete; try again later for complete results.
jshell.console.erroneous = \nIncomplete or erroneous. A single valid expression or statement must proceed Shift-<tab> m.
jshell.console.exprstmt = \nA single valid expression or statement must proceed Shift-<tab> m.
jshell.console.empty = \nEmpty entry. A single valid expression or statement must proceed Shift-<tab> m..
jshell.fix.wrong.shortcut =\
Unexpected character after Shift-Tab.\n\
Use "i" for auto-import, "v" for variable creation, or "m" for method creation.\n\
For more information see:\n\
/help shortcuts
help.usage = \
Usage: jshell <options> <load files>\n\
@ -549,6 +558,11 @@ Shift-<tab> v\n\t\t\
After a complete expression, hold down <shift> while pressing <tab>,\n\t\t\
then release and press "v", the expression will be converted to\n\t\t\
a variable declaration whose type is based on the type of the expression.\n\n\
Shift-<tab> m\n\t\t\
After a complete expression or statement, hold down <shift> while pressing <tab>,\n\t\t\
then release and press "m", the expression or statement will be converted to\n\t\t\
a method declaration. If an expression, the return type is based on the type\n\t\t\
of the expression.\n\n\
Shift-<tab> i\n\t\t\
After an unresolvable identifier, hold down <shift> while pressing <tab>,\n\t\t\
then release and press "i", and jshell will propose possible imports\n\t\t\
@ -1021,7 +1035,3 @@ startup.feedback = \
/set format silent errorpre '| ' \n\
/set format silent errorpost '%n' \n\
/set format silent display '' \n
jshell.fix.wrong.shortcut =\
Unexpected character after Shift-Tab. Use "i" for auto-import or "v" for variable creation. For more information see:\n\
/help shortcuts

View File

@ -91,6 +91,9 @@ class Eval {
private static final Pattern IMPORT_PATTERN = Pattern.compile("import\\p{javaWhitespace}+(?<static>static\\p{javaWhitespace}+)?(?<fullname>[\\p{L}\\p{N}_\\$\\.]+\\.(?<name>[\\p{L}\\p{N}_\\$]+|\\*))");
// for uses that should not change state -- non-evaluations
private boolean preserveState = false;
private int varNumber = 0;
private final JShell state;
@ -142,6 +145,23 @@ class Eval {
return snippets;
}
/**
* Converts the user source of a snippet into a Snippet object (or list of
* objects in the case of: int x, y, z;). Does not install the Snippets
* or execute them. Does not change any state.
*
* @param userSource the source of the snippet
* @return usually a singleton list of Snippet, but may be empty or multiple
*/
List<Snippet> toScratchSnippets(String userSource) {
try {
preserveState = true;
return sourceToSnippets(userSource);
} finally {
preserveState = false;
}
}
/**
* Converts the user source of a snippet into a Snippet object (or list of
* objects in the case of: int x, y, z;). Does not install the Snippets
@ -316,11 +336,15 @@ class Eval {
subkind = SubKind.OTHER_EXPRESSION_SUBKIND;
}
if (shouldGenTempVar(subkind)) {
if (state.tempVariableNameGenerator != null) {
name = state.tempVariableNameGenerator.get();
}
while (name == null || state.keyMap.doesVariableNameExist(name)) {
name = "$" + ++varNumber;
if (preserveState) {
name = "$$";
} else {
if (state.tempVariableNameGenerator != null) {
name = state.tempVariableNameGenerator.get();
}
while (name == null || state.keyMap.doesVariableNameExist(name)) {
name = "$" + ++varNumber;
}
}
guts = Wrap.tempVarWrap(compileSource, typeName, name);
Collection<String> declareReferences = null; //TODO

View File

@ -862,7 +862,7 @@ public class JShell implements AutoCloseable {
* Check if this JShell has been closed
* @throws IllegalStateException if it is closed
*/
private void checkIfAlive() throws IllegalStateException {
void checkIfAlive() throws IllegalStateException {
if (closed) {
throw new IllegalStateException(messageFormat("jshell.exc.closed", this));
}
@ -879,8 +879,8 @@ public class JShell implements AutoCloseable {
if (sn == null) {
throw new NullPointerException(messageFormat("jshell.exc.null"));
} else {
if (sn.key().state() != this) {
throw new IllegalArgumentException(messageFormat("jshell.exc.alien"));
if (sn.key().state() != this || sn.id() == Snippet.UNASSOCIATED_ID) {
throw new IllegalArgumentException(messageFormat("jshell.exc.alien", sn.toString()));
}
return sn;
}

View File

@ -552,6 +552,8 @@ public abstract class Snippet {
}
}
static final String UNASSOCIATED_ID = "*UNASSOCIATED*";
private final Key key;
private final String source;
private final Wrap guts;

View File

@ -132,6 +132,34 @@ public abstract class SourceCodeAnalysis {
*/
public abstract List<SnippetWrapper> wrappers(String input);
/**
* Converts the source code of a snippet into a {@link Snippet} object (or
* list of {@code Snippet} objects in the case of some var declarations,
* e.g.: int x, y, z;).
* Does not install the snippets: declarations are not
* accessible by other snippets; imports are not added.
* Does not execute the snippets.
* <p>
* Queries may be done on the {@code Snippet} object. The {@link Snippet#id()}
* will be {@code "*UNASSOCIATED*"}.
* The returned snippets are not associated with the
* {@link JShell} instance, so attempts to pass them to {@code JShell}
* methods will throw an {@code IllegalArgumentException}.
* They will not appear in queries for snippets --
* for example, {@link JShell#snippets() }.
* <p>
* Restrictions on the input are as in {@link JShell#eval}.
* <p>
* Only preliminary compilation is performed, sufficient to build the
* {@code Snippet}. Snippets known to be erroneous, are returned as
* {@link ErroneousSnippet}, other snippets may or may not be in error.
* <p>
* @param input The input String to convert
* @return usually a singleton list of Snippet, but may be empty or multiple
* @throws IllegalStateException if the {@code JShell} instance is closed.
*/
public abstract List<Snippet> sourceToSnippets(String input);
/**
* Returns a collection of {@code Snippet}s which might need updating if the
* given {@code Snippet} is updated. The returned collection is designed to

View File

@ -532,6 +532,16 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
.collect(toList());
}
@Override
public List<Snippet> sourceToSnippets(String input) {
proc.checkIfAlive();
List<Snippet> snl = proc.eval.toScratchSnippets(input);
for (Snippet sn : snl) {
sn.setId(Snippet.UNASSOCIATED_ID);
}
return snl;
}
@Override
public Collection<Snippet> dependents(Snippet snippet) {
return proc.maps.getDependents(snippet);

View File

@ -29,6 +29,6 @@ jshell.diag.modifier.single.fatal = Modifier {0} not permitted in top-level decl
jshell.diag.modifier.single.ignore = Modifier {0} not permitted in top-level declarations, ignored
jshell.exc.null = Snippet must not be null
jshell.exc.alien = Snippet not from this JShell
jshell.exc.alien = Snippet not from this JShell: {0}
jshell.exc.closed = JShell ({0}) has been closed.
jshell.exc.var.not.valid = Snippet parameter of varValue() {0} must be VALID, it is: {1}

View File

@ -0,0 +1,161 @@
/*
* Copyright (c) 2017, 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 8182270
* @summary test non-eval Snippet analysis
* @build KullaTesting TestingInputStream
* @run testng AnalyzeSnippetTest
*/
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.List;
import jdk.jshell.Snippet;
import jdk.jshell.DeclarationSnippet;
import org.testng.annotations.Test;
import jdk.jshell.JShell;
import jdk.jshell.MethodSnippet;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertEquals;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import jdk.jshell.ErroneousSnippet;
import jdk.jshell.ExpressionSnippet;
import jdk.jshell.ImportSnippet;
import jdk.jshell.Snippet.SubKind;
import jdk.jshell.SourceCodeAnalysis;
import jdk.jshell.StatementSnippet;
import jdk.jshell.TypeDeclSnippet;
import jdk.jshell.VarSnippet;
import static jdk.jshell.Snippet.SubKind.*;
@Test
public class AnalyzeSnippetTest {
JShell state;
SourceCodeAnalysis sca;
@BeforeMethod
public void setUp() {
state = JShell.builder()
.out(new PrintStream(new ByteArrayOutputStream()))
.err(new PrintStream(new ByteArrayOutputStream()))
.build();
sca = state.sourceCodeAnalysis();
}
@AfterMethod
public void tearDown() {
if (state != null) {
state.close();
}
state = null;
sca = null;
}
public void testImport() {
ImportSnippet sn = (ImportSnippet) assertSnippet("import java.util.List;",
SubKind.SINGLE_TYPE_IMPORT_SUBKIND);
assertEquals(sn.name(), "List");
sn = (ImportSnippet) assertSnippet("import static java.nio.file.StandardOpenOption.CREATE;",
SubKind.SINGLE_STATIC_IMPORT_SUBKIND);
assertTrue(sn.isStatic());
}
public void testClass() {
TypeDeclSnippet sn = (TypeDeclSnippet) assertSnippet("class C {}",
SubKind.CLASS_SUBKIND);
assertEquals(sn.name(), "C");
sn = (TypeDeclSnippet) assertSnippet("enum EE {A, B , C}",
SubKind.ENUM_SUBKIND);
}
public void testMethod() {
MethodSnippet sn = (MethodSnippet) assertSnippet("int m(int x) { return x + x; }",
SubKind.METHOD_SUBKIND);
assertEquals(sn.name(), "m");
assertEquals(sn.signature(), "(int)int");
}
public void testVar() {
VarSnippet sn = (VarSnippet) assertSnippet("int i;",
SubKind.VAR_DECLARATION_SUBKIND);
assertEquals(sn.name(), "i");
assertEquals(sn.typeName(), "int");
sn = (VarSnippet) assertSnippet("int jj = 6;",
SubKind.VAR_DECLARATION_WITH_INITIALIZER_SUBKIND);
sn = (VarSnippet) assertSnippet("2 + 2",
SubKind.TEMP_VAR_EXPRESSION_SUBKIND);
}
public void testExpression() {
state.eval("int aa = 10;");
ExpressionSnippet sn = (ExpressionSnippet) assertSnippet("aa",
SubKind.VAR_VALUE_SUBKIND);
assertEquals(sn.name(), "aa");
assertEquals(sn.typeName(), "int");
sn = (ExpressionSnippet) assertSnippet("aa;",
SubKind.VAR_VALUE_SUBKIND);
assertEquals(sn.name(), "aa");
assertEquals(sn.typeName(), "int");
sn = (ExpressionSnippet) assertSnippet("aa = 99",
SubKind.ASSIGNMENT_SUBKIND);
}
public void testStatement() {
StatementSnippet sn = (StatementSnippet) assertSnippet("System.out.println(33)",
SubKind.STATEMENT_SUBKIND);
sn = (StatementSnippet) assertSnippet("if (true) System.out.println(33);",
SubKind.STATEMENT_SUBKIND);
}
public void testErroneous() {
ErroneousSnippet sn = (ErroneousSnippet) assertSnippet("+++",
SubKind.UNKNOWN_SUBKIND);
sn = (ErroneousSnippet) assertSnippet("abc",
SubKind.UNKNOWN_SUBKIND);
}
public void testNoStateChange() {
assertSnippet("int a = 5;", SubKind.VAR_DECLARATION_WITH_INITIALIZER_SUBKIND);
assertSnippet("a", SubKind.UNKNOWN_SUBKIND);
VarSnippet vsn = (VarSnippet) state.eval("int aa = 10;").get(0).snippet();
assertSnippet("++aa;", SubKind.TEMP_VAR_EXPRESSION_SUBKIND);
assertEquals(state.varValue(vsn), "10");
assertSnippet("class CC {}", SubKind.CLASS_SUBKIND);
assertSnippet("new CC();", SubKind.UNKNOWN_SUBKIND);
}
private Snippet assertSnippet(String input, SubKind sk) {
List<Snippet> sns = sca.sourceToSnippets(input);
assertEquals(sns.size(), 1, "snippet count");
Snippet sn = sns.get(0);
assertEquals(sn.id(), "*UNASSOCIATED*");
assertEquals(sn.subKind(), sk);
return sn;
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright (c) 2017, 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 8166334
* @summary test shift-tab shortcuts "fixes"
* @modules
* jdk.jshell/jdk.internal.jshell.tool.resources:open
* jdk.jshell/jdk.jshell:open
* @build UITesting
* @build ToolShiftTabTest
* @run testng/timeout=300 ToolShiftTabTest
*/
import java.util.regex.Pattern;
import org.testng.annotations.Test;
@Test
public class ToolShiftTabTest extends UITesting {
// Shift-tab as escape sequence
private String FIX = "\033\133\132";
public void testFixVariable() throws Exception {
doRunTest((inputSink, out) -> {
inputSink.write("3+4");
inputSink.write(FIX + "v");
inputSink.write("jj\n");
waitOutput(out, "jj ==> 7");
inputSink.write("jj\n");
waitOutput(out, "jj ==> 7");
});
}
public void testFixMethod() throws Exception {
doRunTest((inputSink, out) -> {
inputSink.write("5.5 >= 3.1415926535");
inputSink.write(FIX + "m");
waitOutput(out, "boolean ");
inputSink.write("mm\n");
waitOutput(out, "| created method mm()");
inputSink.write("mm()\n");
waitOutput(out, "==> true");
inputSink.write("/method\n");
waitOutput(out, "boolean mm()");
});
}
public void testFixMethodVoid() throws Exception {
doRunTest((inputSink, out) -> {
inputSink.write("System.out.println(\"Testing\")");
inputSink.write(FIX + "m");
inputSink.write("p\n");
waitOutput(out, "| created method p()");
inputSink.write("p()\n");
waitOutput(out, "Testing");
inputSink.write("/method\n");
waitOutput(out, "void p()");
});
}
public void testFixMethodNoLeaks() throws Exception {
doRunTest((inputSink, out) -> {
inputSink.write("4");
inputSink.write(FIX + "m");
inputSink.write("\u0003 55");
inputSink.write(FIX + "m");
inputSink.write("\u0003 55");
inputSink.write(FIX + "m");
inputSink.write("\u0003 55");
inputSink.write(FIX + "m");
inputSink.write("\u0003 55");
inputSink.write(FIX + "m");
inputSink.write("\u0003'X'");
inputSink.write(FIX + "m");
inputSink.write("nl\n");
waitOutput(out, "| created method nl()");
inputSink.write("/list\n");
waitOutput(out, Pattern.quote("1 : char nl() { return 'X'; }"));
inputSink.write("true\n");
waitOutput(out, Pattern.quote("$2 ==> true"));
inputSink.write("/list\n");
waitOutput(out, "2 : true");
});
}
public void testFixImport() throws Exception {
doRunTest((inputSink, out) -> {
inputSink.write("Frame");
inputSink.write(FIX + "i");
inputSink.write("1");
inputSink.write(".WIDTH\n");
waitOutput(out, "==> 1");
inputSink.write("/import\n");
waitOutput(out, "| import java.awt.Frame");
inputSink.write("Object");
inputSink.write(FIX + "i");
waitOutput(out, "The identifier is resolvable in this context");
});
}
public void testFixBad() throws Exception {
doRunTest((inputSink, out) -> {
inputSink.write("123");
inputSink.write(FIX + "z");
waitOutput(out, "Unexpected character after Shift-Tab");
});
}
}

View File

@ -32,8 +32,8 @@
* @library /tools/lib
* @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask
* @build Compiler UITesting
* @build MergedTabShiftTabCommandTest
* @run testng MergedTabShiftTabCommandTest
* @build ToolTabCommandTest
* @run testng ToolTabCommandTest
*/
import java.util.regex.Pattern;
@ -41,7 +41,7 @@ import java.util.regex.Pattern;
import org.testng.annotations.Test;
@Test
public class MergedTabShiftTabCommandTest extends UITesting {
public class ToolTabCommandTest extends UITesting {
public void testCommand() throws Exception {
// set terminal height so that help output won't hit page breaks

View File

@ -32,8 +32,8 @@
* @library /tools/lib
* @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask
* @build Compiler UITesting
* @build MergedTabShiftTabExpressionTest
* @run testng/timeout=300 MergedTabShiftTabExpressionTest
* @build ToolTabSnippetTest
* @run testng/timeout=300 ToolTabSnippetTest
*/
import java.io.IOException;
@ -49,7 +49,7 @@ import java.util.regex.Pattern;
import org.testng.annotations.Test;
@Test
public class MergedTabShiftTabExpressionTest extends UITesting {
public class ToolTabSnippetTest extends UITesting {
public void testExpression() throws Exception {
Path classes = prepareZip();