src/goTest: fix strict type errors

For golang/vscode-go#57.

Change-Id: I13bae5ae849fae74cabcf7fe2fe88145320d95b8
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/401617
Reviewed-by: Benny Siegert <bsiegert@gmail.com>
Run-TryBot: Jamal Carvalho <jamal@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/src/goTest/explore.ts b/src/goTest/explore.ts
index 6bf28bd..67facfa 100644
--- a/src/goTest/explore.ts
+++ b/src/goTest/explore.ts
@@ -96,7 +96,7 @@
 				if (!options) return;
 
 				try {
-					await inst.runner.run(new TestRunRequest([item]), null, options);
+					await inst.runner.run(new TestRunRequest([item]), undefined, options);
 				} catch (error) {
 					const m = 'Failed to execute tests';
 					outputChannel.appendLine(`${m}: ${error}`);
@@ -242,7 +242,7 @@
 				return;
 			}
 
-			const ws = this.workspace.getWorkspaceFolder(item.uri);
+			const ws = item.uri && this.workspace.getWorkspaceFolder(item.uri);
 			if (!ws) {
 				dispose(this.resolver, item);
 			}
@@ -255,13 +255,13 @@
 
 	protected async didDeleteFile(file: Uri) {
 		const id = GoTest.id(file, 'file');
-		function find(children: TestItemCollection): TestItem {
+		function find(children: TestItemCollection): TestItem | undefined {
 			return findItem(children, (item) => {
 				if (item.id === id) {
 					return item;
 				}
 
-				if (!file.path.startsWith(item.uri.path)) {
+				if (!item.uri || !file.path.startsWith(item.uri.path)) {
 					return;
 				}
 
@@ -272,6 +272,8 @@
 		const found = find(this.ctrl.items);
 		if (found) {
 			dispose(this.resolver, found);
+		}
+		if (found?.parent) {
 			disposeIfEmpty(this.resolver, found.parent);
 		}
 	}
diff --git a/src/goTest/profile.ts b/src/goTest/profile.ts
index 8d49511..78e57c9 100644
--- a/src/goTest/profile.ts
+++ b/src/goTest/profile.ts
@@ -53,12 +53,12 @@
 	}
 
 	preRun(options: ProfilingOptions, item: TestItem): string[] {
-		const kind = Kind.get(options.kind);
+		const kind = options.kind && Kind.get(options.kind);
 		if (!kind) return [];
 
 		const run = new File(kind, item);
 		const flags = [...run.flags];
-		if (this.runs.has(item.id)) this.runs.get(item.id).unshift(run);
+		if (this.runs.has(item.id)) this.runs.get(item.id)?.unshift(run);
 		else this.runs.set(item.id, [run]);
 		return flags;
 	}
@@ -76,12 +76,13 @@
 	}
 
 	async configure(): Promise<ProfilingOptions | undefined> {
-		const { profilekind } = await vscode.window.showQuickPick(
-			Kind.all.map((x) => ({ label: x.label, profilekind: x })),
-			{
-				title: 'Profile'
-			}
-		);
+		const { profilekind } =
+			(await vscode.window.showQuickPick(
+				Kind.all.map((x) => ({ label: x.label, profilekind: x })),
+				{
+					title: 'Profile'
+				}
+			)) ?? {};
 		if (!profilekind) return;
 
 		return {
@@ -125,13 +126,13 @@
 	get tests() {
 		const items = Array.from(this.runs.keys());
 		items.sort((a: string, b: string) => {
-			const aWhen = this.runs.get(a)[0].when.getTime();
-			const bWhen = this.runs.get(b)[0].when.getTime();
+			const aWhen = this.runs.get(a)?.[0].when.getTime() ?? 0;
+			const bWhen = this.runs.get(b)?.[0].when.getTime() ?? 0;
 			return bWhen - aWhen;
 		});
 
 		// Filter out any tests that no longer exist
-		return items.map((x) => this.resolver.all.get(x)).filter((x) => x);
+		return items.map((x) => this.resolver.all.get(x)).filter((x): x is TestItem => !!x);
 	}
 
 	// Profiles associated with the given test
@@ -168,7 +169,7 @@
 	const proc = spawn(getBinPath('go'), ['tool', 'pprof', '-http=:', '-no_browser', profile]);
 	pprofProcesses.add(proc);
 
-	const port = await new Promise<string>((resolve, reject) => {
+	const port = await new Promise<string | undefined>((resolve, reject) => {
 		proc.on('error', (err) => {
 			pprofProcesses.delete(proc);
 			reject(err);
@@ -186,7 +187,7 @@
 			const m = stderr.match(/^Serving web UI on http:\/\/localhost:(?<port>\d+)\n/);
 			if (!m) return;
 
-			resolve(m.groups.port);
+			resolve(m.groups?.port);
 			proc.stdout.off('data', captureStdout);
 		}
 
@@ -307,7 +308,7 @@
 		item.contextValue = 'go:test:test';
 		const options: TextDocumentShowOptions = {
 			preserveFocus: false,
-			selection: new Range(element.range.start, element.range.start)
+			selection: element.range && new Range(element.range.start, element.range.start)
 		};
 		item.command = {
 			title: 'Go to test',
diff --git a/src/goTest/resolve.ts b/src/goTest/resolve.ts
index e208482..b096657 100644
--- a/src/goTest/resolve.ts
+++ b/src/goTest/resolve.ts
@@ -26,7 +26,7 @@
 import { walk, WalkStop } from './walk';
 import { importsTestify } from '../testUtils';
 
-export type ProvideSymbols = (doc: TextDocument, token: CancellationToken) => Thenable<DocumentSymbol[]>;
+export type ProvideSymbols = (doc: TextDocument, token?: CancellationToken) => Thenable<DocumentSymbol[]>;
 
 const testFuncRegex = /^(?<name>(?<kind>Test|Benchmark|Example|Fuzz)($|\P{Ll}.*))/u;
 const testMethodRegex = /^\(\*(?<type>[^)]+)\)\.(?<name>(?<kind>Test)($|\P{Ll}.*))$/u;
@@ -77,7 +77,7 @@
 					return;
 				}
 
-				if (this.workspace.getWorkspaceFolder(item.uri)) {
+				if (item.uri && this.workspace.getWorkspaceFolder(item.uri)) {
 					dispose(this, item);
 				}
 			});
@@ -105,6 +105,7 @@
 
 		const { kind } = GoTest.parseId(item.id);
 
+		if (!item.uri) return;
 		// The user expanded a module or workspace - find all packages
 		if (kind === 'module' || kind === 'workspace') {
 			await walkPackages(this.workspace.fs, item.uri, async (uri) => {
@@ -140,11 +141,11 @@
 
 		function find(items: TestItemCollection) {
 			items.forEach((item) => {
-				const itemStr = item.uri.toString();
+				const itemStr = item.uri?.toString();
 				if (findStr === itemStr) {
 					found.push(item);
 					find(item.children);
-				} else if (findStr.startsWith(itemStr)) {
+				} else if (itemStr && findStr.startsWith(itemStr)) {
 					find(item.children);
 				}
 			});
@@ -170,7 +171,8 @@
 
 	// Create or Retrieve a sub test or benchmark. The ID will be of the form:
 	//     file:///path/to/mod/file.go?test#TestXxx%2fA%2fB%2fC
-	getOrCreateSubTest(item: TestItem, label: string, name: string, dynamic?: boolean): TestItem {
+	getOrCreateSubTest(item: TestItem, label: string, name: string, dynamic?: boolean): TestItem | undefined {
+		if (!item.uri) return;
 		const { kind } = GoTest.parseId(item.id);
 
 		let existing: TestItem | undefined;
@@ -202,7 +204,7 @@
 	async processDocument(doc: TextDocument, ranges?: Range[]) {
 		const seen = new Set<string>();
 		const item = await this.getFile(doc.uri);
-		const symbols = await this.provideDocumentSymbols(doc, null);
+		const symbols = await this.provideDocumentSymbols(doc);
 		const testify = importsTestify(symbols);
 		for (const symbol of symbols) {
 			await this.processSymbol(doc, item, seen, testify, symbol);
@@ -210,12 +212,12 @@
 
 		item.children.forEach((child) => {
 			const { name } = GoTest.parseId(child.id);
-			if (!seen.has(name)) {
+			if (!name || !seen.has(name)) {
 				dispose(this, child);
 				return;
 			}
 
-			if (ranges?.some((r) => !!child.range.intersection(r))) {
+			if (ranges?.some((r) => !!child.range?.intersection(r))) {
 				item.children.forEach((x) => dispose(this, x));
 			}
 		});
@@ -235,7 +237,7 @@
 
 	private shouldSetRange(item: TestItem): boolean {
 		const config = getGoConfig(item.uri);
-		return config.get<boolean>('testExplorer.showDynamicSubtestsInEditor');
+		return !!config.get<boolean>('testExplorer.showDynamicSubtestsInEditor');
 	}
 
 	// Create an item.
@@ -247,7 +249,7 @@
 	}
 
 	// Retrieve an item.
-	private getItem(parent: TestItem | undefined, uri: Uri, kind: GoTestKind, name?: string): TestItem {
+	private getItem(parent: TestItem | undefined, uri: Uri, kind: GoTestKind, name?: string): TestItem | undefined {
 		return (parent?.children || this.ctrl.items).get(GoTest.id(uri, kind, name));
 	}
 
@@ -281,7 +283,7 @@
 
 	// Retrieve or create an item for a Go module.
 	private async getModule(uri: Uri): Promise<TestItem> {
-		const existing = this.getItem(null, uri, 'module');
+		const existing = this.getItem(undefined, uri, 'module');
 		if (existing) {
 			return existing;
 		}
@@ -292,50 +294,54 @@
 
 		// Use the module name as the label
 		const label = findModuleName(contents.toString());
-		const item = this.getOrCreateItem(null, label, uri, 'module');
+		const item = this.getOrCreateItem(undefined, label, uri, 'module');
 		item.canResolveChildren = true;
 		return item;
 	}
 
 	// Retrieve or create an item for a workspace folder that is not a module.
 	private async getWorkspace(ws: WorkspaceFolder): Promise<TestItem> {
-		const existing = this.getItem(null, ws.uri, 'workspace');
+		const existing = this.getItem(undefined, ws.uri, 'workspace');
 		if (existing) {
 			return existing;
 		}
 
 		// Use the workspace folder name as the label
-		const item = this.getOrCreateItem(null, ws.name, ws.uri, 'workspace');
+		const item = this.getOrCreateItem(undefined, ws.name, ws.uri, 'workspace');
 		item.canResolveChildren = true;
 		return item;
 	}
 
 	// Retrieve or create an item for a Go package.
-	private async getPackage(uri: Uri): Promise<TestItem> {
+	private async getPackage(uri: Uri): Promise<TestItem | undefined> {
 		let item: TestItem;
 
 		const nested = getGoConfig(uri).get('testExplorer.packageDisplayMode') === 'nested';
-		const modDir = Uri.file(await getModFolderPath(uri, true)); // TODO support non-file schemes
+		const modDirPath = await getModFolderPath(uri, true);
 		const wsfolder = workspace.getWorkspaceFolder(uri);
-		if (modDir) {
+		if (modDirPath) {
+			const modDir = Uri.file(modDirPath); // TODO support non-file schemes
 			// If the package is in a module, add it as a child of the module
 			let parent = await this.getModule(modDir);
-			if (uri.path === parent.uri.path) {
+			if (uri.path === parent.uri?.path) {
 				return parent;
 			}
 
 			if (nested) {
-				const bits = path.relative(parent.uri.path, uri.path).split(path.sep);
+				const bits = parent.uri ? path.relative(parent.uri.path, uri.path).split(path.sep) : [];
 				while (bits.length > 1) {
+					if (!parent.uri?.path) continue;
 					const dir = bits.shift();
+					if (!dir) continue;
 					const dirUri = uri.with({ path: path.join(parent.uri.path, dir), query: '', fragment: '' });
 					parent = this.getOrCreateItem(parent, dir, dirUri, 'package');
 				}
 			}
 
-			const label = uri.path.startsWith(parent.uri.path)
-				? uri.path.substring(parent.uri.path.length + 1)
-				: uri.path;
+			const label =
+				parent.uri && uri.path.startsWith(parent.uri.path)
+					? uri.path.substring(parent.uri.path.length + 1)
+					: uri.path;
 			item = this.getOrCreateItem(parent, label, uri, 'package');
 		} else if (wsfolder) {
 			// If the package is in a workspace folder, add it as a child of the workspace
@@ -351,14 +357,14 @@
 			item = this.getOrCreateItem(workspace, label, uri, 'package');
 		} else {
 			// Otherwise, add it directly to the root
-			const existing = this.getItem(null, uri, 'package');
+			const existing = this.getItem(undefined, uri, 'package');
 			if (existing) {
 				return existing;
 			}
 
 			const srcPath = path.join(getCurrentGoPath(uri), 'src');
 			const label = uri.path.startsWith(srcPath) ? uri.path.substring(srcPath.length + 1) : uri.path;
-			item = this.getOrCreateItem(null, label, uri, 'package');
+			item = this.getOrCreateItem(undefined, label, uri, 'package');
 		}
 
 		item.canResolveChildren = true;
@@ -382,7 +388,7 @@
 
 	private getTestSuite(type: string): TestSuite {
 		if (this.testSuites.has(type)) {
-			return this.testSuites.get(type);
+			return this.testSuites.get(type) as TestSuite;
 		}
 
 		const methods = new Set<TestItem>();
@@ -420,21 +426,27 @@
 
 		seen.add(symbol.name);
 
-		const kind = match.groups.kind.toLowerCase() as GoTestKind;
-		const suite = match.groups.type ? this.getTestSuite(match.groups.type) : undefined;
+		const kind = match.groups?.kind.toLowerCase() as GoTestKind;
+		const suite = match.groups?.type ? this.getTestSuite(match.groups.type) : undefined;
 		const existing =
 			this.getItem(file, doc.uri, kind, symbol.name) ||
 			(suite?.func && this.getItem(suite?.func, doc.uri, kind, symbol.name));
 
 		if (existing) {
-			if (!existing.range.isEqual(symbol.range)) {
+			if (!existing.range?.isEqual(symbol.range)) {
 				existing.range = symbol.range;
 				this.relocateChildren(existing);
 			}
 			return existing;
 		}
 
-		const item = this.getOrCreateItem(suite?.func || file, match.groups.name, doc.uri, kind, symbol.name);
+		const item = this.getOrCreateItem(
+			suite?.func || file,
+			match.groups?.name ?? '<none>',
+			doc.uri,
+			kind,
+			symbol.name
+		);
 		item.range = symbol.range;
 
 		if (suite) {
@@ -460,11 +472,11 @@
 		const matchRunSuite = text.match(runTestSuiteRegex);
 		if (matchRunSuite) {
 			const g = matchRunSuite.groups;
-			const suite = this.getTestSuite(g.type1 || g.type2);
+			const suite = this.getTestSuite(g?.type1 || g?.type2 || '');
 			suite.func = item;
 
 			for (const method of suite.methods) {
-				if (GoTest.parseId(method.parent.id).kind !== 'file') {
+				if (!method.parent || GoTest.parseId(method.parent.id).kind !== 'file') {
 					continue;
 				}
 
diff --git a/src/goTest/run.ts b/src/goTest/run.ts
index 5005243..0fdb9f2 100644
--- a/src/goTest/run.ts
+++ b/src/goTest/run.ts
@@ -36,7 +36,7 @@
 	flags: string[];
 	isMod: boolean;
 	isBenchmark?: boolean;
-	cancel: CancellationToken;
+	cancel?: CancellationToken;
 
 	run: TestRun;
 	options: ProfilingOptions;
@@ -150,7 +150,8 @@
 		}
 
 		const test = tests[0].item;
-		const { kind, name } = GoTest.parseId(test.id);
+		const { kind, name = '' } = GoTest.parseId(test.id);
+		if (!test.uri) return;
 		const doc = await vscode.workspace.openTextDocument(test.uri);
 		await doc.save();
 
@@ -163,7 +164,7 @@
 
 		const id = `debug #${debugSessionID++} ${name}`;
 		const subs: vscode.Disposable[] = [];
-		const sessionPromise = new Promise<DebugSession>((resolve) => {
+		const sessionPromise = new Promise<DebugSession | null>((resolve) => {
 			subs.push(
 				vscode.debug.onDidStartDebugSession((s) => {
 					if (s.configuration.sessionID === id) {
@@ -184,6 +185,7 @@
 		});
 
 		const run = this.ctrl.createTestRun(request, `Debug ${name}`);
+		if (!testFunctions) return;
 		const started = await debugTestAtCursor(doc, name, testFunctions, goConfig, id);
 		if (!started) {
 			subs.forEach((s) => s.dispose());
@@ -197,7 +199,7 @@
 			return;
 		}
 
-		token.onCancellationRequested(() => vscode.debug.stopDebugging(session));
+		token?.onCancellationRequested(() => vscode.debug.stopDebugging(session));
 
 		await new Promise<void>((resolve) => {
 			const sub = vscode.debug.onDidTerminateDebugSession(didTerminateSession);
@@ -208,7 +210,7 @@
 			});
 
 			function didTerminateSession(s: DebugSession) {
-				if (s.id !== session.id) return;
+				if (s.id !== session?.id) return;
 				resolve();
 				sub.dispose();
 			}
@@ -264,6 +266,7 @@
 		let success = true;
 		const subItems: string[] = [];
 		for (const [pkg, items] of collected.entries()) {
+			if (!pkg.uri) continue;
 			const isMod = isInMod(pkg) || (await isModSupported(pkg.uri, true));
 			const goConfig = getGoConfig(pkg.uri);
 			const flags = getTestFlags(goConfig);
@@ -288,7 +291,7 @@
 			const tests: Record<string, TestItem> = {};
 			const benchmarks: Record<string, TestItem> = {};
 			for (const { item, explicitlyIncluded } of items) {
-				const { kind, name } = GoTest.parseId(item.id);
+				const { kind, name = '' } = GoTest.parseId(item.id);
 				if (/[/#]/.test(name)) subItems.push(name);
 
 				// When the user clicks the run button on a package, they expect all
@@ -303,7 +306,7 @@
 					continue;
 				}
 
-				item.error = null;
+				item.error = undefined;
 				run.enqueued(item);
 
 				// Remove subtests created dynamically from test output
@@ -321,7 +324,7 @@
 			}
 
 			const record = new Map<string, string[]>();
-			const concat = goConfig.get<boolean>('testExplorer.concatenateMessages');
+			const concat = !!goConfig.get<boolean>('testExplorer.concatenateMessages');
 
 			// https://github.com/golang/go/issues/39904
 			if (subItems.length > 0 && Object.keys(tests).length + Object.keys(benchmarks).length > 1) {
@@ -417,18 +420,21 @@
 			return;
 		}
 
-		function getFile(item: TestItem): TestItem {
+		function getFile(item: TestItem): TestItem | undefined {
 			const { kind } = GoTest.parseId(item.id);
 			if (kind === 'file') return item;
-			return getFile(item.parent);
+			return item.parent && getFile(item.parent);
 		}
 
 		const file = getFile(item);
-		files.add(file);
+		if (file) {
+			files.add(file);
+		}
 
-		const pkg = file.parent;
+		const pkg = file?.parent;
+		if (!pkg) return;
 		if (functions.has(pkg)) {
-			functions.get(pkg).push({ item, explicitlyIncluded });
+			functions.get(pkg)?.push({ item, explicitlyIncluded });
 		} else {
 			functions.set(pkg, [{ item, explicitlyIncluded }]);
 		}
@@ -452,7 +458,7 @@
 		const success = await goTest({
 			...rest,
 			outputChannel,
-			dir: pkg.uri.fsPath,
+			dir: pkg.uri?.fsPath ?? '',
 			functions: Object.keys(functions),
 			goTestOutputConsumer: rest.isBenchmark
 				? (e) => this.consumeGoBenchmarkEvent(run, functions, complete, e)
@@ -496,11 +502,11 @@
 				return this.resolver.getOrCreateSubTest(parent, name.substring(pos), name, true);
 			}
 
-			const subName = name.substring(0, pos + m.index);
+			const subName = name.substring(0, pos + (m.index ?? 0));
 			const test = parent
-				? this.resolver.getOrCreateSubTest(parent, name.substring(pos, pos + m.index), subName, true)
+				? this.resolver.getOrCreateSubTest(parent, name.substring(pos, pos + (m.index ?? 0)), subName, true)
 				: tests[subName];
-			return resolve(test, pos + m.index, m[0].length);
+			return resolve(test, pos + (m.index ?? 0), m[0].length);
 		};
 
 		return resolve();
@@ -551,14 +557,14 @@
 		}
 
 		// Find (or create) the (sub)benchmark
-		const test = this.resolveTestName(benchmarks, m.groups.name);
+		const test = m.groups && this.resolveTestName(benchmarks, m.groups.name);
 		if (!test) {
 			return;
 		}
 
 		// If output includes benchmark results, the benchmark passed. If output
 		// only includes the benchmark name, the benchmark is running.
-		if (m.groups.result) {
+		if (m.groups?.result) {
 			run.passed(test);
 			complete.add(test);
 			vscode.commands.executeCommand('testing.showMostRecentOutput');
@@ -590,7 +596,7 @@
 		concat: boolean,
 		e: GoTestOutput
 	) {
-		const test = this.resolveTestName(tests, e.Test);
+		const test = e.Test && this.resolveTestName(tests, e.Test);
 		if (!test) {
 			return;
 		}
@@ -609,7 +615,7 @@
 				// TODO(firelizzard18): add messages on pass, once that capability
 				// is added.
 				complete.add(test);
-				run.passed(test, e.Elapsed * 1000);
+				run.passed(test, (e.Elapsed ?? 0) * 1000);
 				break;
 
 			case 'fail': {
@@ -617,21 +623,21 @@
 				const messages = this.parseOutput(test, record.get(test.id) || []);
 
 				if (!concat) {
-					run.failed(test, messages, e.Elapsed * 1000);
+					run.failed(test, messages, (e.Elapsed ?? 0) * 1000);
 					break;
 				}
 
 				const merged = new Map<string, TestMessage>();
 				for (const { message, location } of messages) {
-					const loc = `${location.uri}:${location.range.start.line}`;
+					const loc = `${location?.uri}:${location?.range.start.line}`;
 					if (merged.has(loc)) {
-						merged.get(loc).message += '\n' + message;
+						merged.get(loc)!.message += '\n' + message;
 					} else {
 						merged.set(loc, { message, location });
 					}
 				}
 
-				run.failed(test, Array.from(merged.values()), e.Elapsed * 1000);
+				run.failed(test, Array.from(merged.values()), (e.Elapsed ?? 0) * 1000);
 				break;
 			}
 
@@ -641,12 +647,12 @@
 				break;
 
 			case 'output':
-				if (/^(=== RUN|\s*--- (FAIL|PASS): )/.test(e.Output)) {
+				if (/^(=== RUN|\s*--- (FAIL|PASS): )/.test(e.Output ?? '')) {
 					break;
 				}
 
-				if (record.has(test.id)) record.get(test.id).push(e.Output);
-				else record.set(test.id, [e.Output]);
+				if (record.has(test.id)) record.get(test.id)!.push(e.Output ?? '');
+				else record.set(test.id, [e.Output ?? '']);
 				break;
 		}
 	}
@@ -661,16 +667,19 @@
 			const got = output.slice(gotI + 1, wantI).join('');
 			const want = output.slice(wantI + 1).join('');
 			const message = TestMessage.diff('Output does not match', want, got);
-			message.location = new Location(test.uri, test.range.start);
+			if (test.uri && test.range) {
+				message.location = new Location(test.uri, test.range.start);
+			}
 			messages.push(message);
 			output = output.slice(0, gotI);
 		}
 
-		let current: Location;
-		const dir = Uri.joinPath(test.uri, '..');
+		let current: Location | undefined;
 		for (const line of output) {
 			const m = line.match(/^\s*(?<file>.*\.go):(?<line>\d+): ?(?<message>.*\n)$/);
-			if (m) {
+			if (m?.groups) {
+				if (!test.uri) return [];
+				const dir = Uri.joinPath(test.uri, '..');
 				const file = Uri.joinPath(dir, m.groups.file);
 				const ln = Number(m.groups.line) - 1; // VSCode uses 0-based line numbering (internally)
 				current = new Location(file, new Position(ln, 0));
diff --git a/src/goTest/utils.ts b/src/goTest/utils.ts
index 33040af..de62641 100644
--- a/src/goTest/utils.ts
+++ b/src/goTest/utils.ts
@@ -96,7 +96,7 @@
 
 export function dispose(resolver: GoTestResolver, item: vscode.TestItem) {
 	resolver.all.delete(item.id);
-	item.parent.children.delete(item.id);
+	item.parent?.children.delete(item.id);
 }
 
 // Dispose of the item if it has no children, recursively. This facilitates
@@ -113,7 +113,9 @@
 	}
 
 	dispose(resolver, item);
-	disposeIfEmpty(resolver, item.parent);
+	if (item.parent) {
+		disposeIfEmpty(resolver, item.parent);
+	}
 }
 
 // The 'name' group captures the module name, and the unnamed group ignores any comment that might follow the name.
@@ -124,5 +126,5 @@
 	if (match === null) {
 		throw new Error('failed to find module name in go.mod');
 	}
-	return match.groups.name;
+	return match.groups?.name ?? '';
 }