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:
Blake Embrey 2015-11-13 16:40:08 -08:00 committed by James M Snell
parent d77a7588cf
commit 39d9afe279
2 changed files with 114 additions and 157 deletions

View File

@ -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;
}

View File

@ -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 }
]);
}