summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Ruby <rubys@intertwingly.net>2018-07-13 15:46:21 -0700
committerRich Trott <rtrott@gmail.com>2018-08-03 20:20:00 -0700
commita2ec80851ceff8ba6745d6909c8a2434ddfdf568 (patch)
tree4ee42b57302d9f3f78a1753b76521afc64c03da7
parentf19fa7ca4deea517f66cb0ef8b5fd9574bb33a66 (diff)
downloadandroid-node-v8-a2ec80851ceff8ba6745d6909c8a2434ddfdf568.tar.gz
android-node-v8-a2ec80851ceff8ba6745d6909c8a2434ddfdf568.tar.bz2
android-node-v8-a2ec80851ceff8ba6745d6909c8a2434ddfdf568.zip
repl: support mult-line string-keyed objects
isRecoverableError is completely reimplemented using acorn and an acorn plugin that examines the state of the parser at the time of the error to determine if the code could be completed on a subsequent line. PR-URL: https://github.com/nodejs/node/pull/21805 Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com> Reviewed-By: John-David Dalton <john.david.dalton@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
-rw-r--r--lib/internal/repl/recoverable.js79
-rw-r--r--lib/repl.js74
-rw-r--r--node.gyp1
-rw-r--r--test/parallel/test-repl.js21
4 files changed, 97 insertions, 78 deletions
diff --git a/lib/internal/repl/recoverable.js b/lib/internal/repl/recoverable.js
new file mode 100644
index 0000000000..465d77451a
--- /dev/null
+++ b/lib/internal/repl/recoverable.js
@@ -0,0 +1,79 @@
+'use strict';
+
+const acorn = require('internal/deps/acorn/dist/acorn');
+const { tokTypes: tt } = acorn;
+
+// If the error is that we've unexpectedly ended the input,
+// then let the user try to recover by adding more input.
+// Note: `e` (the original exception) is not used by the current implemention,
+// but may be needed in the future.
+function isRecoverableError(e, code) {
+ let recoverable = false;
+
+ // Determine if the point of the any error raised is at the end of the input.
+ // There are two cases to consider:
+ //
+ // 1. Any error raised after we have encountered the 'eof' token.
+ // This prevents us from declaring partial tokens (like '2e') as
+ // recoverable.
+ //
+ // 2. Three cases where tokens can legally span lines. This is
+ // template, comment, and strings with a backslash at the end of
+ // the line, indicating a continuation. Note that we need to look
+ // for the specific errors of 'unterminated' kind (not, for example,
+ // a syntax error in a ${} expression in a template), and the only
+ // way to do that currently is to look at the message. Should Acorn
+ // change these messages in the future, this will lead to a test
+ // failure, indicating that this code needs to be updated.
+ //
+ acorn.plugins.replRecoverable = (parser) => {
+ parser.extend('nextToken', (nextToken) => {
+ return function() {
+ Reflect.apply(nextToken, this, []);
+
+ if (this.type === tt.eof) recoverable = true;
+ };
+ });
+
+ parser.extend('raise', (raise) => {
+ return function(pos, message) {
+ switch (message) {
+ case 'Unterminated template':
+ case 'Unterminated comment':
+ recoverable = true;
+ break;
+
+ case 'Unterminated string constant':
+ const token = this.input.slice(this.lastTokStart, this.pos);
+ // see https://www.ecma-international.org/ecma-262/#sec-line-terminators
+ recoverable = /\\(?:\r\n?|\n|\u2028|\u2029)$/.test(token);
+ }
+
+ Reflect.apply(raise, this, [pos, message]);
+ };
+ });
+ };
+
+ // For similar reasons as `defaultEval`, wrap expressions starting with a
+ // curly brace with parenthesis. Note: only the open parenthesis is added
+ // here as the point is to test for potentially valid but incomplete
+ // expressions.
+ if (/^\s*\{/.test(code) && isRecoverableError(e, `(${code}`)) return true;
+
+ // Try to parse the code with acorn. If the parse fails, ignore the acorn
+ // error and return the recoverable status.
+ try {
+ acorn.parse(code, { plugins: { replRecoverable: true } });
+
+ // Odd case: the underlying JS engine (V8, Chakra) rejected this input
+ // but Acorn detected no issue. Presume that additional text won't
+ // address this issue.
+ return false;
+ } catch {
+ return recoverable;
+ }
+}
+
+module.exports = {
+ isRecoverableError
+};
diff --git a/lib/repl.js b/lib/repl.js
index 92c90de7bb..4a01595ce1 100644
--- a/lib/repl.js
+++ b/lib/repl.js
@@ -73,6 +73,7 @@ const {
} = require('internal/errors').codes;
const { sendInspectorCommand } = require('internal/util/inspector');
const { experimentalREPLAwait } = process.binding('config');
+const { isRecoverableError } = require('internal/repl/recoverable');
// Lazy-loaded.
let processTopLevelAwait;
@@ -227,7 +228,8 @@ function REPLServer(prompt,
// It's confusing for `{ a : 1 }` to be interpreted as a block
// statement rather than an object literal. So, we first try
// to wrap it in parentheses, so that it will be interpreted as
- // an expression.
+ // an expression. Note that if the above condition changes,
+ // lib/internal/repl/recoverable.js needs to be changed to match.
code = `(${code.trim()})\n`;
wrappedCmd = true;
}
@@ -1505,76 +1507,6 @@ function regexpEscape(s) {
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
-// If the error is that we've unexpectedly ended the input,
-// then let the user try to recover by adding more input.
-function isRecoverableError(e, code) {
- if (e && e.name === 'SyntaxError') {
- var message = e.message;
- if (message === 'Unterminated template literal' ||
- message === 'Unexpected end of input') {
- return true;
- }
-
- if (message === 'missing ) after argument list') {
- const frames = e.stack.split(/\r?\n/);
- const pos = frames.findIndex((f) => f.match(/^\s*\^+$/));
- return pos > 0 && frames[pos - 1].length === frames[pos].length;
- }
-
- if (message === 'Invalid or unexpected token')
- return isCodeRecoverable(code);
- }
- return false;
-}
-
-// Check whether a code snippet should be forced to fail in the REPL.
-function isCodeRecoverable(code) {
- var current, previous, stringLiteral;
- var isBlockComment = false;
- var isSingleComment = false;
- var isRegExpLiteral = false;
- var lastChar = code.charAt(code.length - 2);
- var prevTokenChar = null;
-
- for (var i = 0; i < code.length; i++) {
- previous = current;
- current = code[i];
-
- if (previous === '\\' && (stringLiteral || isRegExpLiteral)) {
- current = null;
- } else if (stringLiteral) {
- if (stringLiteral === current) {
- stringLiteral = null;
- }
- } else if (isRegExpLiteral && current === '/') {
- isRegExpLiteral = false;
- } else if (isBlockComment && previous === '*' && current === '/') {
- isBlockComment = false;
- } else if (isSingleComment && current === '\n') {
- isSingleComment = false;
- } else if (!isBlockComment && !isRegExpLiteral && !isSingleComment) {
- if (current === '/' && previous === '/') {
- isSingleComment = true;
- } else if (previous === '/') {
- if (current === '*') {
- isBlockComment = true;
- // Distinguish between a division operator and the start of a regex
- // by examining the non-whitespace character that precedes the /
- } else if ([null, '(', '[', '{', '}', ';'].includes(prevTokenChar)) {
- isRegExpLiteral = true;
- }
- } else {
- if (current.trim()) prevTokenChar = current;
- if (current === '\'' || current === '"') {
- stringLiteral = current;
- }
- }
- }
- }
-
- return stringLiteral ? lastChar === '\\' : isBlockComment;
-}
-
function Recoverable(err) {
this.err = err;
}
diff --git a/node.gyp b/node.gyp
index 7460d73c0b..682111051c 100644
--- a/node.gyp
+++ b/node.gyp
@@ -147,6 +147,7 @@
'lib/internal/readline.js',
'lib/internal/repl.js',
'lib/internal/repl/await.js',
+ 'lib/internal/repl/recoverable.js',
'lib/internal/socket_list.js',
'lib/internal/test/binding.js',
'lib/internal/test/heap.js',
diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js
index 35cd3e11af..8cb4b686b8 100644
--- a/test/parallel/test-repl.js
+++ b/test/parallel/test-repl.js
@@ -162,13 +162,11 @@ const errorTests = [
// Template expressions
{
send: '`io.js ${"1.0"',
- expect: [
- kSource,
- kArrow,
- '',
- /^SyntaxError: /,
- ''
- ]
+ expect: '... '
+ },
+ {
+ send: '+ ".2"}`',
+ expect: '\'io.js 1.0.2\''
},
{
send: '`io.js ${',
@@ -315,6 +313,15 @@ const errorTests = [
send: '1 }',
expect: '{ a: 1 }'
},
+ // Multiline string-keyed object (e.g. JSON)
+ {
+ send: '{ "a": ',
+ expect: '... '
+ },
+ {
+ send: '1 }',
+ expect: '{ a: 1 }'
+ },
// Multiline anonymous function with comment
{
send: '(function() {',