Showing with 136 additions and 90 deletions.
  1. +71 −75 lib/repl.js
  2. +47 −0 test/simple/test-repl-harmony.js
  3. +18 −15 test/simple/test-repl.js
@@ -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.
@@ -111,35 +112,45 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {

function defaultEval(code, context, file, cb) {
var err, result;
// first, create the Script object to check the syntax
try {
if (self.useGlobal) {
result = vm.runInThisContext(code, {
filename: file,
displayErrors: false
});
} else {
result = vm.runInContext(code, context, {
filename: file,
displayErrors: false
});
}
var script = vm.createScript(code, {
filename: file,
displayErrors: false
});
} catch (e) {
err = e;
debug('parse error %j', code, e);
}
if (err && process.domain && !isSyntaxError(err)) {
process.domain.emit('error', err);
process.domain.exit();
}
else {
cb(err, result);

if (!err) {
try {
if (self.useGlobal) {
result = script.runInThisContext({ displayErrors: false });
} else {
result = script.runInContext(context, { displayErrors: false });
}
} catch (e) {
err = e;
if (err && process.domain) {
debug('not recoverable, send to domain');
process.domain.emit('error', err);
process.domain.exit();
return;
}
}
}

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 = [];
self.displayPrompt();
});

@@ -165,6 +176,7 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {

self.resetContext();
self.bufferedCommand = '';
self.lines.level = [];

self.prompt = !util.isUndefined(prompt) ? prompt : '> ';

@@ -222,10 +234,12 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
}

self.bufferedCommand = '';
self.lines.level = [];
self.displayPrompt();
});

rli.on('line', function(cmd) {
debug('line %j', cmd);
sawSIGINT = false;
var skipCatchall = false;
cmd = trimWhitespace(cmd);
@@ -245,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 && !isSyntaxError(e)) 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 (isSyntaxError(e)) {
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
@@ -930,20 +936,10 @@ REPLServer.prototype.convertToContext = function(cmd) {
};


/**
* Returns `true` if "e" is a SyntaxError, `false` otherwise.
* This function filters out false positives likes JSON.parse() errors and
* RegExp syntax errors.
*/
function isSyntaxError(e) {
// Convert error to string
e = e && (e.stack || e.toString());
return e && e.match(/^SyntaxError/) &&
// RegExp syntax error
!e.match(/^SyntaxError: Invalid regular expression/) &&
!e.match(/^SyntaxError: Invalid flags supplied to RegExp constructor/) &&
// "strict mode" syntax errors
!e.match(/^SyntaxError: .*strict mode.*/i) &&
// JSON.parse() error
!e.match(/\n {4}at Object.parse \(native\)\n/);
// 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);
}
@@ -0,0 +1,47 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

var common = require('../common');
var assert = require('assert');

var spawn = require('child_process').spawn;
var args = ['--harmony', '--use-strict', '-i'];
var child = spawn(process.execPath, args);

var input = 'function x(){const y=1;y=2}\n';
var expectOut = /^> SyntaxError: Assignment to constant variable.\n/;

child.stderr.setEncoding('utf8');
child.stderr.on('data', function(c) {
throw new Error('child.stderr be silent');
});

child.stdout.setEncoding('utf8');
var out = '';
child.stdout.on('data', function(c) {
out += c;
});
child.stdout.on('end', function() {
assert(expectOut.test(out));
console.log('ok');
});

child.stdin.end(input);
@@ -40,7 +40,7 @@ var net = require('net'),
// absolute path to test/fixtures/a.js
var moduleFilename = require('path').join(common.fixturesDir, 'a');

common.error('repl test');
console.error('repl test');

// function for REPL to run
invoke_me = function(arg) {
@@ -51,7 +51,7 @@ function send_expect(list) {
if (list.length > 0) {
var cur = list.shift();

common.error('sending ' + JSON.stringify(cur.send));
console.error('sending ' + JSON.stringify(cur.send));

cur.client.expect = cur.expect;
cur.client.list = list;
@@ -74,7 +74,7 @@ function error_test() {

client_unix.on('data', function(data) {
read_buffer += data.toString('ascii', 0, data.length);
common.error('Unix data: ' + JSON.stringify(read_buffer) + ', expecting ' +
console.error('Unix data: ' + JSON.stringify(read_buffer) + ', expecting ' +
(client_unix.expect.exec ?
client_unix.expect :
JSON.stringify(client_unix.expect)));
@@ -83,13 +83,13 @@ function error_test() {
// if it's an exact match, then don't do the regexp
if (read_buffer !== client_unix.expect) {
assert.ok(read_buffer.match(client_unix.expect));
common.error('match');
console.error('match');
}
read_buffer = '';
if (client_unix.list && client_unix.list.length > 0) {
send_expect(client_unix.list);
} else {
common.error('End of Error test, running TCP test.');
console.error('End of Error test, running TCP test.');
tcp_test();
}

@@ -100,12 +100,12 @@ function error_test() {
if (client_unix.list && client_unix.list.length > 0) {
send_expect(client_unix.list);
} else {
common.error('End of Error test, running TCP test.\n');
console.error('End of Error test, running TCP test.\n');
tcp_test();
}

} else {
common.error('didn\'t see prompt yet, buffering.');
console.error('didn\'t see prompt yet, buffering.');
}
});

@@ -119,6 +119,9 @@ function error_test() {
// You can recover with the .break command
{ client: client_unix, send: '.break',
expect: prompt_unix },
// But passing the same string to eval() should throw
{ client: client_unix, send: 'eval("function test_func() {")',
expect: /^SyntaxError: Unexpected end of input/ },
// Floating point numbers are not interpreted as REPL commands.
{ client: client_unix, send: '.1234',
expect: '0.1234' },
@@ -233,20 +236,20 @@ function tcp_test() {

client_tcp.on('data', function(data) {
read_buffer += data.toString('ascii', 0, data.length);
common.error('TCP data: ' + JSON.stringify(read_buffer) +
console.error('TCP data: ' + JSON.stringify(read_buffer) +
', expecting ' + JSON.stringify(client_tcp.expect));
if (read_buffer.indexOf(prompt_tcp) !== -1) {
assert.strictEqual(client_tcp.expect, read_buffer);
common.error('match');
console.error('match');
read_buffer = '';
if (client_tcp.list && client_tcp.list.length > 0) {
send_expect(client_tcp.list);
} else {
common.error('End of TCP test.\n');
console.error('End of TCP test.\n');
clean_up();
}
} else {
common.error('didn\'t see prompt yet, buffering');
console.error('didn\'t see prompt yet, buffering');
}
});

@@ -302,20 +305,20 @@ function unix_test() {

client_unix.on('data', function(data) {
read_buffer += data.toString('ascii', 0, data.length);
common.error('Unix data: ' + JSON.stringify(read_buffer) +
console.error('Unix data: ' + JSON.stringify(read_buffer) +
', expecting ' + JSON.stringify(client_unix.expect));
if (read_buffer.indexOf(prompt_unix) !== -1) {
assert.strictEqual(client_unix.expect, read_buffer);
common.error('match');
console.error('match');
read_buffer = '';
if (client_unix.list && client_unix.list.length > 0) {
send_expect(client_unix.list);
} else {
common.error('End of Unix test, running Error test.\n');
console.error('End of Unix test, running Error test.\n');
process.nextTick(error_test);
}
} else {
common.error('didn\'t see prompt yet, buffering.');
console.error('didn\'t see prompt yet, buffering.');
}
});