repl: refactor LineParser
implementation
Move the core logic from `LineParser` should fail handling into the recoverable error check for the REPL default eval. PR-URL: https://github.com/nodejs/node/pull/6171 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com>
This commit is contained in:
parent
d77a7588cf
commit
39d9afe279
262
lib/repl.js
262
lib/repl.js
@ -99,105 +99,6 @@ exports.writer = util.inspect;
|
||||
exports._builtinLibs = internalModule.builtinLibs;
|
||||
|
||||
|
||||
class LineParser {
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._literal = null;
|
||||
this.shouldFail = false;
|
||||
this.blockComment = false;
|
||||
this.regExpLiteral = false;
|
||||
this.prevTokenChar = null;
|
||||
}
|
||||
|
||||
parseLine(line) {
|
||||
var previous = null;
|
||||
this.shouldFail = false;
|
||||
const wasWithinStrLiteral = this._literal !== null;
|
||||
|
||||
for (const current of line) {
|
||||
if (previous === '\\') {
|
||||
// valid escaping, skip processing. previous doesn't matter anymore
|
||||
previous = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this._literal) {
|
||||
if (this.regExpLiteral && current === '/') {
|
||||
this.regExpLiteral = false;
|
||||
previous = null;
|
||||
continue;
|
||||
}
|
||||
if (previous === '*' && current === '/') {
|
||||
if (this.blockComment) {
|
||||
this.blockComment = false;
|
||||
previous = null;
|
||||
continue;
|
||||
} else {
|
||||
this.shouldFail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ignore rest of the line if `current` and `previous` are `/`s
|
||||
if (previous === current && previous === '/' && !this.blockComment) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (previous === '/') {
|
||||
if (current === '*') {
|
||||
this.blockComment = true;
|
||||
} else if (
|
||||
// Distinguish between a division operator and the start of a regex
|
||||
// by examining the non-whitespace character that precedes the /
|
||||
[null, '(', '[', '{', '}', ';'].includes(this.prevTokenChar)
|
||||
) {
|
||||
this.regExpLiteral = true;
|
||||
}
|
||||
previous = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.blockComment || this.regExpLiteral) continue;
|
||||
|
||||
if (current === this._literal) {
|
||||
this._literal = null;
|
||||
} else if (current === '\'' || current === '"') {
|
||||
this._literal = this._literal || current;
|
||||
}
|
||||
|
||||
if (current.trim() && current !== '/') this.prevTokenChar = current;
|
||||
|
||||
previous = current;
|
||||
}
|
||||
|
||||
const isWithinStrLiteral = this._literal !== null;
|
||||
|
||||
if (!wasWithinStrLiteral && !isWithinStrLiteral) {
|
||||
// Current line has nothing to do with String literals, trim both ends
|
||||
line = line.trim();
|
||||
} else if (wasWithinStrLiteral && !isWithinStrLiteral) {
|
||||
// was part of a string literal, but it is over now, trim only the end
|
||||
line = line.trimRight();
|
||||
} else if (isWithinStrLiteral && !wasWithinStrLiteral) {
|
||||
// was not part of a string literal, but it is now, trim only the start
|
||||
line = line.trimLeft();
|
||||
}
|
||||
|
||||
const lastChar = line.charAt(line.length - 1);
|
||||
|
||||
this.shouldFail = this.shouldFail ||
|
||||
((!this._literal && lastChar === '\\') ||
|
||||
(this._literal && lastChar !== '\\'));
|
||||
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function REPLServer(prompt,
|
||||
stream,
|
||||
eval_,
|
||||
@ -249,8 +150,6 @@ function REPLServer(prompt,
|
||||
self.breakEvalOnSigint = !!breakEvalOnSigint;
|
||||
self.editorMode = false;
|
||||
|
||||
self._inTemplateLiteral = false;
|
||||
|
||||
// just for backwards compat, see github.com/joyent/node/pull/7127
|
||||
self.rli = this;
|
||||
|
||||
@ -262,29 +161,20 @@ function REPLServer(prompt,
|
||||
|
||||
eval_ = eval_ || defaultEval;
|
||||
|
||||
function preprocess(code) {
|
||||
let cmd = code;
|
||||
if (/^\s*\{/.test(cmd) && /\}\s*$/.test(cmd)) {
|
||||
function defaultEval(code, context, file, cb) {
|
||||
var err, result, script, wrappedErr;
|
||||
var wrappedCmd = false;
|
||||
var input = code;
|
||||
|
||||
if (/^\s*\{/.test(code) && /\}\s*$/.test(code)) {
|
||||
// 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.
|
||||
cmd = `(${cmd})`;
|
||||
self.wrappedCmd = true;
|
||||
code = `(${code.trim()})\n`;
|
||||
wrappedCmd = true;
|
||||
}
|
||||
// Append a \n so that it will be either
|
||||
// terminated, or continued onto the next expression if it's an
|
||||
// unexpected end of input.
|
||||
return `${cmd}\n`;
|
||||
}
|
||||
|
||||
function defaultEval(code, context, file, cb) {
|
||||
// Remove trailing new line
|
||||
code = code.replace(/\n$/, '');
|
||||
code = preprocess(code);
|
||||
|
||||
var input = code;
|
||||
var err, result, wrappedErr;
|
||||
// first, create the Script object to check the syntax
|
||||
|
||||
if (code === '\n')
|
||||
@ -298,22 +188,22 @@ function REPLServer(prompt,
|
||||
// value for statements and declarations that don't return a value.
|
||||
code = `'use strict'; void 0;\n${code}`;
|
||||
}
|
||||
var script = vm.createScript(code, {
|
||||
script = vm.createScript(code, {
|
||||
filename: file,
|
||||
displayErrors: true
|
||||
});
|
||||
} catch (e) {
|
||||
debug('parse error %j', code, e);
|
||||
if (self.wrappedCmd) {
|
||||
self.wrappedCmd = false;
|
||||
if (wrappedCmd) {
|
||||
wrappedCmd = false;
|
||||
// unwrap and try again
|
||||
code = `${input.substring(1, input.length - 2)}\n`;
|
||||
code = input;
|
||||
wrappedErr = e;
|
||||
continue;
|
||||
}
|
||||
// preserve original error for wrapped command
|
||||
const error = wrappedErr || e;
|
||||
if (isRecoverableError(error, self))
|
||||
if (isRecoverableError(error, code))
|
||||
err = new Recoverable(error);
|
||||
else
|
||||
err = error;
|
||||
@ -400,7 +290,6 @@ function REPLServer(prompt,
|
||||
(_, pre, line) => pre + (line - 1));
|
||||
}
|
||||
top.outputStream.write((e.stack || e) + '\n');
|
||||
top.lineParser.reset();
|
||||
top.bufferedCommand = '';
|
||||
top.lines.level = [];
|
||||
top.displayPrompt();
|
||||
@ -427,7 +316,6 @@ function REPLServer(prompt,
|
||||
self.outputStream = output;
|
||||
|
||||
self.resetContext();
|
||||
self.lineParser = new LineParser();
|
||||
self.bufferedCommand = '';
|
||||
self.lines.level = [];
|
||||
|
||||
@ -490,7 +378,6 @@ function REPLServer(prompt,
|
||||
sawSIGINT = false;
|
||||
}
|
||||
|
||||
self.lineParser.reset();
|
||||
self.bufferedCommand = '';
|
||||
self.lines.level = [];
|
||||
self.displayPrompt();
|
||||
@ -498,6 +385,7 @@ function REPLServer(prompt,
|
||||
|
||||
self.on('line', function onLine(cmd) {
|
||||
debug('line %j', cmd);
|
||||
cmd = cmd || '';
|
||||
sawSIGINT = false;
|
||||
|
||||
if (self.editorMode) {
|
||||
@ -515,23 +403,28 @@ function REPLServer(prompt,
|
||||
return;
|
||||
}
|
||||
|
||||
// leading whitespaces in template literals should not be trimmed.
|
||||
if (self._inTemplateLiteral) {
|
||||
self._inTemplateLiteral = false;
|
||||
} else {
|
||||
cmd = self.lineParser.parseLine(cmd);
|
||||
}
|
||||
// Check REPL keywords and empty lines against a trimmed line input.
|
||||
const trimmedCmd = cmd.trim();
|
||||
|
||||
// Check to see if a REPL keyword was used. If it returns true,
|
||||
// display next prompt and return.
|
||||
if (cmd && cmd.charAt(0) === '.' && isNaN(parseFloat(cmd))) {
|
||||
var matches = cmd.match(/^\.([^\s]+)\s*(.*)$/);
|
||||
var keyword = matches && matches[1];
|
||||
var rest = matches && matches[2];
|
||||
if (self.parseREPLKeyword(keyword, rest) === true) {
|
||||
return;
|
||||
} else if (!self.bufferedCommand) {
|
||||
self.outputStream.write('Invalid REPL keyword\n');
|
||||
if (trimmedCmd) {
|
||||
if (trimmedCmd.charAt(0) === '.' && isNaN(parseFloat(trimmedCmd))) {
|
||||
const matches = trimmedCmd.match(/^\.([^\s]+)\s*(.*)$/);
|
||||
const keyword = matches && matches[1];
|
||||
const rest = matches && matches[2];
|
||||
if (self.parseREPLKeyword(keyword, rest) === true) {
|
||||
return;
|
||||
}
|
||||
if (!self.bufferedCommand) {
|
||||
self.outputStream.write('Invalid REPL keyword\n');
|
||||
finish(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Print a new line when hitting enter.
|
||||
if (!self.bufferedCommand) {
|
||||
finish(null);
|
||||
return;
|
||||
}
|
||||
@ -546,12 +439,10 @@ function REPLServer(prompt,
|
||||
debug('finish', e, ret);
|
||||
self.memory(cmd);
|
||||
|
||||
self.wrappedCmd = false;
|
||||
if (e && !self.bufferedCommand && cmd.trim().startsWith('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.lineParser.reset();
|
||||
self.bufferedCommand = '';
|
||||
self.displayPrompt();
|
||||
return;
|
||||
@ -559,8 +450,7 @@ function REPLServer(prompt,
|
||||
|
||||
// If error was SyntaxError and not JSON.parse error
|
||||
if (e) {
|
||||
if (e instanceof Recoverable && !self.lineParser.shouldFail &&
|
||||
!sawCtrlD) {
|
||||
if (e instanceof Recoverable && !sawCtrlD) {
|
||||
// Start buffering data like that:
|
||||
// {
|
||||
// ... x: 1
|
||||
@ -574,7 +464,6 @@ function REPLServer(prompt,
|
||||
}
|
||||
|
||||
// Clear buffer if no SyntaxErrors
|
||||
self.lineParser.reset();
|
||||
self.bufferedCommand = '';
|
||||
sawCtrlD = false;
|
||||
|
||||
@ -1234,7 +1123,6 @@ function defineDefaultCommands(repl) {
|
||||
repl.defineCommand('break', {
|
||||
help: 'Sometimes you get stuck, this gets you out',
|
||||
action: function() {
|
||||
this.lineParser.reset();
|
||||
this.bufferedCommand = '';
|
||||
this.displayPrompt();
|
||||
}
|
||||
@ -1249,7 +1137,6 @@ function defineDefaultCommands(repl) {
|
||||
repl.defineCommand('clear', {
|
||||
help: clearMessage,
|
||||
action: function() {
|
||||
this.lineParser.reset();
|
||||
this.bufferedCommand = '';
|
||||
if (!this.useGlobal) {
|
||||
this.outputStream.write('Clearing context...\n');
|
||||
@ -1370,20 +1257,13 @@ REPLServer.prototype.convertToContext = util.deprecate(function(cmd) {
|
||||
return cmd;
|
||||
}, 'replServer.convertToContext() is deprecated', 'DEP0024');
|
||||
|
||||
function bailOnIllegalToken(parser) {
|
||||
return parser._literal === null &&
|
||||
!parser.blockComment &&
|
||||
!parser.regExpLiteral;
|
||||
}
|
||||
|
||||
// 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, self) {
|
||||
function isRecoverableError(e, code) {
|
||||
if (e && e.name === 'SyntaxError') {
|
||||
var message = e.message;
|
||||
if (message === 'Unterminated template literal' ||
|
||||
message === 'Missing } in template expression') {
|
||||
self._inTemplateLiteral = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1393,11 +1273,81 @@ function isRecoverableError(e, self) {
|
||||
return true;
|
||||
|
||||
if (message === 'Invalid or unexpected token')
|
||||
return !bailOnIllegalToken(self.lineParser);
|
||||
return isCodeRecoverable(code);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check whether a code snippet should be forced to fail in the REPL.
|
||||
function isCodeRecoverable(code) {
|
||||
var current, previous, stringLiteral;
|
||||
var isBlockComment = false;
|
||||
var isSingleComment = false;
|
||||
var isRegExpLiteral = false;
|
||||
var lastChar = code.charAt(code.length - 2);
|
||||
var prevTokenChar = null;
|
||||
|
||||
for (var i = 0; i < code.length; i++) {
|
||||
previous = current;
|
||||
current = code[i];
|
||||
|
||||
if (previous === '\\' && (stringLiteral || isRegExpLiteral)) {
|
||||
current = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stringLiteral) {
|
||||
if (stringLiteral === current) {
|
||||
stringLiteral = null;
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
if (isRegExpLiteral && current === '/') {
|
||||
isRegExpLiteral = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isBlockComment && previous === '*' && current === '/') {
|
||||
isBlockComment = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSingleComment && current === '\n') {
|
||||
isSingleComment = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isBlockComment || isRegExpLiteral || isSingleComment) continue;
|
||||
|
||||
if (current === '/' && previous === '/') {
|
||||
isSingleComment = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previous === '/') {
|
||||
if (current === '*') {
|
||||
isBlockComment = true;
|
||||
} else if (
|
||||
// Distinguish between a division operator and the start of a regex
|
||||
// by examining the non-whitespace character that precedes the /
|
||||
[null, '(', '[', '{', '}', ';'].includes(prevTokenChar)
|
||||
) {
|
||||
isRegExpLiteral = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.trim()) prevTokenChar = current;
|
||||
}
|
||||
|
||||
if (current === '\'' || current === '"') {
|
||||
stringLiteral = current;
|
||||
}
|
||||
}
|
||||
|
||||
return stringLiteral ? lastChar === '\\' : isBlockComment;
|
||||
}
|
||||
|
||||
function Recoverable(err) {
|
||||
this.err = err;
|
||||
}
|
||||
|
@ -407,7 +407,14 @@ function error_test() {
|
||||
{
|
||||
client: client_unix, send: '(function() {\nif (false) {} /bar"/;\n}())',
|
||||
expect: prompt_multiline + prompt_multiline + 'undefined\n' + prompt_unix
|
||||
}
|
||||
},
|
||||
|
||||
// Newline within template string maintains whitespace.
|
||||
{ client: client_unix, send: '`foo \n`',
|
||||
expect: prompt_multiline + '\'foo \\n\'\n' + prompt_unix },
|
||||
// Whitespace is not evaluated.
|
||||
{ client: client_unix, send: ' \t \n',
|
||||
expect: prompt_unix }
|
||||
]);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user