Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : Support for multiline statements #16

Merged
merged 1 commit into from
Aug 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ class IO {
this.stdin = stdin;
this.stdout = stdout;

this.buffer = '';
this.buffer = ''; // used for capturing the current statement
this.cursor = 0;
this.prefix = '';
this.suffix = '';
this.multilineBuffer = ''; // for buffering the multiline statements

this._paused = false;
this.transformBuffer = transformBuffer;
Expand Down Expand Up @@ -134,7 +135,19 @@ class IO {
this.buffer = '';
this.cursor = 0;
this.history.unshift(b);
this.stdout.write(`${await onLine(b)}\n`);
this.multilineBuffer += b;
// always buffer the line so that when we encounter multi-line
// statements we can execute them as needed.
const code = this.multilineBuffer;
const result = await onLine(code);
// online returns a Symbol when it sees a multi-line statement
if (IO.kNeedsAnotherLine !== result) {
this.stdout.write(`${result}\n`);
this.multilineBuffer = '';
this.setPrefix('> ');
} else {
this.setPrefix('... ');
}
this.unpause();
} else {
this.stdout.write('\n');
Expand Down Expand Up @@ -298,4 +311,7 @@ class IO {
}
}

// Symbol to notify that IO needs an another line
IO.kNeedsAnotherLine = Symbol('IO.kNeedsAnotherLine');

module.exports = IO;
79 changes: 79 additions & 0 deletions src/recoverable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict';

const acorn = require('acorn');
const { tokTypes: tt } = acorn;

// If the error is that we've unexpectedly ended the input,
// then let the user try to recover by adding more input.
// Note: `e` (the original exception) is not used by the current implemention,
// but may be needed in the future.
function isRecoverableError(e, code) {
let recoverable = false;

// Determine if the point of the any error raised is at the end of the input.
// There are two cases to consider:
//
// 1. Any error raised after we have encountered the 'eof' token.
// This prevents us from declaring partial tokens (like '2e') as
// recoverable.
//
// 2. Three cases where tokens can legally span lines. This is
// template, comment, and strings with a backslash at the end of
// the line, indicating a continuation. Note that we need to look
// for the specific errors of 'unterminated' kind (not, for example,
// a syntax error in a ${} expression in a template), and the only
// way to do that currently is to look at the message. Should Acorn
// change these messages in the future, this will lead to a test
// failure, indicating that this code needs to be updated.
//
acorn.plugins.replRecoverable = (parser) => {
parser.extend('nextToken', (nextToken) => {
return function() {
Reflect.apply(nextToken, this, []);

if (this.type === tt.eof) recoverable = true;
};
});

parser.extend('raise', (raise) => {
return function(pos, message) {
switch (message) {
case 'Unterminated template':
case 'Unterminated comment':
case 'Unexpected end of input':
recoverable = true;
break;

case 'Unterminated string constant':
const token = this.input.slice(this.lastTokStart, this.pos);
// see https://www.ecma-international.org/ecma-262/#sec-line-terminators
recoverable = /\\(?:\r\n?|\n|\u2028|\u2029)$/.test(token);
}

Reflect.apply(raise, this, [pos, message]);
};
});
};

// For similar reasons as `defaultEval`, wrap expressions starting with a
// curly brace with parenthesis. Note: only the open parenthesis is added
// here as the point is to test for potentially valid but incomplete
// expressions.
if (/^\s*\{/.test(code) && isRecoverableError(e, `(${code}`)) return true;

// Try to parse the code with acorn. If the parse fails, ignore the acorn
// error and return the recoverable status.
try {
acorn.parse(code, { plugins: { replRecoverable: true } });

// Odd case: the underlying JS engine (V8, Chakra) rejected this input
// but Acorn detected no issue. Presume that additional text won't
// address this issue.
return false;
} catch (e) {
return recoverable;
}
}

module.exports = isRecoverableError

31 changes: 20 additions & 11 deletions src/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const highlight = require('./highlight');
const { processTopLevelAwait } = require('./await');
const { Runtime, mainContextIdPromise } = require('./inspector');
const { strEscape, isIdentifier } = require('./util');
const isRecoverableError = require('./recoverable');

const inspect = (v) => util.inspect(v, { colors: true, showProxy: 2 });

Expand Down Expand Up @@ -96,21 +97,29 @@ Prototype REPL - https://github.com/nodejs/repl`,
const evaluateResult = await this.eval(line, awaited);

if (evaluateResult.exceptionDetails) {
// lets try for recovering
const result = isRecoverableError(evaluateResult.exceptionDetails.exception, line);
if (result) {
return IO.kNeedsAnotherLine;
} else {
// we tried our best - throw error
await this.callFunctionOn(
(err) => {
global.REPL.lastError = err;
},
evaluateResult.exceptionDetails.exception,
);
return inspect(global.REPL.lastError);
}
} else {
await this.callFunctionOn(
(err) => {
global.REPL.lastError = err;
(result) => {
global.REPL.last = result;
},
evaluateResult.exceptionDetails.exception,
evaluateResult.result,
);
return inspect(global.REPL.lastError);
return inspect(global.REPL.last);
}
await this.callFunctionOn(
(result) => {
global.REPL.last = result;
},
evaluateResult.result,
);
return inspect(global.REPL.last);
}

async onAutocomplete(buffer) {
Expand Down