diff --git a/lib/repl.js b/lib/repl.js index b00666267a646b..e4364e7d11cc42 100644 --- a/lib/repl.js +++ b/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; } diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js index 7e426eb54ee51c..b6de19856985ed 100644 --- a/test/parallel/test-repl.js +++ b/test/parallel/test-repl.js @@ -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 } ]); }