repl: make last error available as `_error`

This is pretty useful when trying to inspect the last
error caught by a REPL, and is made to be analogous to `_`,
which contains the last successful completion value.

PR-URL: https://github.com/nodejs/node/pull/18919
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Evan Lucas <evanlucas@me.com>
Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Prince John Wesley <princejohnwesley@gmail.com>
Reviewed-By: Shingo Inoue <leko.noor@gmail.com>
This commit is contained in:
Anna Henningsen 2018-02-21 21:50:20 +01:00
parent 648d668fcc
commit a8b5192fef
No known key found for this signature in database
GPG Key ID: 9C63F3A6CD2AD8F9
3 changed files with 103 additions and 0 deletions

View File

@ -142,6 +142,12 @@ global or scoped variable, the input `fs` will be evaluated on-demand as
```
#### Assignment of the `_` (underscore) variable
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/18919
description: Added `_error` support.
-->
The default evaluator will, by default, assign the result of the most recently
evaluated expression to the special variable `_` (underscore).
@ -162,6 +168,17 @@ Expression assignment to _ now disabled.
4
```
Similarly, `_error` will refer to the last seen error, if there was any.
Explicitly setting `_error` to a value will disable this behavior.
<!-- eslint-skip -->
```js
> throw new Error('foo');
Error: foo
> _error.message
'foo'
```
### Custom Evaluation Functions
When a new `repl.REPLServer` is created, a custom evaluation function may be

View File

@ -156,6 +156,8 @@ function REPLServer(prompt,
self.replMode = replMode || exports.REPL_MODE_SLOPPY;
self.underscoreAssigned = false;
self.last = undefined;
self.underscoreErrAssigned = false;
self.lastError = undefined;
self.breakEvalOnSigint = !!breakEvalOnSigint;
self.editorMode = false;
// Context id for use with the inspector protocol.
@ -388,6 +390,8 @@ function REPLServer(prompt,
internalUtil.decorateErrorStack(e);
Error.prepareStackTrace = pstrace;
const isError = internalUtil.isError(e);
if (!self.underscoreErrAssigned)
self.lastError = e;
if (e instanceof SyntaxError && e.stack) {
// remove repl:line-number and stack trace
e.stack = e.stack
@ -796,6 +800,7 @@ REPLServer.prototype.createContext = function() {
REPLServer.prototype.resetContext = function() {
this.context = this.createContext();
this.underscoreAssigned = false;
this.underscoreErrAssigned = false;
this.lines = [];
this.lines.level = [];
@ -811,6 +816,19 @@ REPLServer.prototype.resetContext = function() {
}
});
Object.defineProperty(this.context, '_error', {
configurable: true,
get: () => this.lastError,
set: (value) => {
this.lastError = value;
if (!this.underscoreErrAssigned) {
this.underscoreErrAssigned = true;
this.outputStream.write(
'Expression assignment to _error now disabled.\n');
}
}
});
// Allow REPL extensions to extend the new context
this.emit('reset', this.context);
};

View File

@ -10,6 +10,7 @@ testStrictMode();
testResetContext();
testResetContextGlobal();
testMagicMode();
testError();
function testSloppyMode() {
const r = initRepl(repl.REPL_MODE_SLOPPY);
@ -153,6 +154,73 @@ function testResetContextGlobal() {
delete global.require;
}
function testError() {
const r = initRepl(repl.REPL_MODE_STRICT);
r.write(`_error; // initial value undefined
throw new Error('foo'); // throws error
_error; // shows error
fs.readdirSync('/nonexistent?'); // throws error, sync
_error.code; // shows error code
_error.syscall; // shows error syscall
setImmediate(() => { throw new Error('baz'); }); undefined;
// throws error, async
`);
setImmediate(() => {
const lines = r.output.accum.trim().split('\n');
const expectedLines = [
'undefined',
// The error, both from the original throw and the `_error` echo.
'Error: foo',
'Error: foo',
// The sync error, with individual property echoes
/Error: ENOENT: no such file or directory, scandir '.*nonexistent.*'/,
/fs\.readdirSync/,
"'ENOENT'",
"'scandir'",
// Dummy 'undefined' from the explicit silencer + one from the comment
'undefined',
'undefined',
// The message from the original throw
'Error: baz',
/setImmediate/,
/^ at/,
/^ at/,
/^ at/,
/^ at/,
];
for (const line of lines) {
const expected = expectedLines.shift();
if (typeof expected === 'string')
assert.strictEqual(line, expected);
else
assert(expected.test(line), `${line} should match ${expected}`);
}
assert.strictEqual(expectedLines.length, 0);
// Reset output, check that '_error' is the asynchronously caught error.
r.output.accum = '';
r.write(`_error.message // show the message
_error = 0; // disable auto-assignment
throw new Error('quux'); // new error
_error; // should not see the new error
`);
assertOutput(r.output, [
"'baz'",
'Expression assignment to _error now disabled.',
'0',
'Error: quux',
'0'
]);
});
}
function initRepl(mode, useGlobal) {
const inputStream = new stream.PassThrough();
const outputStream = new stream.PassThrough();