Repository: OmniSharp/omnisharp-vscode
Pull request #5405 (2022-10-12 02:55:21)

coreclr-debug nullability
This handles the coreclr-debug folder. This is the last src folder that needs to be converted; one more PR will follow to address recent changes to the other folders & enable strict mode for all of src.

Behavior changes:
* DotNetCliError has been removed. We now show the complete error string in both the notification and the output channel. (This isn't strictly necessary - I can always add the error back, just didn't think the overhead of a custom error type was worth it here.)
* getDotnetInfo now throws if version info can't be parsed.
* ❗Fixed dotnetExeName using `path.join` instead of just string concatenation, resulting in a Windows executable of `dotnet\.exe` rather than `dotnet.exe`.
* Converted DotnetInfo from a class to an interface and removed the unused OsVersion property.

Impact:
22 strict violations in src, down from 43 in master. (Fun fact: including test, we're now at 198 strict violations. Below 200!)


Patch:
diff --git a/src/coreclr-debug/ParsedEnvironmentFile.ts b/src/coreclr-debug/ParsedEnvironmentFile.ts
index 01cb585d..0e997bba 100644
--- a/src/coreclr-debug/ParsedEnvironmentFile.ts
+++ b/src/coreclr-debug/ParsedEnvironmentFile.ts
@@ -3,13 +3,13 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import * as fs from 'fs-extra';
+import * as fs from 'fs';
 
 export class ParsedEnvironmentFile {
     public Env: { [key: string]: any };
-    public Warning: string | null;
+    public Warning: string | undefined;
 
-    private constructor(env: { [key: string]: any }, warning: string | null) {
+    private constructor(env: { [key: string]: any }, warning: string | undefined) {
         this.Env = env;
         this.Warning = warning;
     }
@@ -20,26 +20,22 @@ export class ParsedEnvironmentFile {
     }
 
     public static CreateFromContent(content: string, envFile: string, initialEnv: { [key: string]: any } | undefined): ParsedEnvironmentFile {
-
         // Remove UTF-8 BOM if present
         if (content.charAt(0) === '\uFEFF') {
-            content = content.substr(1);
+            content = content.substring(1);
         }
 
         let parseErrors: string[] = [];
-        let env: { [key: string]: any } = initialEnv;
-        if (!env) {
-            env = {};
-        }
+        let env = initialEnv ?? {};
 
         content.split("\n").forEach(line => {
             // Split the line between key and value
-            const r: RegExpMatchArray = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/);
+            const match = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/);
 
-            if (r !== null) {
-                const key: string = r[1];
-                let value: string = r[2] || "";
-                if ((value.length > 0) && (value.charAt(0) === '"') && (value.charAt(value.length - 1) === '"')) {
+            if (match !== null) {
+                const key = match[1];
+                let value = match[2] ?? "";
+                if (value.length > 0 && value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') {
                     value = value.replace(/\\n/gm, "\n");
                 }
 
@@ -57,14 +53,11 @@ export class ParsedEnvironmentFile {
         });
 
         // show error message if single lines cannot get parsed
-        let warning: string = null;
+        let warning: string | undefined;
         if (parseErrors.length !== 0) {
-            warning = "Ignoring non-parseable lines in envFile " + envFile + ": ";
-            parseErrors.forEach(function (value, idx, array) {
-                warning += "\"" + value + "\"" + ((idx !== array.length - 1) ? ", " : ".");
-            });
+            warning = `Ignoring non-parseable lines in envFile ${envFile}: ${parseErrors.join(", ")}.`;
         }
 
         return new ParsedEnvironmentFile(env, warning);
     }
-}
\ No newline at end of file
+}
diff --git a/src/coreclr-debug/activate.ts b/src/coreclr-debug/activate.ts
index 71494649..c5a31a3b 100644
--- a/src/coreclr-debug/activate.ts
+++ b/src/coreclr-debug/activate.ts
@@ -12,16 +12,14 @@ import { DebuggerPrerequisiteWarning, DebuggerPrerequisiteFailure, DebuggerNotIn
 import { EventStream } from '../EventStream';
 import CSharpExtensionExports from '../CSharpExtensionExports';
 import { getRuntimeDependencyPackageWithId } from '../tools/RuntimeDependencyPackageUtils';
-import { getDotnetInfo, DotnetInfo } from '../utils/getDotnetInfo';
+import { getDotnetInfo } from '../utils/getDotnetInfo';
 import { DotnetDebugConfigurationProvider } from './debugConfigurationProvider';
 import { Options } from '../omnisharp/options';
 
-let _debugUtil: CoreClrDebugUtil = null;
-
 export async function activate(thisExtension: vscode.Extension<CSharpExtensionExports>, context: vscode.ExtensionContext, platformInformation: PlatformInformation, eventStream: EventStream, options: Options) {
-    _debugUtil = new CoreClrDebugUtil(context.extensionPath);
+    const debugUtil = new CoreClrDebugUtil(context.extensionPath);
 
-    if (!CoreClrDebugUtil.existsSync(_debugUtil.debugAdapterDir())) {
+    if (!CoreClrDebugUtil.existsSync(debugUtil.debugAdapterDir())) {
         let isValidArchitecture: boolean = await checkIsValidArchitecture(platformInformation, eventStream);
         // If this is a valid architecture, we should have had a debugger, so warn if we didn't, otherwise
         // a warning was already issued, so do nothing.
@@ -29,11 +27,11 @@ export async function activate(thisExtension: vscode.Extension<CSharpExtensionEx
             eventStream.post(new DebuggerPrerequisiteFailure("[ERROR]: C# Extension failed to install the debugger package."));
             showInstallErrorMessage(eventStream);
         }
-    } else if (!CoreClrDebugUtil.existsSync(_debugUtil.installCompleteFilePath())) {
-        completeDebuggerInstall(platformInformation, eventStream, options);
+    } else if (!CoreClrDebugUtil.existsSync(debugUtil.installCompleteFilePath())) {
+        completeDebuggerInstall(debugUtil, platformInformation, eventStream, options);
     }
 
-    const factory = new DebugAdapterExecutableFactory(platformInformation, eventStream, thisExtension.packageJSON, thisExtension.extensionPath, options);
+    const factory = new DebugAdapterExecutableFactory(debugUtil, platformInformation, eventStream, thisExtension.packageJSON, thisExtension.extensionPath, options);
     context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('coreclr', new DotnetDebugConfigurationProvider(platformInformation)));
     context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('clr', new DotnetDebugConfigurationProvider(platformInformation)));
     context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('coreclr', factory));
@@ -74,30 +72,29 @@ async function checkIsValidArchitecture(platformInformation: PlatformInformation
     return false;
 }
 
-async function completeDebuggerInstall(platformInformation: PlatformInformation, eventStream: EventStream, options: Options): Promise<boolean> {
-    return _debugUtil.checkDotNetCli(options.dotNetCliPaths)
-        .then(async (dotnetInfo: DotnetInfo) => {
+async function completeDebuggerInstall(debugUtil: CoreClrDebugUtil, platformInformation: PlatformInformation, eventStream: EventStream, options: Options): Promise<boolean> {
+    try {
+        await debugUtil.checkDotNetCli(options.dotNetCliPaths);
+        const isValidArchitecture = await checkIsValidArchitecture(platformInformation, eventStream);
+        if (!isValidArchitecture) {
+            eventStream.post(new DebuggerNotInstalledFailure());
+            vscode.window.showErrorMessage('Failed to complete the installation of the C# extension. Please see the error in the output window below.');
+            return false;
+        }
 
-            let isValidArchitecture: boolean = await checkIsValidArchitecture(platformInformation, eventStream);
+        // Write install.complete
+        CoreClrDebugUtil.writeEmptyFile(debugUtil.installCompleteFilePath());
 
-            if (!isValidArchitecture) {
-                eventStream.post(new DebuggerNotInstalledFailure());
-                vscode.window.showErrorMessage('Failed to complete the installation of the C# extension. Please see the error in the output window below.');
-                return false;
-            }
-
-            // Write install.complete
-            CoreClrDebugUtil.writeEmptyFile(_debugUtil.installCompleteFilePath());
+        return true;
+    } catch (err) {
+        const error = err as Error;
 
-            return true;
-        }, (err) => {
-            // Check for dotnet tools failed. pop the UI
-            // err is a DotNetCliError but use defaults in the unexpected case that it's not
-            showDotnetToolsWarning(err.ErrorMessage || _debugUtil.defaultDotNetCliErrorMessage());
-            eventStream.post(new DebuggerPrerequisiteWarning(err.ErrorString || err));
-            // TODO: log telemetry?
-            return false;
-        });
+        // Check for dotnet tools failed. pop the UI
+        showDotnetToolsWarning(error.message);
+        eventStream.post(new DebuggerPrerequisiteWarning(error.message));
+        // TODO: log telemetry?
+        return false;
+    }
 }
 
 function showInstallErrorMessage(eventStream: EventStream) {
@@ -133,7 +130,7 @@ function showDotnetToolsWarning(message: string): void {
 // Else it will launch the debug adapter
 export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescriptorFactory {
 
-    constructor(private readonly platformInfo: PlatformInformation, private readonly eventStream: EventStream, private readonly packageJSON: any, private readonly extensionPath: string, private readonly options: Options) {
+    constructor(private readonly debugUtil: CoreClrDebugUtil, private readonly platformInfo: PlatformInformation, private readonly eventStream: EventStream, private readonly packageJSON: any, private readonly extensionPath: string, private readonly options: Options) {
     }
 
     async createDebugAdapterDescriptor(_session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined): Promise<vscode.DebugAdapterDescriptor> {
@@ -161,8 +158,7 @@ export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescrip
             }
             // install.complete does not exist, check dotnetCLI to see if we can complete.
             else if (!CoreClrDebugUtil.existsSync(util.installCompleteFilePath())) {
-                let success: boolean = await completeDebuggerInstall(this.platformInfo, this.eventStream, this.options);
-
+                let success = await completeDebuggerInstall(this.debugUtil, this.platformInfo, this.eventStream, this.options);
                 if (!success) {
                     this.eventStream.post(new DebuggerNotInstalledFailure());
                     throw new Error('Failed to complete the installation of the C# extension. Please see the error in the output window below.');
@@ -172,14 +168,16 @@ export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescrip
 
         // debugger has finished installation, kick off our debugger process
 
-        // Check for targetArchitecture
-        let dotNetInfo = await getDotnetInfo(this.options.dotNetCliPaths);
-        const targetArchitecture: string = getTargetArchitecture(this.platformInfo, _session.configuration.targetArchitecture, dotNetInfo);
-
         // use the executable specified in the package.json if it exists or determine it based on some other information (e.g. the session)
         if (!executable) {
+            const dotNetInfo = await getDotnetInfo(this.options.dotNetCliPaths);
+            const targetArchitecture = getTargetArchitecture(this.platformInfo, _session.configuration.targetArchitecture, dotNetInfo);
             const command = path.join(common.getExtensionPath(), ".debugger", targetArchitecture, "vsdbg-ui" + CoreClrDebugUtil.getPlatformExeExtension());
-            executable = new vscode.DebugAdapterExecutable(command, [], { env: { 'DOTNET_ROOT' : dotNetInfo.CliPath ? path.dirname(dotNetInfo.CliPath) : '' } });
+            executable = new vscode.DebugAdapterExecutable(command, [], {
+                env: {
+                    DOTNET_ROOT: dotNetInfo.CliPath ? path.dirname(dotNetInfo.CliPath) : '',
+                }
+            });
         }
 
         // make VS Code launch the DA executable
diff --git a/src/coreclr-debug/debugConfigurationProvider.ts b/src/coreclr-debug/debugConfigurationProvider.ts
index ff88bbe9..bf989ff3 100644
--- a/src/coreclr-debug/debugConfigurationProvider.ts
+++ b/src/coreclr-debug/debugConfigurationProvider.ts
@@ -11,7 +11,7 @@ import { PlatformInformation } from '../platform';
 export class DotnetDebugConfigurationProvider implements vscode.DebugConfigurationProvider {
     constructor(public platformInformation: PlatformInformation) {}
 
-    public async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration, token?: vscode.CancellationToken): Promise<vscode.DebugConfiguration>
+    public async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration, token?: vscode.CancellationToken): Promise<vscode.DebugConfiguration | null | undefined>
     {
         if (!debugConfiguration)
         {
@@ -21,7 +21,7 @@ export class DotnetDebugConfigurationProvider implements vscode.DebugConfigurati
         // Process Id is empty, handle Attach to Process Dialog.
         if (debugConfiguration.request === "attach" && !debugConfiguration.processId && !debugConfiguration.processName)
         {
-            let process: AttachItem = undefined;
+            let process: AttachItem | undefined;
             if (debugConfiguration.pipeTransport)
             {
                 process = await RemoteAttachPicker.ShowAttachEntries(debugConfiguration, this.platformInformation);
@@ -33,15 +33,15 @@ export class DotnetDebugConfigurationProvider implements vscode.DebugConfigurati
                 process = await attacher.ShowAttachEntries();
             }
 
-            if (process)
+            if (process !== undefined)
             {
                 debugConfiguration.processId = process.id;
 
-                if (debugConfiguration.type == "coreclr" && 
-                    this.platformInformation.isMacOS() && 
+                if (debugConfiguration.type == "coreclr" &&
+                    this.platformInformation.isMacOS() &&
                     this.platformInformation.architecture == 'arm64')
                 {
-                    // For Apple Silicon M1, it is possible that the process we are attaching to is being emulated as x86_64. 
+                    // For Apple Silicon M1, it is possible that the process we are attaching to is being emulated as x86_64.
                     // The process is emulated if it has process flags has P_TRANSLATED (0x20000).
                     if (process.flags & 0x20000)
                     {
@@ -62,4 +62,4 @@ export class DotnetDebugConfigurationProvider implements vscode.DebugConfigurati
 
         return debugConfiguration;
     }
-}
\ No newline at end of file
+}
diff --git a/src/coreclr-debug/util.ts b/src/coreclr-debug/util.ts
index 962816c2..ed1254fd 100644
--- a/src/coreclr-debug/util.ts
+++ b/src/coreclr-debug/util.ts
@@ -8,15 +8,10 @@ import * as fs from 'fs';
 import * as semver from 'semver';
 import * as os from 'os';
 import { PlatformInformation } from './../platform';
-import { getDotnetInfo, DotnetInfo, DOTNET_MISSING_MESSAGE } from '../utils/getDotnetInfo';
+import { getDotnetInfo, DotnetInfo } from '../utils/getDotnetInfo';
 
 const MINIMUM_SUPPORTED_DOTNET_CLI: string = '1.0.0';
 
-export class DotNetCliError extends Error {
-    public ErrorMessage: string; // the message to display to the user
-    public ErrorString: string; // the string to log for this error
-}
-
 export class CoreClrDebugUtil {
     private _extensionDir: string = '';
     private _debugAdapterDir: string = '';
@@ -61,34 +56,18 @@ export class CoreClrDebugUtil {
         });
     }
 
-    public defaultDotNetCliErrorMessage(): string {
-        return 'Failed to find up to date dotnet cli on the path.';
-    }
-
     // This function checks for the presence of dotnet on the path and ensures the Version
     // is new enough for us.
-    // Returns: a promise that returns a DotnetInfo class
-    // Throws: An DotNetCliError() from the return promise if either dotnet does not exist or is too old.
-    public async checkDotNetCli(dotNetCliPaths: string[]): Promise<DotnetInfo> {
-        let dotnetInfo = await getDotnetInfo(dotNetCliPaths);
-
-        if (dotnetInfo.FullInfo === DOTNET_MISSING_MESSAGE) {
-            // something went wrong with spawning 'dotnet --info'
-            let dotnetError = new DotNetCliError();
-            dotnetError.ErrorMessage = 'The .NET Core SDK cannot be located. .NET Core debugging will not be enabled. Make sure the .NET Core SDK is installed and is on the path.';
-            dotnetError.ErrorString = "Failed to spawn 'dotnet --info'";
-            throw dotnetError;
-        }
-
-        // succesfully spawned 'dotnet --info', check the Version
-        if (semver.lt(dotnetInfo.Version, MINIMUM_SUPPORTED_DOTNET_CLI)) {
-            let dotnetError = new DotNetCliError();
-            dotnetError.ErrorMessage = 'The .NET Core SDK located on the path is too old. .NET Core debugging will not be enabled. The minimum supported version is ' + MINIMUM_SUPPORTED_DOTNET_CLI + '.';
-            dotnetError.ErrorString = "dotnet cli is too old";
-            throw dotnetError;
+    public async checkDotNetCli(dotNetCliPaths: string[]): Promise<void> {
+        try {
+            const dotnetInfo = await getDotnetInfo(dotNetCliPaths);
+            if (semver.lt(dotnetInfo.Version, MINIMUM_SUPPORTED_DOTNET_CLI)) {
+                throw new Error(`The .NET Core SDK located on the path is too old. .NET Core debugging will not be enabled. The minimum supported version is ${MINIMUM_SUPPORTED_DOTNET_CLI}.`);
+            }
+        } catch (error) {
+            const message = error instanceof Error ? error.message : `${error}`;
+            throw new Error(`The .NET Core SDK cannot be located: ${message}. .NET Core debugging will not be enabled. Make sure the .NET Core SDK is installed and is on the path.`);
         }
-
-        return dotnetInfo;
     }
 
     public static isMacOSSupported(): boolean {
@@ -102,10 +81,11 @@ export class CoreClrDebugUtil {
             fs.accessSync(path, fs.constants.F_OK);
             return true;
         } catch (err) {
-            if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
+            const error = err as NodeJS.ErrnoException;
+            if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
                 return false;
             } else {
-                throw Error(err.code);
+                throw Error(error.code);
             }
         }
     }
@@ -121,52 +101,40 @@ export class CoreClrDebugUtil {
 
 const MINIMUM_SUPPORTED_OSX_ARM64_DOTNET_CLI: string = '6.0.0';
 
-export function getTargetArchitecture(platformInfo: PlatformInformation, launchJsonTargetArchitecture: string, dotnetInfo: DotnetInfo): string {
-    let targetArchitecture = "";
+export function getTargetArchitecture(platformInfo: PlatformInformation, launchJsonTargetArchitecture: string | undefined, dotnetInfo: DotnetInfo): string {
+    if (!platformInfo.isMacOS())
+    {
+        // Nothing to do here.
+        return '';
+    }
 
     // On Apple M1 Machines, we need to determine if we need to use the 'x86_64' or 'arm64' debugger.
-    if (platformInfo.isMacOS())
+
+    // 'targetArchitecture' is specified in launch.json configuration, use that.
+    if (launchJsonTargetArchitecture)
     {
-        // 'targetArchitecture' is specified in launch.json configuration, use that.
-        if (launchJsonTargetArchitecture)
+        if (launchJsonTargetArchitecture !== "x86_64" && launchJsonTargetArchitecture !== "arm64")
         {
-            if (launchJsonTargetArchitecture !== "x86_64" && launchJsonTargetArchitecture !== "arm64")
-            {
-                throw new Error(`The value '${launchJsonTargetArchitecture}' for 'targetArchitecture' in launch configuraiton is invalid. Expected 'x86_64' or 'arm64'.`);
-            }
-            targetArchitecture = launchJsonTargetArchitecture;
+            throw new Error(`The value '${launchJsonTargetArchitecture}' for 'targetArchitecture' in launch configuraiton is invalid. Expected 'x86_64' or 'arm64'.`);
         }
-        else if (dotnetInfo)
-        {
-            // Find which targetArchitecture to use based on SDK Version or RID.
+        return launchJsonTargetArchitecture;
+    }
 
-            // If we are lower than .NET 6, use 'x86_64' since 'arm64' was not supported until .NET 6
-            if (dotnetInfo.Version && semver.lt(dotnetInfo.Version, MINIMUM_SUPPORTED_OSX_ARM64_DOTNET_CLI))
-            {
-                targetArchitecture = 'x86_64';
-            }
-            else if (dotnetInfo.RuntimeId)
-            {
-                if (dotnetInfo.RuntimeId.includes('arm64'))
-                {
-                    targetArchitecture = 'arm64';
-                }
-                else if (dotnetInfo.RuntimeId.includes('x64'))
-                {
-                    targetArchitecture = 'x86_64';
-                }
-                else
-                {
-                    throw new Error(`Unexpected RuntimeId '${dotnetInfo.RuntimeId}'.`);
-                }
-            }
-        }
+    // If we are lower than .NET 6, use 'x86_64' since 'arm64' was not supported until .NET 6.
+    if (semver.lt(dotnetInfo.Version, MINIMUM_SUPPORTED_OSX_ARM64_DOTNET_CLI))
+    {
+        return 'x86_64';
+    }
 
-        if (!targetArchitecture) {
-            // Unable to retrieve any targetArchitecture, go with platformInfo.
-            targetArchitecture = platformInfo.architecture;
-        }
+    // Otherwise, look at the runtime ID.
+    if (dotnetInfo.RuntimeId.includes('arm64'))
+    {
+        return 'arm64';
+    }
+    else if (dotnetInfo.RuntimeId.includes('x64'))
+    {
+        return 'x86_64';
     }
 
-    return targetArchitecture;
+    throw new Error(`Unexpected RuntimeId '${dotnetInfo.RuntimeId}'.`);
 }
diff --git a/src/features/reportIssue.ts b/src/features/reportIssue.ts
index 3657c340..eee71730 100644
--- a/src/features/reportIssue.ts
+++ b/src/features/reportIssue.ts
@@ -15,7 +15,15 @@ const issuesUrl = "https://github.com/OmniSharp/omnisharp-vscode/issues/new";
 
 export default async function reportIssue(vscode: vscode, csharpExtVersion: string, eventStream: EventStream, getDotnetInfo: IGetDotnetInfo, isValidPlatformForMono: boolean, options: Options, dotnetResolver: IHostExecutableResolver, monoResolver: IHostExecutableResolver) {
     // Get info for the dotnet that the Omnisharp executable is run on, not the dotnet Omnisharp will execute user code on.
-    const dotnetInfo = await getDotnetInfo([ dirname((await dotnetResolver.getHostExecutableInfo(options)).path) ]);
+    let fullDotnetInfo: string | undefined;
+    try {
+        const dotnetInfo = await getDotnetInfo([ dirname((await dotnetResolver.getHostExecutableInfo(options)).path) ]);
+        fullDotnetInfo = dotnetInfo.FullInfo;
+    } catch (error) {
+        const message = error instanceof Error ? error.message : `${error}`;
+        fullDotnetInfo = message;
+    }
+
     const monoInfo = await getMonoIfPlatformValid(isValidPlatformForMono, options, monoResolver);
     let extensions = getInstalledExtensions(vscode);
 
@@ -41,7 +49,7 @@ export default async function reportIssue(vscode: vscode, csharpExtVersion: stri
 
 ${monoInfo}
 <details><summary>Dotnet Information</summary>
-${dotnetInfo.FullInfo}</details>
+${fullDotnetInfo}</details>
 <details><summary>Visual Studio Code Extensions</summary>
 ${generateExtensionTable(extensions)}
 </details>
diff --git a/src/utils/getDotnetInfo.ts b/src/utils/getDotnetInfo.ts
index 8acc852d..d4071c72 100644
--- a/src/utils/getDotnetInfo.ts
+++ b/src/utils/getDotnetInfo.ts
@@ -7,21 +7,16 @@ import { join } from "path";
 import { execChildProcess } from "../common";
 import { CoreClrDebugUtil } from "../coreclr-debug/util";
 
-export const DOTNET_MISSING_MESSAGE = "A valid dotnet installation could not be found.";
+let _dotnetInfo: DotnetInfo | undefined;
 
-let _dotnetInfo: DotnetInfo;
-
-// This function checks for the presence of dotnet on the path and ensures the Version
-// is new enough for us.
-// Returns: a promise that returns a DotnetInfo class
-// Throws: An DotNetCliError() from the return promise if either dotnet does not exist or is too old.
+// This function calls `dotnet --info` and returns the result as a DotnetInfo object.
 export async function getDotnetInfo(dotNetCliPaths: string[]): Promise<DotnetInfo> {
     if (_dotnetInfo !== undefined) {
         return _dotnetInfo;
     }
 
-    let dotnetExeName = join('dotnet', CoreClrDebugUtil.getPlatformExeExtension());
-    let dotnetExecutablePath: string | undefined = undefined;
+    let dotnetExeName = `dotnet${CoreClrDebugUtil.getPlatformExeExtension()}`;
+    let dotnetExecutablePath: string | undefined;
 
     for (const dotnetPath of dotNetCliPaths) {
         let dotnetFullPath = join(dotnetPath, dotnetExeName);
@@ -31,41 +26,47 @@ export async function getDotnetInfo(dotNetCliPaths: string[]): Promise<DotnetInf
         }
     }
 
-    const dotnetInfo = new DotnetInfo();
-
     try {
-        let data = await execChildProcess(`${dotnetExecutablePath || 'dotnet'} --info`, process.cwd(), process.env);
+        const data = await execChildProcess(`${dotnetExecutablePath ?? 'dotnet'} --info`, process.cwd(), process.env);
 
-        dotnetInfo.CliPath = dotnetExecutablePath;
-        dotnetInfo.FullInfo = data;
+        const cliPath = dotnetExecutablePath;
+        const fullInfo = data;
 
-        let lines: string[] = data.replace(/\r/mg, '').split('\n');
-        lines.forEach(line => {
-            let match: RegExpMatchArray;
+        let version: string | undefined;
+        let runtimeId: string | undefined;
+
+        const lines = data.replace(/\r/mg, '').split('\n');
+        for (const line of lines) {
+            let match: RegExpMatchArray | null;
             if (match = /^\ Version:\s*([^\s].*)$/.exec(line)) {
-                dotnetInfo.Version = match[1];
-            } else if (match = /^\ OS Version:\s*([^\s].*)$/.exec(line)) {
-                dotnetInfo.OsVersion = match[1];
+                version = match[1];
             } else if (match = /^\ RID:\s*([\w\-\.]+)$/.exec(line)) {
-                dotnetInfo.RuntimeId = match[1];
+                runtimeId = match[1];
+            }
+
+            if (version !== undefined && runtimeId !== undefined) {
+                _dotnetInfo = {
+                    CliPath: cliPath,
+                    FullInfo: fullInfo,
+                    Version: version,
+                    RuntimeId: runtimeId,
+                };
+                return _dotnetInfo;
             }
-        });
+        }
 
-        _dotnetInfo = dotnetInfo;
+        throw new Error('Failed to parse dotnet version information');
     }
     catch
     {
         // something went wrong with spawning 'dotnet --info'
-        dotnetInfo.FullInfo = DOTNET_MISSING_MESSAGE;
+        throw new Error('A valid dotnet installation could not be found');
     }
-
-    return dotnetInfo;
 }
 
-export class DotnetInfo {
-    public CliPath?: string;
-    public FullInfo: string;
-    public Version: string;
-    public OsVersion: string;
-    public RuntimeId: string;
-}
\ No newline at end of file
+export interface DotnetInfo {
+    CliPath?: string;
+    FullInfo: string;
+    Version: string;
+    RuntimeId: string;
+}


Test patch:
diff --git a/test/unitTests/Fakes/FakeGetDotnetInfo.ts b/test/unitTests/Fakes/FakeGetDotnetInfo.ts
index b49d9db4..f4fec284 100644
--- a/test/unitTests/Fakes/FakeGetDotnetInfo.ts
+++ b/test/unitTests/Fakes/FakeGetDotnetInfo.ts
@@ -9,7 +9,6 @@ import { DotnetInfo } from "../../../src/utils/getDotnetInfo";
 export const fakeDotnetInfo: DotnetInfo = {
     FullInfo: "myDotnetInfo",
     Version: "1.0.x",
-    OsVersion: "Fake86",
     RuntimeId: "1.1.x"
 };
-export const FakeGetDotnetInfo: IGetDotnetInfo = async () => Promise.resolve(fakeDotnetInfo);
\ No newline at end of file
+export const FakeGetDotnetInfo: IGetDotnetInfo = async () => Promise.resolve(fakeDotnetInfo);
diff --git a/test/unitTests/ParsedEnvironmentFile.test.ts b/test/unitTests/ParsedEnvironmentFile.test.ts
index 35898ffd..1aa171aa 100644
--- a/test/unitTests/ParsedEnvironmentFile.test.ts
+++ b/test/unitTests/ParsedEnvironmentFile.test.ts
@@ -11,28 +11,25 @@ suite("ParsedEnvironmentFile", () => {
 
     test("Add single variable", () => {
         const content = `MyName=VALUE`;
-        const fakeConfig: { [key: string]: any } = {};
-        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]);
+        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", undefined);
 
-        expect(result.Warning).to.be.null;
+        expect(result.Warning).to.be.undefined;
         result.Env["MyName"].should.equal("VALUE");
     });
 
     test("Handle quoted values", () => {
         const content = `MyName="VALUE"`;
-        const fakeConfig: { [key: string]: any } = {};
-        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]);
+        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", undefined);
 
-        expect(result.Warning).to.be.null;
+        expect(result.Warning).to.be.undefined;
         result.Env["MyName"].should.equal("VALUE");
     });
 
     test("Handle BOM", () => {
         const content = "\uFEFFMyName=VALUE";
-        const fakeConfig: { [key: string]: any } = {};
-        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]);
+        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", undefined);
 
-        expect(result.Warning).to.be.null;
+        expect(result.Warning).to.be.undefined;
         result.Env["MyName"].should.equal("VALUE");
     });
 
@@ -42,10 +39,9 @@ MyName1=Value1
 MyName2=Value2
 
 `;
-        const fakeConfig: { [key: string]: any } = {};
-        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]);
+        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", undefined);
 
-        expect(result.Warning).to.be.null;
+        expect(result.Warning).to.be.undefined;
         result.Env["MyName1"].should.equal("Value1");
         result.Env["MyName2"].should.equal("Value2");
     });
@@ -56,13 +52,13 @@ MyName1=Value1
 MyName2=Value2
 
 `;
-        const initialEnv: { [key: string]: any } = {
+        const initialEnv = {
             "MyName1": "Value7",
             "ThisShouldNotChange": "StillHere"
         };
         const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", initialEnv);
 
-        expect(result.Warning).to.be.null;
+        expect(result.Warning).to.be.undefined;
         result.Env["MyName1"].should.equal("Value1");
         result.Env["MyName2"].should.equal("Value2");
         result.Env["ThisShouldNotChange"].should.equal("StillHere");
@@ -74,10 +70,9 @@ MyName1=Value1
 # This is a comment in the middle of the file
 MyName2=Value2
 `;
-        const fakeConfig: { [key: string]: any } = {};
-        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]);
+        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", undefined);
 
-        expect(result.Warning).to.be.null;
+        expect(result.Warning).to.be.undefined;
         result.Env["MyName1"].should.equal("Value1");
         result.Env["MyName2"].should.equal("Value2");
     });
@@ -89,10 +84,10 @@ MyName1=Value1
 MyName2=Value2
 
 `;
-        const fakeConfig: { [key: string]: any } = {};
-        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]);
+        const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", undefined);
 
-        result.Warning.should.startWith("Ignoring non-parseable lines in envFile TestEnvFileName");
+        expect(result.Warning).not.to.be.undefined;
+        result.Warning!.should.startWith("Ignoring non-parseable lines in envFile TestEnvFileName");
         result.Env["MyName1"].should.equal("Value1");
         result.Env["MyName2"].should.equal("Value2");
     });
diff --git a/test/unitTests/coreclr-debug/targetArchitecture.test.ts b/test/unitTests/coreclr-debug/targetArchitecture.test.ts
index 0c49ddb8..f0d422b6 100644
--- a/test/unitTests/coreclr-debug/targetArchitecture.test.ts
+++ b/test/unitTests/coreclr-debug/targetArchitecture.test.ts
@@ -14,9 +14,14 @@ suite("getTargetArchitecture Tests", () => {
 
     suite("Windows", () => {
         test("Windows x86_64", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("win32", "x86_64", null);
+            const platformInfo = new PlatformInformation("win32", "x86_64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "5.0.0",
+                RuntimeId: "win10-x64",
+            };
 
-            const targetArchitecture = getTargetArchitecture(platformInfo, null, null);
+            const targetArchitecture = getTargetArchitecture(platformInfo, undefined, dotnetInfo);
 
             assert.equal(targetArchitecture, "");
         });
@@ -24,9 +29,14 @@ suite("getTargetArchitecture Tests", () => {
 
     suite("Linux", () => {
         test("getTargetArchitecture on Linux", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("linux", "x86_64", null);
+            const platformInfo = new PlatformInformation("linux", "x86_64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "5.0.0",
+                RuntimeId: "linux-x64",
+            };
 
-            const targetArchitecture = getTargetArchitecture(platformInfo, null, null);
+            const targetArchitecture = getTargetArchitecture(platformInfo, undefined, dotnetInfo);
 
             assert.equal(targetArchitecture, "");
         });
@@ -34,51 +44,64 @@ suite("getTargetArchitecture Tests", () => {
 
     suite("macOS", () => {
         test("Apple x86_64", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("darwin", "x86_64", null);
+            const platformInfo = new PlatformInformation("darwin", "x86_64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "5.0.0",
+                RuntimeId: "osx.11.0-x64",
+            };
 
-            const targetArchitecture = getTargetArchitecture(platformInfo, null, null);
+            const targetArchitecture = getTargetArchitecture(platformInfo, undefined, dotnetInfo);
 
             assert.equal(targetArchitecture, "x86_64");
         });
 
         test("Apple ARM64 on .NET 5", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("darwin", "arm64", null);
-            const dotnetInfo: DotnetInfo = new DotnetInfo();
-            dotnetInfo.Version = "5.0.0";
-            dotnetInfo.RuntimeId = "osx.11.0-x64";
+            const platformInfo = new PlatformInformation("darwin", "arm64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "5.0.0",
+                RuntimeId: "osx.11.0-x64",
+            };
 
-            const targetArchitecture = getTargetArchitecture(platformInfo, null, dotnetInfo);
+            const targetArchitecture = getTargetArchitecture(platformInfo, undefined, dotnetInfo);
 
             assert.equal(targetArchitecture, "x86_64");
         });
 
         test("Apple ARM64 on .NET 6 osx-arm64", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("darwin", "arm64", null);
-            const dotnetInfo: DotnetInfo = new DotnetInfo();
-            dotnetInfo.Version = "6.0.0";
-            dotnetInfo.RuntimeId = "osx.11.0-arm64";
+            const platformInfo = new PlatformInformation("darwin", "arm64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "6.0.0",
+                RuntimeId: "osx.11.0-arm64",
+            };
 
-            const targetArchitecture = getTargetArchitecture(platformInfo, null, dotnetInfo);
+            const targetArchitecture = getTargetArchitecture(platformInfo, undefined, dotnetInfo);
 
             assert.equal(targetArchitecture, "arm64");
         });
 
         test("Apple ARM64 on .NET 6 osx-x64", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("darwin", "arm64", null);
-            const dotnetInfo: DotnetInfo = new DotnetInfo();
-            dotnetInfo.Version = "6.0.0";
-            dotnetInfo.RuntimeId = "osx.11.0-x64";
+            const platformInfo = new PlatformInformation("darwin", "arm64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "6.0.0",
+                RuntimeId: "osx.11.0-x64",
+            };
 
-            const targetArchitecture = getTargetArchitecture(platformInfo, null, dotnetInfo);
+            const targetArchitecture = getTargetArchitecture(platformInfo, undefined, dotnetInfo);
 
             assert.equal(targetArchitecture, "x86_64");
         });
 
         test("Apple ARM64 on .NET 6 osx-arm64 with targetArchitecture: 'arm64'", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("darwin", "arm64", null);
-            const dotnetInfo: DotnetInfo = new DotnetInfo();
-            dotnetInfo.Version = "6.0.0";
-            dotnetInfo.RuntimeId = "osx.11.0-arm64";
+            const platformInfo = new PlatformInformation("darwin", "arm64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "6.0.0",
+                RuntimeId: "osx.11.0-arm64",
+            };
 
             const targetArchitecture = getTargetArchitecture(platformInfo, "arm64", dotnetInfo);
 
@@ -86,10 +109,12 @@ suite("getTargetArchitecture Tests", () => {
         });
 
         test("Apple ARM64 on .NET 6 osx-arm64 with targetArchitecture: 'x86_64'", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("darwin", "arm64", null);
-            const dotnetInfo: DotnetInfo = new DotnetInfo();
-            dotnetInfo.Version = "6.0.0";
-            dotnetInfo.RuntimeId = "osx.11.0-x86_64";
+            const platformInfo = new PlatformInformation("darwin", "arm64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "6.0.0",
+                RuntimeId: "osx.11.0-x86_64",
+            };
 
             const targetArchitecture = getTargetArchitecture(platformInfo, "x86_64", dotnetInfo);
 
@@ -97,10 +122,12 @@ suite("getTargetArchitecture Tests", () => {
         });
 
         test("Apple ARM64 on .NET 6 osx-arm64 with invalid targetArchitecture", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("darwin", "arm64", null);
-            const dotnetInfo: DotnetInfo = new DotnetInfo();
-            dotnetInfo.Version = "6.0.0";
-            dotnetInfo.RuntimeId = "osx.11.0-x86_64";
+            const platformInfo = new PlatformInformation("darwin", "arm64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "6.0.0",
+                RuntimeId: "osx.11.0-x86_64",
+            };
 
             let fn = function () { getTargetArchitecture(platformInfo, "x64", dotnetInfo); };
 
@@ -108,12 +135,14 @@ suite("getTargetArchitecture Tests", () => {
         });
 
         test("Apple ARM64 on .NET 6 osx-arm64 with invalid RuntimeId", () => {
-            const platformInfo: PlatformInformation = new PlatformInformation("darwin", "arm64", null);
-            const dotnetInfo: DotnetInfo = new DotnetInfo();
-            dotnetInfo.Version = "6.0.0";
-            dotnetInfo.RuntimeId = "osx.11.0-FUTURE_ISA";
-
-            let fn = function () { getTargetArchitecture(platformInfo, null, dotnetInfo); };
+            const platformInfo = new PlatformInformation("darwin", "arm64");
+            const dotnetInfo: DotnetInfo = {
+                FullInfo: "Irrelevant",
+                Version: "6.0.0",
+                RuntimeId: "osx.11.0-FUTURE_ISA",
+            };
+
+            let fn = function () { getTargetArchitecture(platformInfo, undefined, dotnetInfo); };
 
             expect(fn).to.throw(`Unexpected RuntimeId 'osx.11.0-FUTURE_ISA'.`);
         });