Skip to content

Commit 81ea52a

Browse files
committed
repl: improving line continuation handling
As it is, REPL doesn't honour the line continuation feature very well. This patch 1. keeps track of the beginning of the string literals and if they don't end or current line doesn't end with line continuation, then error out. 2. monitors if the line continuation character is used without the string literal and errors out if that happens. PR-URL: #2163 Reviewed-By: Jeremiah Senkpiel <[email protected]>
1 parent 30edb5a commit 81ea52a

File tree

2 files changed

+102
-15
lines changed

2 files changed

+102
-15
lines changed

lib/repl.js

+75-15
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ function REPLServer(prompt,
174174
self._domain.on('error', function(e) {
175175
debug('domain error');
176176
self.outputStream.write((e.stack || e) + '\n');
177+
self._currentStringLiteral = null;
177178
self.bufferedCommand = '';
178179
self.lines.level = [];
179180
self.displayPrompt();
@@ -200,6 +201,8 @@ function REPLServer(prompt,
200201
self.outputStream = output;
201202

202203
self.resetContext();
204+
// Initialize the current string literal found, to be null
205+
self._currentStringLiteral = null;
203206
self.bufferedCommand = '';
204207
self.lines.level = [];
205208

@@ -258,21 +261,86 @@ function REPLServer(prompt,
258261
sawSIGINT = false;
259262
}
260263

264+
self._currentStringLiteral = null;
261265
self.bufferedCommand = '';
262266
self.lines.level = [];
263267
self.displayPrompt();
264268
});
265269

270+
function parseLine(line, currentStringLiteral) {
271+
var previous = null, current = null;
272+
273+
for (var i = 0; i < line.length; i += 1) {
274+
if (previous === '\\') {
275+
// if it is a valid escaping, then skip processing
276+
previous = current;
277+
continue;
278+
}
279+
280+
current = line.charAt(i);
281+
if (current === currentStringLiteral) {
282+
currentStringLiteral = null;
283+
} else if (current === '\'' ||
284+
current === '"' &&
285+
currentStringLiteral === null) {
286+
currentStringLiteral = current;
287+
}
288+
previous = current;
289+
}
290+
291+
return currentStringLiteral;
292+
}
293+
294+
function getFinisherFunction(cmd, defaultFn) {
295+
if ((self._currentStringLiteral === null &&
296+
cmd.charAt(cmd.length - 1) === '\\') ||
297+
(self._currentStringLiteral !== null &&
298+
cmd.charAt(cmd.length - 1) !== '\\')) {
299+
300+
// If the line continuation is used outside string literal or if the
301+
// string continuation happens with out line continuation, then fail hard.
302+
// Even if the error is recoverable, get the underlying error and use it.
303+
return function(e, ret) {
304+
var error = e instanceof Recoverable ? e.err : e;
305+
306+
if (arguments.length === 2) {
307+
// using second argument only if it is actually passed. Otherwise
308+
// `undefined` will be printed when invalid REPL commands are used.
309+
return defaultFn(error, ret);
310+
}
311+
312+
return defaultFn(error);
313+
};
314+
}
315+
return defaultFn;
316+
}
317+
266318
self.on('line', function(cmd) {
267319
debug('line %j', cmd);
268320
sawSIGINT = false;
269321
var skipCatchall = false;
322+
var finisherFn = finish;
270323

271324
// leading whitespaces in template literals should not be trimmed.
272325
if (self._inTemplateLiteral) {
273326
self._inTemplateLiteral = false;
274327
} else {
275-
cmd = trimWhitespace(cmd);
328+
const wasWithinStrLiteral = self._currentStringLiteral !== null;
329+
self._currentStringLiteral = parseLine(cmd, self._currentStringLiteral);
330+
const isWithinStrLiteral = self._currentStringLiteral !== null;
331+
332+
if (!wasWithinStrLiteral && !isWithinStrLiteral) {
333+
// Current line has nothing to do with String literals, trim both ends
334+
cmd = cmd.trim();
335+
} else if (wasWithinStrLiteral && !isWithinStrLiteral) {
336+
// was part of a string literal, but it is over now, trim only the end
337+
cmd = cmd.trimRight();
338+
} else if (isWithinStrLiteral && !wasWithinStrLiteral) {
339+
// was not part of a string literal, but it is now, trim only the start
340+
cmd = cmd.trimLeft();
341+
}
342+
343+
finisherFn = getFinisherFunction(cmd, finish);
276344
}
277345

278346
// Check to see if a REPL keyword was used. If it returns true,
@@ -305,9 +373,9 @@ function REPLServer(prompt,
305373
}
306374

307375
debug('eval %j', evalCmd);
308-
self.eval(evalCmd, self.context, 'repl', finish);
376+
self.eval(evalCmd, self.context, 'repl', finisherFn);
309377
} else {
310-
finish(null);
378+
finisherFn(null);
311379
}
312380

313381
function finish(e, ret) {
@@ -318,6 +386,7 @@ function REPLServer(prompt,
318386
self.outputStream.write('npm should be run outside of the ' +
319387
'node repl, in your normal shell.\n' +
320388
'(Press Control-D to exit.)\n');
389+
self._currentStringLiteral = null;
321390
self.bufferedCommand = '';
322391
self.displayPrompt();
323392
return;
@@ -339,6 +408,7 @@ function REPLServer(prompt,
339408
}
340409

341410
// Clear buffer if no SyntaxErrors
411+
self._currentStringLiteral = null;
342412
self.bufferedCommand = '';
343413

344414
// If we got any output - print it (if no error)
@@ -870,6 +940,7 @@ function defineDefaultCommands(repl) {
870940
repl.defineCommand('break', {
871941
help: 'Sometimes you get stuck, this gets you out',
872942
action: function() {
943+
this._currentStringLiteral = null;
873944
this.bufferedCommand = '';
874945
this.displayPrompt();
875946
}
@@ -884,6 +955,7 @@ function defineDefaultCommands(repl) {
884955
repl.defineCommand('clear', {
885956
help: clearMessage,
886957
action: function() {
958+
this._currentStringLiteral = null;
887959
this.bufferedCommand = '';
888960
if (!this.useGlobal) {
889961
this.outputStream.write('Clearing context...\n');
@@ -949,18 +1021,6 @@ function defineDefaultCommands(repl) {
9491021
});
9501022
}
9511023

952-
953-
function trimWhitespace(cmd) {
954-
const trimmer = /^\s*(.+)\s*$/m;
955-
var matches = trimmer.exec(cmd);
956-
957-
if (matches && matches.length === 2) {
958-
return matches[1];
959-
}
960-
return '';
961-
}
962-
963-
9641024
function regexpEscape(s) {
9651025
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
9661026
}

test/parallel/test-repl.js

+27
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,33 @@ function error_test() {
198198
// a REPL command
199199
{ client: client_unix, send: '.toString',
200200
expect: 'Invalid REPL keyword\n' + prompt_unix },
201+
// fail when we are not inside a String and a line continuation is used
202+
{ client: client_unix, send: '[] \\',
203+
expect: /^SyntaxError: Unexpected token ILLEGAL/ },
204+
// do not fail when a String is created with line continuation
205+
{ client: client_unix, send: '\'the\\\nfourth\\\neye\'',
206+
expect: prompt_multiline + prompt_multiline +
207+
'\'thefourtheye\'\n' + prompt_unix },
208+
// Don't fail when a partial String is created and line continuation is used
209+
// with whitespace characters at the end of the string. We are to ignore it.
210+
// This test is to make sure that we properly remove the whitespace
211+
// characters at the end of line, unlike the buggy `trimWhitespace` function
212+
{ client: client_unix, send: ' \t .break \t ',
213+
expect: prompt_unix },
214+
// multiline strings preserve whitespace characters in them
215+
{ client: client_unix, send: '\'the \\\n fourth\t\t\\\n eye \'',
216+
expect: prompt_multiline + prompt_multiline +
217+
'\'the fourth\\t\\t eye \'\n' + prompt_unix },
218+
// more than one multiline strings also should preserve whitespace chars
219+
{ client: client_unix, send: '\'the \\\n fourth\' + \'\t\t\\\n eye \'',
220+
expect: prompt_multiline + prompt_multiline +
221+
'\'the fourth\\t\\t eye \'\n' + prompt_unix },
222+
// using REPL commands within a string literal should still work
223+
{ client: client_unix, send: '\'\\\n.break',
224+
expect: prompt_unix },
225+
// using REPL command "help" within a string literal should still work
226+
{ client: client_unix, send: '\'thefourth\\\n.help\neye\'',
227+
expect: /'thefourtheye'/ },
201228
]);
202229
}
203230

0 commit comments

Comments
 (0)