repl: Simplify paren wrap, continuation-detection
authorisaacs <i@izs.me>
Thu, 29 Aug 2013 21:48:24 +0000 (14:48 -0700)
committerisaacs <i@izs.me>
Wed, 4 Sep 2013 18:13:41 +0000 (11:13 -0700)
This simplifies the logic that was in isSyntaxError, as well as the
choice to wrap command input in parens to coerce to an expression
statement.

1. Rather than a growing blacklist of allowed-to-throw syntax errors,
just sniff for the one we really care about ("Unexpected end of input")
and let all the others pass through.

2. Wrapping {a:1} in parens makes sense, because blocks and line labels
are silly and confusing and should not be in JavaScript at all.
However, wrapping functions and other types of programs in parens is
weird and required yet *more* hacking to work around.  By only wrapping
statements that start with { and end with }, we can handle the confusing
use-case, without having to then do extra work for functions and other
cases.

This also fixes the repl wart where `console.log)(` works in the repl,
but only by virtue of the fact that it's wrapped in parens first, as
well as potential side effects of double-running the commands, such as:

    > x = 1
    1
    > eval('x++; throw new SyntaxError("e")')
    ... ^C
    > x
    3

lib/repl.js

index e7c1ae9..151d6ca 100644 (file)
@@ -50,6 +50,7 @@ var rl = require('readline');
 var Console = require('console').Console;
 var EventEmitter = require('events').EventEmitter;
 var domain = require('domain');
+var debug = util.debuglog('repl');
 
 // If obj.hasOwnProperty has been overridden, then calling
 // obj.hasOwnProperty(prop) will break.
@@ -119,8 +120,9 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
       });
     } catch (e) {
       err = e;
-      err._isSyntaxError = isSyntaxError(err);
+      debug('parse error %j', code, e);
     }
+
     if (!err) {
       try {
         if (self.useGlobal) {
@@ -130,21 +132,22 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
         }
       } catch (e) {
         err = e;
-        err._isSyntaxError = false;
+        if (err && process.domain) {
+          debug('not recoverable, send to domain');
+          process.domain.emit('error', err);
+          process.domain.exit();
+          return;
+        }
       }
     }
-    if (err && process.domain && !err._isSyntaxError) {
-      process.domain.emit('error', err);
-      process.domain.exit();
-    }
-    else {
-      cb(err, result);
-    }
+
+    cb(err, result);
   }
 
   self.eval = self._domain.bind(eval_);
 
   self._domain.on('error', function(e) {
+    debug('domain error');
     self.outputStream.write((e.stack || e) + '\n');
     self.bufferedCommand = '';
     self.lines.level = [];
@@ -236,6 +239,7 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
   });
 
   rli.on('line', function(cmd) {
+    debug('line %j', cmd);
     sawSIGINT = false;
     var skipCatchall = false;
     cmd = trimWhitespace(cmd);
@@ -255,60 +259,52 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
     }
 
     if (!skipCatchall) {
-      var evalCmd = self.bufferedCommand + cmd + '\n';
-
-      // This try is for determining if the command is complete, or should
-      // continue onto the next line.
-      // We try to evaluate both expressions e.g.
-      //  '{ a : 1 }'
-      // and statements e.g.
-      //  'for (var i = 0; i < 10; i++) console.log(i);'
-
-      // First we attempt to eval as expression with parens.
-      // This catches '{a : 1}' properly.
-      self.eval('(' + evalCmd + ')',
-                self.context,
-                'repl',
-                function(e, ret) {
-            if (e && !e._isSyntaxError) return finish(e);
-
-            if (util.isFunction(ret) &&
-                /^[\r\n\s]*function/.test(evalCmd) || e) {
-              // Now as statement without parens.
-              self.eval(evalCmd, self.context, 'repl', finish);
-            } else {
-              finish(null, ret);
-            }
-          });
+      var evalCmd = self.bufferedCommand + cmd;
+      if (/^\s*\{/.test(evalCmd) && /\}\s*$/.test(evalCmd)) {
+        // 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.
+        evalCmd = '(' + evalCmd + ')\n';
+      } else {
+        // otherwise we just append a \n so that it will be either
+        // terminated, or continued onto the next expression if it's an
+        // unexpected end of input.
+        evalCmd = evalCmd + '\n';
+      }
 
+      debug('eval %j', evalCmd);
+      self.eval(evalCmd, self.context, 'repl', finish);
     } else {
       finish(null);
     }
 
     function finish(e, ret) {
-
+      debug('finish', e, ret);
       self.memory(cmd);
 
+      if (e && !self.bufferedCommand && cmd.trim().match(/^npm /)) {
+        self.outputStream.write('npm should be run outside of the ' +
+                                'node repl, in your normal shell.\n' +
+                                '(Press Control-D to exit.)\n');
+        self.bufferedCommand = '';
+        self.displayPrompt();
+        return;
+      }
+
       // If error was SyntaxError and not JSON.parse error
-      if (e && e._isSyntaxError) {
-        if (!self.bufferedCommand && cmd.trim().match(/^npm /)) {
-          self.outputStream.write('npm should be run outside of the ' +
-                                  'node repl, in your normal shell.\n' +
-                                  '(Press Control-D to exit.)\n');
-          self.bufferedCommand = '';
+      if (e) {
+        if (isRecoverableError(e)) {
+          // Start buffering data like that:
+          // {
+          // ...  x: 1
+          // ... }
+          self.bufferedCommand += cmd + '\n';
           self.displayPrompt();
           return;
+        } else {
+          self._domain.emit('error', e);
         }
-
-        // Start buffering data like that:
-        // {
-        // ...  x: 1
-        // ... }
-        self.bufferedCommand += cmd + '\n';
-        self.displayPrompt();
-        return;
-      } else if (e) {
-        self._domain.emit('error', e);
       }
 
       // Clear buffer if no SyntaxErrors
@@ -940,15 +936,10 @@ REPLServer.prototype.convertToContext = function(cmd) {
 };
 
 
-/**
- * Returns `true` if "e" is a SyntaxError, `false` otherwise.
- * filters out strict-mode errors, which are not recoverable
- */
-function isSyntaxError(e) {
-  // Convert error to string
-  e = e && (e.stack || e.toString());
-  return e && e.match(/^SyntaxError/) &&
-      // "strict mode" syntax errors
-      !e.match(/^SyntaxError: .*strict mode.*/i) &&
-      !e.match(/^SyntaxError: Assignment to constant variable/i);
+// 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) {
+  return e &&
+      e.name === 'SyntaxError' &&
+      /^Unexpected end of input/.test(e.message);
 }