Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
74 changes: 43 additions & 31 deletions packages/mongodb-ts-autocomplete/src/autocompleter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MongoDBAutocompleter } from './index';
import type { AutocompletionContext } from './autocompletion-context';
import { analyzeDocuments } from 'mongodb-schema';
import { expect } from 'chai';
import { relativeNodePath } from '@mongodb-js/ts-autocomplete';

/*
This is intended as deliberately diabolical database and collection names to
Expand All @@ -25,6 +26,7 @@ describe('MongoDBAutocompleter', function () {
let fallbackServiceHost: ts.LanguageServiceHost;
let autocompleterContext: AutocompletionContext;
let autocompleter: MongoDBAutocompleter;
let autocompleterWithFallback: MongoDBAutocompleter;
let encounteredPaths: EncounteredPaths;

beforeEach(function () {
Expand All @@ -51,19 +53,19 @@ describe('MongoDBAutocompleter', function () {
ts.sys.readFile(fileName) || '',
);

encounteredPaths.getScriptSnapshot.push(fileName);
encounteredPaths.getScriptSnapshot.push(relativeNodePath(fileName));
return result;
},
fileExists: (fileName: string) => {
const result = ts.sys.fileExists(fileName);
if (result) {
encounteredPaths.fileExists.push(fileName);
encounteredPaths.fileExists.push(relativeNodePath(fileName));
}
return result;
},
readFile: (fileName: string) => {
const result = ts.sys.readFile(fileName);
encounteredPaths.readFile.push(fileName);
encounteredPaths.readFile.push(relativeNodePath(fileName));
return result;
},
readDirectory: (...args) => {
Expand Down Expand Up @@ -126,11 +128,21 @@ describe('MongoDBAutocompleter', function () {

autocompleter = new MongoDBAutocompleter({
context: autocompleterContext,
});

autocompleterWithFallback = new MongoDBAutocompleter({
context: autocompleterContext,
fallbackServiceHost,
});
});

afterEach(function () {
it('autocompletes', async function () {
Copy link
Collaborator Author

@lerouxb lerouxb Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The best way to fully test/prove this automatically (ie. that it can recreate the list) is do comment out deps in extract-types.ts to make it just an empty structure, run extract-types so none of the @types/node and lib files are in there, then run this test. It then finds the entire list.

I tried modifying extract-types to calculate the list automatically by running autocomplete, but it becomes a bit code-surgery with things passed down just for testing and a chicken and egg problem of extract-types using autocomplete which imports the extracted types which aren't generated yet... I tried to fix it with dynamic import, but then TS doesn't want to import the code from the script and it just becomes a rabbit hole.

I think this test that lists what's missing even if you start over with an empty list and the rest of the tests fail because they don't use the fallback is probably good enough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I have now added a version of this as a comment next to the test.)

await autocompleterWithFallback.autocomplete('db.foo.find({ fo');

encounteredPaths.fileExists.sort();
encounteredPaths.getScriptSnapshot.sort();
encounteredPaths.readFile.sort();

// this is what tells us what we're missing in extract-types.ts
expect(encounteredPaths).to.deep.equal({
fileExists: [],
Expand All @@ -139,33 +151,6 @@ describe('MongoDBAutocompleter', function () {
});
});

it('deals with no connection', async function () {
// The body of tests are all wrapped in loops so that we exercise the
// caching logic in the autocompleter.
for (let i = 0; i < 2; i++) {
autocompleterContext.currentDatabaseAndConnection = () => {
return undefined;
};

const completions = await autocompleter.autocomplete('db.');
expect(completions).to.deep.equal([]);
}
});

it('does not leak the bson package', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('bson.');
expect(completions).to.deep.equal([]);
}
});

it('does not leak the ShellAPI package', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('ShellAPI.');
expect(completions).to.deep.equal([]);
}
});

it('completes a bson expression', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('Ob');
Expand Down Expand Up @@ -285,6 +270,33 @@ describe('MongoDBAutocompleter', function () {
]);
});

it('deals with no connection', async function () {
// The body of tests are all wrapped in loops so that we exercise the
// caching logic in the autocompleter.
for (let i = 0; i < 2; i++) {
autocompleterContext.currentDatabaseAndConnection = () => {
return undefined;
};

const completions = await autocompleter.autocomplete('db.');
expect(completions).to.deep.equal([]);
}
});

it('does not leak the bson package', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('bson.');
expect(completions).to.deep.equal([]);
}
});

it('does not leak the ShellAPI package', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('ShellAPI.');
expect(completions).to.deep.equal([]);
}
});

describe('getConnectionSchemaCode', function () {
it('generates code for a connection', async function () {
const docs = [
Expand Down
23 changes: 12 additions & 11 deletions packages/ts-autocomplete/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type UpdateDefinitionFunction = (
newDef: Record<TypeFilename, string | boolean>,
) => void;

function relativeNodePath(fileName: string): string {
export function relativeNodePath(fileName: string): string {
const parts = fileName.split(/\/node_modules\//g);
if (parts.length === 1 && fileName.endsWith('package.json')) {
// special case: when it looks up this package itself it isn't going to find
Expand Down Expand Up @@ -65,13 +65,14 @@ function getVirtualLanguageService(
return (versions[fileName] ?? 1).toString();
},
getScriptSnapshot: (fileName) => {
fileName = relativeNodePath(fileName);
if (fileName in codeHolder) {
const relativeFileName = relativeNodePath(fileName);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually a bug - I was passing the relative filename to the fallback resolver and that needs the original absolute filename so it can actually load it from disk.


if (relativeFileName in codeHolder) {
// if its a boolean rather than code, just return a blank string if for
// some reason we ever get here.
const code =
typeof codeHolder[fileName] === 'string'
? (codeHolder[fileName] as string)
typeof codeHolder[relativeFileName] === 'string'
? (codeHolder[relativeFileName] as string)
: '';
return ts.ScriptSnapshot.fromString(code);
}
Expand All @@ -86,8 +87,8 @@ function getVirtualLanguageService(
return ts.getDefaultLibFilePath(options);
},
fileExists: (fileName) => {
fileName = relativeNodePath(fileName);
if (fileName in codeHolder) {
const relativeFileName = relativeNodePath(fileName);
if (relativeFileName in codeHolder) {
return true;
}

Expand All @@ -98,13 +99,13 @@ function getVirtualLanguageService(
return false;
},
readFile: (fileName) => {
fileName = relativeNodePath(fileName);
if (fileName in codeHolder) {
const relativeFileName = relativeNodePath(fileName);
if (relativeFileName in codeHolder) {
// if its a boolean rather than code, just return a blank string if for
// some reason we ever get here.
const code =
typeof codeHolder[fileName] === 'string'
? (codeHolder[fileName] as string)
typeof codeHolder[relativeFileName] === 'string'
? (codeHolder[relativeFileName] as string)
: undefined;
return code;
}
Expand Down
Loading