repl: Assignment of _ allowed with warning

This commit addresses https://github.com/nodejs/node/issues/5431 by
changing the way that the repl handles assignment to the global _
variable.

Prior to this commit, node sets the result of the last expression
evaluated in the repl to `_`. This causes problems for users of
underscore, lodash and other packages where it is common to assign
`_` to the package, e.g. `_ = require('lodash');`.

Changes in this commit now result in the following behavior.

- If unassigned on the repl, `_` continues to refer to the last
  evaluated expression.
- If assigned, the default behavior of assigning `_` to the last
  evaluated expression is disabled, and `_` now references whatever
  value was explicitly set. A warning is issued on the repl -
  'expression assignment to _ now disabled'.
- If `_` is assigned multiple times, the warning is only displayed once.
- When `.clear` is executed in the repl, `_` continues to refer to its
  most recent value, whatever that is (this is per existing behavior).
  If `_` had been explicitly set prior to `.clear` it will not change
  again with the evaluation of the next expression.

PR-URL: https://github.com/nodejs/node/pull/5535
Fixes: https://github.com/nodejs/node/issues/5431
Reviewed-By: Roman Reiss <me@silverwind.io>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Lance Ball 2016-03-02 16:45:16 -05:00 committed by silverwind
parent c67937bebb
commit ad8257fa5b
3 changed files with 183 additions and 5 deletions

View File

@ -86,6 +86,8 @@ The special variable `_` (underscore) contains the result of the last expression
4 4
``` ```
Explicitly setting `_` will disable this behavior until the context is reset.
The REPL provides access to any variables in the global scope. You can expose The REPL provides access to any variables in the global scope. You can expose
a variable to the REPL explicitly by assigning it to the `context` object a variable to the REPL explicitly by assigning it to the `context` object
associated with each `REPLServer`. For example: associated with each `REPLServer`. For example:

View File

@ -205,6 +205,8 @@ function REPLServer(prompt,
self.useGlobal = !!useGlobal; self.useGlobal = !!useGlobal;
self.ignoreUndefined = !!ignoreUndefined; self.ignoreUndefined = !!ignoreUndefined;
self.replMode = replMode || exports.REPL_MODE_SLOPPY; self.replMode = replMode || exports.REPL_MODE_SLOPPY;
self.underscoreAssigned = false;
self.last = undefined;
self._inTemplateLiteral = false; self._inTemplateLiteral = false;
@ -471,7 +473,9 @@ function REPLServer(prompt,
// the second argument to this function is there, print it. // the second argument to this function is there, print it.
arguments.length === 2 && arguments.length === 2 &&
(!self.ignoreUndefined || ret !== undefined)) { (!self.ignoreUndefined || ret !== undefined)) {
self.context._ = ret; if (!self.underscoreAssigned) {
self.last = ret;
}
self.outputStream.write(self.writer(ret) + '\n'); self.outputStream.write(self.writer(ret) + '\n');
} }
@ -545,20 +549,22 @@ REPLServer.prototype.createContext = function() {
context.module = module; context.module = module;
context.require = require; context.require = require;
this.underscoreAssigned = false;
this.lines = []; this.lines = [];
this.lines.level = []; this.lines.level = [];
// make built-in modules available directly // make built-in modules available directly
// (loaded lazily) // (loaded lazily)
exports._builtinLibs.forEach(function(name) { exports._builtinLibs.forEach((name) => {
Object.defineProperty(context, name, { Object.defineProperty(context, name, {
get: function() { get: () => {
var lib = require(name); var lib = require(name);
context._ = context[name] = lib; this.last = context[name] = lib;
return lib; return lib;
}, },
// allow the creation of other globals with this name // allow the creation of other globals with this name
set: function(val) { set: (val) => {
delete context[name]; delete context[name];
context[name] = val; context[name] = val;
}, },
@ -566,6 +572,20 @@ REPLServer.prototype.createContext = function() {
}); });
}); });
Object.defineProperty(context, '_', {
configurable: true,
get: () => {
return this.last;
},
set: (value) => {
this.last = value;
if (!this.underscoreAssigned) {
this.underscoreAssigned = true;
this.outputStream.write('Expression assignment to _ now disabled.\n');
}
}
});
return context; return context;
}; };

View File

@ -0,0 +1,156 @@
'use strict';
require('../common');
const assert = require('assert');
const repl = require('repl');
const stream = require('stream');
testSloppyMode();
testStrictMode();
testResetContext();
testMagicMode();
function testSloppyMode() {
const r = initRepl(repl.REPL_MODE_SLOPPY);
// cannot use `let` in sloppy mode
r.write(`_; // initial value undefined
var x = 10; // evaluates to undefined
_; // still undefined
y = 10; // evaluates to 10
_; // 10 from last eval
_ = 20; // explicitly set to 20
_; // 20 from user input
_ = 30; // make sure we can set it twice and no prompt
_; // 30 from user input
y = 40; // make sure eval doesn't change _
_; // remains 30 from user input
`);
assertOutput(r.output, [
'undefined',
'undefined',
'undefined',
'10',
'10',
'Expression assignment to _ now disabled.',
'20',
'20',
'30',
'30',
'40',
'30'
]);
}
function testStrictMode() {
const r = initRepl(repl.REPL_MODE_STRICT);
r.write(`_; // initial value undefined
var x = 10; // evaluates to undefined
_; // still undefined
let _ = 20; // use 'let' only in strict mode - evals to undefined
_; // 20 from user input
_ = 30; // make sure we can set it twice and no prompt
_; // 30 from user input
var y = 40; // make sure eval doesn't change _
_; // remains 30 from user input
function f() { let _ = 50; } // undefined
f(); // undefined
_; // remains 30 from user input
`);
assertOutput(r.output, [
'undefined',
'undefined',
'undefined',
'undefined',
'20',
'30',
'30',
'undefined',
'30',
'undefined',
'undefined',
'30'
]);
}
function testMagicMode() {
const r = initRepl(repl.REPL_MODE_MAGIC);
r.write(`_; // initial value undefined
x = 10; //
_; // last eval - 10
let _ = 20; // undefined
_; // 20 from user input
_ = 30; // make sure we can set it twice and no prompt
_; // 30 from user input
var y = 40; // make sure eval doesn't change _
_; // remains 30 from user input
function f() { let _ = 50; return _; } // undefined
f(); // 50
_; // remains 30 from user input
`);
assertOutput(r.output, [
'undefined',
'10',
'10',
'undefined',
'20',
'30',
'30',
'undefined',
'30',
'undefined',
'50',
'30'
]);
}
function testResetContext() {
const r = initRepl(repl.REPL_MODE_SLOPPY);
r.write(`_ = 10; // explicitly set to 10
_; // 10 from user input
.clear // Clearing context...
_; // remains 10
x = 20; // but behavior reverts to last eval
_; // expect 20
`);
assertOutput(r.output, [
'Expression assignment to _ now disabled.',
'10',
'10',
'Clearing context...',
'10',
'20',
'20'
]);
}
function initRepl(mode) {
const inputStream = new stream.PassThrough();
const outputStream = new stream.PassThrough();
outputStream.accum = '';
outputStream.on('data', (data) => {
outputStream.accum += data;
});
return repl.start({
input: inputStream,
output: outputStream,
useColors: false,
terminal: false,
prompt: '',
replMode: mode
});
}
function assertOutput(output, expected) {
const lines = output.accum.trim().split('\n');
assert.deepStrictEqual(lines, expected);
}