Browse Source

repl: Simplify paren wrap, continuation-detection

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
v0.11.7-release
isaacs 11 years ago
parent
commit
9ef9a9dee5
  1. 115
      lib/repl.js

115
lib/repl.js

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

Loading…
Cancel
Save