aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/internal/repl/recoverable.js79
-rw-r--r--lib/repl.js74
2 files changed, 82 insertions, 71 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;
}