Skip to content

Commit 98f30e8

Browse files
d9kcspotcode
andauthored
Fix #1667 multiline function arguments support in REPL (#1677)
* Fix #1667 multiline function arguments support in REPL * introduce dependency mechanism to recovery_codes to prevent erroneous suppression * finish tests * fix skipped tests * Fix bug where node env reset also deletes coverage data Co-authored-by: Andrew Bradley <[email protected]>
1 parent b5aa55d commit 98f30e8

File tree

5 files changed

+213
-126
lines changed

5 files changed

+213
-126
lines changed

src/repl.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -679,17 +679,24 @@ function lineCount(value: string) {
679679
}
680680

681681
/**
682-
* TS diagnostic codes which are recoverable, meaning that the user likely entered and incomplete line of code
682+
* TS diagnostic codes which are recoverable, meaning that the user likely entered an incomplete line of code
683683
* and should be prompted for the next. For example, starting a multi-line for() loop and not finishing it.
684+
* null value means code is always recoverable. `Set` means code is only recoverable when occurring alongside at least one
685+
* of the other codes.
684686
*/
685-
const RECOVERY_CODES: Set<number> = new Set([
686-
1003, // "Identifier expected."
687-
1005, // "')' expected."
688-
1109, // "Expression expected."
689-
1126, // "Unexpected end of text."
690-
1160, // "Unterminated template literal."
691-
1161, // "Unterminated regular expression literal."
692-
2355, // "A function whose declared type is neither 'void' nor 'any' must return a value."
687+
const RECOVERY_CODES: Map<number, Set<number> | null> = new Map([
688+
[1003, null], // "Identifier expected."
689+
[1005, null], // "')' expected.", "'}' expected."
690+
[1109, null], // "Expression expected."
691+
[1126, null], // "Unexpected end of text."
692+
[1160, null], // "Unterminated template literal."
693+
[1161, null], // "Unterminated regular expression literal."
694+
[2355, null], // "A function whose declared type is neither 'void' nor 'any' must return a value."
695+
[2391, null], // "Function implementation is missing or not immediately following the declaration."
696+
[
697+
7010, // "Function, which lacks return-type annotation, implicitly has an 'any' return type."
698+
new Set([1005]), // happens when fn signature spread across multiple lines: 'function a(\nb: any\n) {'
699+
],
693700
]);
694701

695702
/**
@@ -708,7 +715,13 @@ const topLevelAwaitDiagnosticCodes = [
708715
* Check if a function can recover gracefully.
709716
*/
710717
function isRecoverable(error: TSError) {
711-
return error.diagnosticCodes.every((code) => RECOVERY_CODES.has(code));
718+
return error.diagnosticCodes.every((code) => {
719+
const deps = RECOVERY_CODES.get(code);
720+
return (
721+
deps === null ||
722+
(deps && error.diagnosticCodes.some((code) => deps.has(code)))
723+
);
724+
});
712725
}
713726

714727
/**

src/test/helpers.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { Readable } from 'stream';
1212
*/
1313
import type * as tsNodeTypes from '../index';
1414
import type _createRequire from 'create-require';
15-
import { has, once } from 'lodash';
15+
import { has, mapValues, once } from 'lodash';
1616
import semver = require('semver');
1717
const createRequire: typeof _createRequire = require('create-require');
1818
export { tsNodeTypes };
@@ -218,25 +218,29 @@ export function resetNodeEnvironment() {
218218
resetObject(require('module'), defaultModule);
219219

220220
// May be modified by REPL tests, since the REPL sets globals.
221-
resetObject(global, defaultGlobal);
221+
// Avoid deleting nyc's coverage data.
222+
resetObject(global, defaultGlobal, ['__coverage__']);
222223
}
223224

224225
function captureObjectState(object: any) {
226+
const descriptors = Object.getOwnPropertyDescriptors(object);
227+
const values = mapValues(descriptors, (_d, key) => object[key]);
225228
return {
226-
descriptors: Object.getOwnPropertyDescriptors(object),
227-
values: { ...object },
229+
descriptors,
230+
values,
228231
};
229232
}
230233
// Redefine all property descriptors and delete any new properties
231234
function resetObject(
232235
object: any,
233-
state: ReturnType<typeof captureObjectState>
236+
state: ReturnType<typeof captureObjectState>,
237+
doNotDeleteTheseKeys: string[] = []
234238
) {
235239
const currentDescriptors = Object.getOwnPropertyDescriptors(object);
236240
for (const key of Object.keys(currentDescriptors)) {
237-
if (!has(state.descriptors, key)) {
238-
delete object[key];
239-
}
241+
if (doNotDeleteTheseKeys.includes(key)) continue;
242+
if (has(state.descriptors, key)) continue;
243+
delete object[key];
240244
}
241245
// Trigger nyc's setter functions
242246
for (const [key, value] of Object.entries(state.values)) {

src/test/repl/helpers.ts

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as promisify from 'util.promisify';
22
import { PassThrough } from 'stream';
33
import { getStream, TEST_DIR, tsNodeTypes } from '../helpers';
44
import type { ExecutionContext } from 'ava';
5+
import { expect, TestInterface } from '../testlib';
56

67
export interface ContextWithTsNodeUnderTest {
78
tsNodeUnderTest: Pick<
@@ -10,12 +11,24 @@ export interface ContextWithTsNodeUnderTest {
1011
>;
1112
}
1213

14+
export type ContextWithReplHelpers = ContextWithTsNodeUnderTest &
15+
Awaited<ReturnType<typeof contextReplHelpers>>;
16+
1317
export interface CreateReplViaApiOptions {
14-
registerHooks: true;
18+
registerHooks: boolean;
1519
createReplOpts?: Partial<tsNodeTypes.CreateReplOptions>;
1620
createServiceOpts?: Partial<tsNodeTypes.CreateOptions>;
1721
}
1822

23+
export interface ExecuteInReplOptions extends CreateReplViaApiOptions {
24+
waitMs?: number;
25+
waitPattern?: string | RegExp;
26+
/** When specified, calls `startInternal` instead of `start` and passes options */
27+
startInternalOptions?: Parameters<
28+
tsNodeTypes.ReplService['startInternal']
29+
>[0];
30+
}
31+
1932
/**
2033
* pass to test.context() to get REPL testing helper functions
2134
*/
@@ -55,18 +68,7 @@ export async function contextReplHelpers(
5568
return { stdin, stdout, stderr, replService, service };
5669
}
5770

58-
// Todo combine with replApiMacro
59-
async function executeInRepl(
60-
input: string,
61-
options: CreateReplViaApiOptions & {
62-
waitMs?: number;
63-
waitPattern?: string | RegExp;
64-
/** When specified, calls `startInternal` instead of `start` and passes options */
65-
startInternalOptions?: Parameters<
66-
tsNodeTypes.ReplService['startInternal']
67-
>[0];
68-
}
69-
) {
71+
async function executeInRepl(input: string, options: ExecuteInReplOptions) {
7072
const {
7173
waitPattern,
7274
// Wait longer if there's a signal to end it early
@@ -102,3 +104,53 @@ export async function contextReplHelpers(
102104
};
103105
}
104106
}
107+
108+
export function replMacros<T extends ContextWithReplHelpers>(
109+
_test: TestInterface<T>
110+
) {
111+
return { noErrorsAndStdoutContains, stderrContains };
112+
113+
function noErrorsAndStdoutContains(
114+
title: string,
115+
script: string,
116+
contains: string,
117+
options?: Partial<ExecuteInReplOptions>
118+
) {
119+
testReplInternal(title, script, contains, undefined, contains, options);
120+
}
121+
function stderrContains(
122+
title: string,
123+
script: string,
124+
errorContains: string,
125+
options?: Partial<ExecuteInReplOptions>
126+
) {
127+
testReplInternal(
128+
title,
129+
script,
130+
undefined,
131+
errorContains,
132+
errorContains,
133+
options
134+
);
135+
}
136+
function testReplInternal(
137+
title: string,
138+
script: string,
139+
stdoutContains: string | undefined,
140+
stderrContains: string | undefined,
141+
waitPattern: string,
142+
options?: Partial<ExecuteInReplOptions>
143+
) {
144+
_test(title, async (t) => {
145+
const { stdout, stderr } = await t.context.executeInRepl(script, {
146+
registerHooks: true,
147+
startInternalOptions: { useGlobal: false },
148+
waitPattern,
149+
...options,
150+
});
151+
if (stderrContains) expect(stderr).toContain(stderrContains);
152+
else expect(stderr).toBe('');
153+
if (stdoutContains) expect(stdout).toContain(stdoutContains);
154+
});
155+
}
156+
}

src/test/repl/node-repl-tla.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export async function upstreamTopLevelAwaitTests({
6464
return promise;
6565
}
6666

67-
runAndWait([
67+
await runAndWait([
6868
'function foo(x) { return x; }',
6969
'function koo() { return Promise.resolve(4); }',
7070
]);

0 commit comments

Comments
 (0)