Skip to content

Commit a280bf6

Browse files
authored
chore(cli-repl): clean up autocomplete implementation and fix flaky test MONGOSH-1908 MONGOSH-1960 (#2317)
* chore(cli-repl): clean autocomplete implementation up a bit Best viewed as a whitespace-diff-disabled change, doesn't include any functional work. * chore(cli-repl): fix flaky autocompletion test MONGOSH-1960 Our current autocomplete logic does not always wait for the collection or database name caches to be filled, in case it takes longer than 200ms to do so. By ensuring that the caches are filled before we run the test, we get consistent results. * chore(cli-repl,shell-api): filter autocompletions for CCs MONGOSH-1908 Collection and database names, as well as other completion items, can mostly contain arbitrary characters with only a few exceptions. Since some characters may mess with our REPL input, we should filter those autocompletions out.
1 parent abe83bb commit a280bf6

File tree

3 files changed

+89
-45
lines changed

3 files changed

+89
-45
lines changed

packages/cli-repl/src/cli-repl.spec.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2365,7 +2365,7 @@ describe('CliRepl', function () {
23652365
});
23662366

23672367
it('includes collection names', async function () {
2368-
if (!hasCollectionNames) return;
2368+
if (!hasCollectionNames) return this.skip();
23692369
const collname = `testcollection${Date.now()}${
23702370
(Math.random() * 1000) | 0
23712371
}`;
@@ -2413,7 +2413,10 @@ describe('CliRepl', function () {
24132413
});
24142414

24152415
it('completes use <db>', async function () {
2416-
if (!hasDatabaseNames) return;
2416+
if (!hasDatabaseNames) return this.skip();
2417+
input.write('db.getMongo()._listDatabases()\n'); // populate database cache
2418+
await waitEval(cliRepl.bus);
2419+
24172420
input.write('use adm');
24182421
await tab();
24192422
await waitCompletion(cliRepl.bus);
@@ -2428,13 +2431,14 @@ describe('CliRepl', function () {
24282431
});
24292432

24302433
it('completes properties of shell API result types', async function () {
2431-
if (!hasCollectionNames) return;
2434+
if (!hasCollectionNames) return this.skip();
2435+
24322436
input.write(
24332437
'res = db.autocompleteTestColl.deleteMany({ deletetestdummykey: 1 })\n'
24342438
);
24352439
await waitEval(cliRepl.bus);
24362440

2437-
// Consitency check: The result actually has a shell API type tag:
2441+
// Consistency check: The result actually has a shell API type tag:
24382442
output = '';
24392443
input.write('res[Symbol.for("@@mongosh.shellApiType")]\n');
24402444
await waitEval(cliRepl.bus);
@@ -2445,6 +2449,24 @@ describe('CliRepl', function () {
24452449
await waitCompletion(cliRepl.bus);
24462450
expect(output).to.include('res.acknowledged');
24472451
});
2452+
2453+
it('completes only collection names that do not include control characters', async function () {
2454+
if (!hasCollectionNames) return this.skip();
2455+
2456+
input.write(
2457+
'db["actestcoll1"].insertOne({}); db["actestcoll2\\x1bfooobar"].insertOne({})\n'
2458+
);
2459+
await waitEval(cliRepl.bus);
2460+
input.write('db._getCollectionNames()\n'); // populate collection name cache
2461+
await waitEval(cliRepl.bus);
2462+
2463+
output = '';
2464+
input.write('db.actestc');
2465+
await tabtab();
2466+
await waitCompletion(cliRepl.bus);
2467+
expect(output).to.include('db.actestcoll1');
2468+
expect(output).to.not.include('db.actestcoll2');
2469+
});
24482470
});
24492471
}
24502472

packages/cli-repl/src/mongosh-repl.ts

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import { Script, createContext, runInContext } from 'vm';
4545

4646
declare const __non_webpack_require__: any;
4747

48+
// eslint-disable-next-line no-control-regex
49+
const CONTROL_CHAR_REGEXP = /[\x00-\x1F\x7F-\x9F]/g;
50+
4851
/**
4952
* All CLI flags that are useful for {@link MongoshNodeRepl}.
5053
*/
@@ -388,52 +391,64 @@ class MongoshNodeRepl implements EvaluationListener {
388391
null,
389392
instanceState.getAutocompleteParameters()
390393
);
391-
(repl as Mutable<typeof repl>).completer = callbackify(
394+
const innerCompleter = async (
395+
text: string
396+
): Promise<[string[], string]> => {
397+
// Merge the results from the repl completer and the mongosh completer.
398+
const [
399+
[replResults, replOrig],
400+
[mongoshResults, , mongoshResultsExclusive],
401+
] = await Promise.all([
402+
(async () => (await origReplCompleter(text)) || [[]])(),
403+
(async () => await mongoshCompleter(text))(),
404+
]);
405+
this.bus.emit('mongosh:autocompletion-complete'); // For testing.
406+
407+
// Sometimes the mongosh completion knows that what it is doing is right,
408+
// and that autocompletion based on inspecting the actual objects that
409+
// are being accessed will not be helpful, e.g. in `use a<tab>`, we know
410+
// that we want *only* database names and not e.g. `assert`.
411+
if (mongoshResultsExclusive) {
412+
return [mongoshResults, text];
413+
}
414+
415+
// The REPL completer may not complete the entire string; for example,
416+
// when completing ".ed" to ".editor", it reports as having completed
417+
// only the last piece ("ed"), or when completing "{ $g", it completes
418+
// only "$g" and not the entire result.
419+
// The mongosh completer always completes on the entire string.
420+
// In order to align them, we always extend the REPL results to include
421+
// the full string prefix.
422+
const replResultPrefix = replOrig
423+
? text.substr(0, text.lastIndexOf(replOrig))
424+
: '';
425+
const longReplResults = replResults.map(
426+
(result: string) => replResultPrefix + result
427+
);
428+
429+
// Remove duplicates, because shell API methods might otherwise show
430+
// up in both completions.
431+
const deduped = [...new Set([...longReplResults, ...mongoshResults])];
432+
433+
return [deduped, text];
434+
};
435+
436+
const nodeReplCompleter = callbackify(
392437
async (text: string): Promise<[string[], string]> => {
393438
this.insideAutoCompleteOrGetPrompt = true;
394439
try {
395-
// Merge the results from the repl completer and the mongosh completer.
396-
const [
397-
[replResults, replOrig],
398-
[mongoshResults, , mongoshResultsExclusive],
399-
] = await Promise.all([
400-
(async () => (await origReplCompleter(text)) || [[]])(),
401-
(async () => await mongoshCompleter(text))(),
402-
]);
403-
this.bus.emit('mongosh:autocompletion-complete'); // For testing.
404-
405-
// Sometimes the mongosh completion knows that what it is doing is right,
406-
// and that autocompletion based on inspecting the actual objects that
407-
// are being accessed will not be helpful, e.g. in `use a<tab>`, we know
408-
// that we want *only* database names and not e.g. `assert`.
409-
if (mongoshResultsExclusive) {
410-
return [mongoshResults, text];
411-
}
412-
413-
// The REPL completer may not complete the entire string; for example,
414-
// when completing ".ed" to ".editor", it reports as having completed
415-
// only the last piece ("ed"), or when completing "{ $g", it completes
416-
// only "$g" and not the entire result.
417-
// The mongosh completer always completes on the entire string.
418-
// In order to align them, we always extend the REPL results to include
419-
// the full string prefix.
420-
const replResultPrefix = replOrig
421-
? text.substr(0, text.lastIndexOf(replOrig))
422-
: '';
423-
const longReplResults = replResults.map(
424-
(result: string) => replResultPrefix + result
440+
// eslint-disable-next-line prefer-const
441+
let [results, completeOn] = await innerCompleter(text);
442+
results = results.filter(
443+
(result) => !CONTROL_CHAR_REGEXP.test(result)
425444
);
426-
427-
// Remove duplicates, because shell API methods might otherwise show
428-
// up in both completions.
429-
const deduped = [...new Set([...longReplResults, ...mongoshResults])];
430-
431-
return [deduped, text];
445+
return [results, completeOn];
432446
} finally {
433447
this.insideAutoCompleteOrGetPrompt = false;
434448
}
435449
}
436450
);
451+
(repl as Mutable<typeof repl>).completer = nodeReplCompleter;
437452

438453
// This is used below for multiline history manipulation.
439454
let originalHistory: string[] | null = null;

packages/shell-api/src/shell-instance-state.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ export interface ShellPlugin {
126126
transformError?: (err: Error) => Error;
127127
}
128128

129+
// eslint-disable-next-line no-control-regex
130+
const CONTROL_CHAR_REGEXP = /[\x00-\x1F\x7F-\x9F]/g;
131+
129132
/**
130133
* Anything to do with the state of the shell API and API objects is stored here.
131134
*
@@ -452,8 +455,10 @@ export default class ShellInstanceState {
452455
try {
453456
const collectionNames =
454457
await this.currentDb._getCollectionNamesForCompletion();
455-
return collectionNames.filter((name) =>
456-
name.toLowerCase().startsWith(collName.toLowerCase())
458+
return collectionNames.filter(
459+
(name) =>
460+
name.toLowerCase().startsWith(collName.toLowerCase()) &&
461+
!CONTROL_CHAR_REGEXP.test(name)
457462
);
458463
} catch (err: any) {
459464
if (
@@ -469,8 +474,10 @@ export default class ShellInstanceState {
469474
try {
470475
const dbNames =
471476
await this.currentDb._mongo._getDatabaseNamesForCompletion();
472-
return dbNames.filter((name) =>
473-
name.toLowerCase().startsWith(dbName.toLowerCase())
477+
return dbNames.filter(
478+
(name) =>
479+
name.toLowerCase().startsWith(dbName.toLowerCase()) &&
480+
!CONTROL_CHAR_REGEXP.test(name)
474481
);
475482
} catch (err: any) {
476483
if (

0 commit comments

Comments
 (0)