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

Fix REPL bug with previous line carryover #1480

Merged
merged 8 commits into from Oct 6, 2021
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");
}
);
}
);