Skip to content

Commit

Permalink
Fix REPL bug with previous line carryover (#1480)
Browse files Browse the repository at this point in the history
* Fix expressions carrying over from the previous line

* Simplify semicolon insertion logic and avoid issues with line number

* fix-previous-line-carryover: Add tests, simplify code, remove dead code

* Minor refactor of ASI and and test tweak

* Fix linting issue

* Avoid adding a semicolon if one already exists, since TS emits double-semicolons
as empty statements and this *might* affect stack trace line numbers at
some point

* lint-fix

* Add test case for avoiding double-semicolons; also fix tests to allow console.log() capture

Co-authored-by: Andrew Bradley <cspotcode@gmail.com>
  • Loading branch information
TheUnlocked and cspotcode committed Oct 6, 2021
1 parent 1660ed1 commit 86a27be
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 9 deletions.
21 changes: 12 additions & 9 deletions src/repl.ts
Expand Up @@ -510,6 +510,18 @@ function appendCompileAndEvalInput(options: {
undo();
} else {
state.output = output;

// Insert a semicolon to make sure that the code doesn't interact with the next line,
// for example to prevent `2\n+ 2` from producing 4.
// This is safe since the output will not change since we can only get here with successful inputs,
// and adding a semicolon to the end of a successful input won't ever change the output.
state.input = state.input.replace(
/([^\n\s])([\n\s]*)$/,
(all, lastChar, whitespace) => {
if (lastChar !== ';') return `${lastChar};${whitespace}`;
return all;
}
);
}

let commands: Array<{ mustAwait?: true; execCommand: () => any }> = [];
Expand Down Expand Up @@ -586,15 +598,6 @@ function appendToEvalState(state: EvalState, input: string) {
const undoOutput = state.output;
const undoLines = state.lines;

// Handle ASI issues with TypeScript re-evaluation.
if (
undoInput.charAt(undoInput.length - 1) === '\n' &&
/^\s*[\/\[(`-]/.test(input) &&
!/;\s*$/.test(undoInput)
) {
state.input = `${state.input.slice(0, -1)};\n`;
}

state.input += input;
state.lines += lineCount(input);
state.version++;
Expand Down
122 changes: 122 additions & 0 deletions src/test/repl/repl.spec.ts
Expand Up @@ -288,3 +288,125 @@ test.suite(
});
}
);

test.suite(
'REPL inputs are syntactically independent of each other',
(test) => {
// Serial because it's timing-sensitive
test.serial(
'arithmetic operators are independent of previous values',
async (t) => {
const { stdout, stderr } = await t.context.executeInRepl(
`9
+ 3
7
- 3
3
* 7\n.break
100
/ 2\n.break
5
** 2\n.break
console.log('done!')
`,
{
registerHooks: true,
startInternalOptions: { useGlobal: false },
waitPattern: 'done!\nundefined\n>',
}
);
expect(stdout).not.toContain('12');
expect(stdout).not.toContain('4');
expect(stdout).not.toContain('21');
expect(stdout).not.toContain('50');
expect(stdout).not.toContain('25');
expect(stdout).toContain('3');
expect(stdout).toContain('-3');
}
);

// Serial because it's timing-sensitive
test.serial(
'automatically inserted semicolons do not appear in error messages at the end',
async (t) => {
const { stdout, stderr } = await t.context.executeInRepl(
`(
a
console.log('done!')`,
{
registerHooks: true,
startInternalOptions: { useGlobal: false },
waitPattern: 'done!\nundefined\n>',
}
);
expect(stderr).toContain("error TS1005: ')' expected.");
expect(stderr).not.toContain(';');
}
);

// Serial because it's timing-sensitive
test.serial(
'automatically inserted semicolons do not appear in error messages at the start',
async (t) => {
const { stdout, stderr } = await t.context.executeInRepl(
`)
console.log('done!')`,
{
registerHooks: true,
startInternalOptions: { useGlobal: false },
waitPattern: 'done!\nundefined\n>',
}
);
expect(stderr).toContain(
'error TS1128: Declaration or statement expected.'
);
expect(stderr).toContain(')');
expect(stderr).not.toContain(';');
}
);

// Serial because it's timing-sensitive
test.serial(
'automatically inserted semicolons do not break function calls',
async (t) => {
const { stdout, stderr } = await t.context.executeInRepl(
`function foo(a: number) {
return a + 1;
}
foo(
1
)`,
{
registerHooks: true,
startInternalOptions: { useGlobal: false },
waitPattern: '2\n>',
}
);
expect(stderr).toBe('');
expect(stdout).toContain('2');
}
);

// Serial because it's timing-sensitive
test.serial(
'automatically inserted semicolons do not affect subsequent line numbers',
async (t) => {
// If first line of input ends in a semicolon, should not add a second semicolon.
// That will cause an extra blank line in the compiled output which will
// offset the stack line number.
const { stdout, stderr } = await t.context.executeInRepl(
`1;
new Error().stack!.split('\\n')[1]
console.log('done!')`,
{
registerHooks: true,
startInternalOptions: { useGlobal: false },
waitPattern: 'done!',
}
);
expect(stderr).toBe('');
expect(stdout).toContain(":1:1'\n");
}
);
}
);

0 comments on commit 86a27be

Please sign in to comment.