Repository: rokucommunity/brighterscript
Pull request #996 (2023-12-23 17:00:19)

Improve null safety
This adds a second tsconfig which can be used to view null safety errors, and fixes a handful of them. Nothing here should change runtime behavior and I've aimed to make the changes as noninvasive as possible.

- I've been loose with test files, preferring unsafe assertions to code changes in them
- When possible, I've added annotations rather than modify runtime code

I've added explicit type annotations and intermediate interfaces for the return types of some functions and methods which were previously implicit. In some cases this helps inference go through without changes and in other cases it just made it easier for me to determine what exactly was undefined in a nested object.

I've skipped a couple of the harder errors. Most notably, there seems to be an implicit distinction between an "input BsConfig" and an "initialized BsConfig", in that many fields in the BsConfig class are optional but assumed to be present during actual processing. The potentially pedantic way to address this would be to make a second `InitializedBsConfig` type which is used internally and is produced by parsing a BsConfig, but this would be significantly more invasive than what I am aiming for here.

Please let me know if you would prefer any modifications to my approach.


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 2cd1956d..816362f0 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -42,6 +42,7 @@ module.exports = {
         '@typescript-eslint/no-implicit-any-catch': 'off',
         '@typescript-eslint/no-invalid-this': 'off',
         '@typescript-eslint/no-magic-numbers': 'off',
+        '@typescript-eslint/no-non-null-assertion': 'off',
         '@typescript-eslint/no-parameter-properties': 'off',
         //had to add this rule to prevent eslint from crashing
         '@typescript-eslint/no-restricted-imports': ['off', {}],
@@ -61,6 +62,7 @@ module.exports = {
         '@typescript-eslint/no-type-alias': 'off',
         '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
         '@typescript-eslint/no-unnecessary-condition': 'off',
+        '@typescript-eslint/no-unnecessary-type-assertion': 'off',
         '@typescript-eslint/no-unsafe-assignment': 'off',
         '@typescript-eslint/no-unsafe-call': 'off',
         '@typescript-eslint/no-unsafe-member-access': 'off',
@@ -240,8 +242,6 @@ module.exports = {
         files: ['benchmarks/**/*.ts'],
         rules: {
             '@typescript-eslint/dot-notation': 'off',
-            '@typescript-eslint/no-unnecessary-type-assertion': 'off',
-            '@typescript-eslint/no-non-null-assertion': 'off',
             'camelcase': 'off'
         }
     }]
diff --git a/package.json b/package.json
index 00399ce9..0f6ad59c 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,8 @@
     "docs": "ts-node scripts/compile-doc-examples.ts",
     "benchmark": "cd ./benchmarks && ts-node ./index.ts",
     "scrape-roku-docs": "ts-node scripts/scrape-roku-docs.ts",
-    "rescrape-roku-docs": "rm scripts/.cache.json && ts-node scripts/scrape-roku-docs.ts"
+    "rescrape-roku-docs": "rm scripts/.cache.json && ts-node scripts/scrape-roku-docs.ts",
+    "null-check": "tsc -p tsconfig-null-safe.json"
   },
   "mocha": {
     "spec": "src/**/*.spec.ts",
diff --git a/src/BusyStatusTracker.spec.ts b/src/BusyStatusTracker.spec.ts
index 5872e799..f7137aac 100644
--- a/src/BusyStatusTracker.spec.ts
+++ b/src/BusyStatusTracker.spec.ts
@@ -89,9 +89,9 @@ describe('BusyStatusTracker', () => {
         try {
             tracker.run((finalize) => {
                 expect(latestStatus).to.eql(BusyStatus.busy);
-                finalize();
+                finalize?.();
                 expect(latestStatus).to.eql(BusyStatus.idle);
-                finalize();
+                finalize?.();
                 expect(latestStatus).to.eql(BusyStatus.idle);
             });
         } catch { }
@@ -125,7 +125,7 @@ describe('BusyStatusTracker', () => {
     });
 
     it('supports unsubscribing from events', () => {
-        const changes = []; //contains every busy/idle status change
+        const changes: BusyStatus[] = []; //contains every busy/idle status change
         const disconnect = tracker.on('change', (status) => changes.push(status));
 
         expect(changes.length).to.eql(0);
diff --git a/src/CodeActionUtil.ts b/src/CodeActionUtil.ts
index c80d5d10..cb8257af 100644
--- a/src/CodeActionUtil.ts
+++ b/src/CodeActionUtil.ts
@@ -12,15 +12,15 @@ export class CodeActionUtil {
             const uri = URI.file(change.filePath).toString();
 
             //create the edit changes array for this uri
-            if (!edit.changes[uri]) {
-                edit.changes[uri] = [];
+            if (!edit.changes![uri]) {
+                edit.changes![uri] = [];
             }
             if (change.type === 'insert') {
-                edit.changes[uri].push(
+                edit.changes![uri].push(
                     TextEdit.insert(change.position, change.newText)
                 );
             } else if (change.type === 'replace') {
-                edit.changes[uri].push(
+                edit.changes![uri].push(
                     TextEdit.replace(change.range, change.newText)
                 );
             }
@@ -31,7 +31,7 @@ export class CodeActionUtil {
         return action;
     }
 
-    public serializableDiagnostics(diagnostics: Diagnostic[]) {
+    public serializableDiagnostics(diagnostics: Diagnostic[] | undefined) {
         return diagnostics?.map(({ range, severity, code, source, message, relatedInformation }) => ({
             range: range,
             severity: severity,
diff --git a/src/CommentFlagProcessor.spec.ts b/src/CommentFlagProcessor.spec.ts
index 5b72ab8e..7105a0f4 100644
--- a/src/CommentFlagProcessor.spec.ts
+++ b/src/CommentFlagProcessor.spec.ts
@@ -2,13 +2,16 @@ import { expect } from './chai-config.spec';
 import { Range } from 'vscode-languageserver';
 import { CommentFlagProcessor } from './CommentFlagProcessor';
 import { Lexer } from './lexer/Lexer';
+import type { BscFile } from '.';
 
 describe('CommentFlagProcessor', () => {
     let processor: CommentFlagProcessor;
 
+    const mockBscFile: BscFile = null as any;
+
     describe('tokenizeByWhitespace', () => {
         beforeEach(() => {
-            processor = new CommentFlagProcessor(null);
+            processor = new CommentFlagProcessor(mockBscFile);
         });
 
         it('works with single chars', () => {
@@ -66,18 +69,18 @@ describe('CommentFlagProcessor', () => {
 
     describe('tokenize', () => {
         beforeEach(() => {
-            processor = new CommentFlagProcessor(null, [`'`]);
+            processor = new CommentFlagProcessor(mockBscFile, [`'`]);
         });
 
         it('skips non disable comments', () => {
             expect(
-                processor['tokenize'](`'not disable comment`, null)
+                processor['tokenize'](`'not disable comment`, null as any as Range)
             ).not.to.exist;
         });
 
         it('tokenizes bs:disable-line comment', () => {
             expect(
-                processor['tokenize'](`'bs:disable-line`, null)
+                processor['tokenize'](`'bs:disable-line`, null as any as Range)
             ).to.eql({
                 commentTokenText: `'`,
                 disableType: 'line',
diff --git a/src/CommentFlagProcessor.ts b/src/CommentFlagProcessor.ts
index 268abaac..27b07ac8 100644
--- a/src/CommentFlagProcessor.ts
+++ b/src/CommentFlagProcessor.ts
@@ -51,11 +51,12 @@ export class CommentFlagProcessor {
         let affectedRange: Range;
         if (tokenized.disableType === 'line') {
             affectedRange = util.createRange(range.start.line, 0, range.start.line, range.start.character);
-        } else if (tokenized.disableType === 'next-line') {
+        } else {
+            // tokenized.disableType must be 'next-line'
             affectedRange = util.createRange(range.start.line + 1, 0, range.start.line + 1, Number.MAX_SAFE_INTEGER);
         }
 
-        let commentFlag: CommentFlag;
+        let commentFlag: CommentFlag | null = null;
 
         //statement to disable EVERYTHING
         if (tokenized.codes.length === 0) {
@@ -115,10 +116,10 @@ export class CommentFlagProcessor {
     /**
      * Small tokenizer for bs:disable comments
      */
-    private tokenize(text: string, range: Range) {
+    private tokenize(text: string, range: Range): DisableToken | null {
         let lowerText = text.toLowerCase();
         let offset = 0;
-        let commentTokenText: string;
+        let commentTokenText: string | null = null;
 
         for (const starter of this.commentStarters) {
             if (text.startsWith(starter)) {
@@ -177,9 +178,10 @@ export class CommentFlagProcessor {
      * Given a string, extract each item split by whitespace
      * @param text the text to tokenize
      */
-    private tokenizeByWhitespace(text: string) {
-        let tokens = [] as Array<{ startIndex: number; text: string }>;
-        let currentToken = null;
+    private tokenizeByWhitespace(text: string): Token[] {
+        let tokens = [] as Array<Token>;
+        let currentToken: Token | null = null;
+
         for (let i = 0; i < text.length; i++) {
             let char = text[i];
             //if we hit whitespace
@@ -206,3 +208,17 @@ export class CommentFlagProcessor {
         return tokens;
     }
 }
+
+interface Token {
+    startIndex: number;
+    text: string;
+}
+
+interface DisableToken {
+    commentTokenText: string | null;
+    disableType: 'line' | 'next-line';
+    codes: {
+        code: string;
+        range: Range;
+    }[];
+}
diff --git a/src/DependencyGraph.ts b/src/DependencyGraph.ts
index 9b3247e3..fd7742b8 100644
--- a/src/DependencyGraph.ts
+++ b/src/DependencyGraph.ts
@@ -137,17 +137,19 @@ export class Node {
     ) {
         if (dependencies.length > 0) {
             this.subscriptions = [];
-        }
-        for (let dependency of this.dependencies) {
-            let sub = this.graph.onchange(dependency, (event) => {
-                //notify the graph that we changed since one of our dependencies changed
-                this.graph.emit(this.key, event);
-            });
 
-            this.subscriptions.push(sub);
+            for (let dependency of this.dependencies) {
+                let sub = this.graph.onchange(dependency, (event) => {
+                    //notify the graph that we changed since one of our dependencies changed
+                    this.graph.emit(this.key, event);
+                });
+
+                this.subscriptions.push(sub);
+            }
         }
     }
-    private subscriptions: Array<() => void>;
+
+    private subscriptions: Array<() => void> | undefined;
 
     /**
      * Return the full list of unique dependencies for this node by traversing all descendents
@@ -161,7 +163,7 @@ export class Node {
             let dependency = dependencyStack.pop();
 
             //if this is a new dependency and we aren't supposed to skip it
-            if (!dependencyMap[dependency] && !exclude.includes(dependency)) {
+            if (dependency && !dependencyMap[dependency] && !exclude.includes(dependency)) {
                 dependencyMap[dependency] = true;
 
                 //get the node for this dependency
diff --git a/src/DiagnosticCollection.spec.ts b/src/DiagnosticCollection.spec.ts
index 2a4d09c1..c61c0300 100644
--- a/src/DiagnosticCollection.spec.ts
+++ b/src/DiagnosticCollection.spec.ts
@@ -35,7 +35,7 @@ describe('DiagnosticCollection', () => {
 
     it('does not crash for diagnostics with missing locations', () => {
         const [d1] = addDiagnostics('file1.brs', ['I have no location']);
-        delete d1.range;
+        delete (d1 as any).range;
         testPatch({
             'file1.brs': ['I have no location']
         });
@@ -104,7 +104,7 @@ describe('DiagnosticCollection', () => {
     }
 
     function addDiagnostics(srcPath: string, messages: string[]) {
-        const newDiagnostics = [];
+        const newDiagnostics: BsDiagnostic[] = [];
         for (const message of messages) {
             newDiagnostics.push({
                 file: {
@@ -117,6 +117,6 @@ describe('DiagnosticCollection', () => {
             });
         }
         diagnostics.push(...newDiagnostics);
-        return newDiagnostics as typeof diagnostics;
+        return newDiagnostics;
     }
 });
diff --git a/src/DiagnosticFilterer.ts b/src/DiagnosticFilterer.ts
index 5cd24b5e..7c7f3b10 100644
--- a/src/DiagnosticFilterer.ts
+++ b/src/DiagnosticFilterer.ts
@@ -6,8 +6,12 @@ import { standardizePath as s } from './util';
 
 export class DiagnosticFilterer {
     private byFile: Record<string, BsDiagnostic[]>;
-    private filters: Array<{ src?: string; codes?: (number | string)[] }>;
-    private rootDir: string;
+    private filters: Array<{ src?: string; codes?: (number | string)[] }> | undefined;
+    private rootDir: string | undefined;
+
+    constructor() {
+        this.byFile = {};
+    }
 
     /**
      * Filter a list of diagnostics based on the provided filters
@@ -24,7 +28,7 @@ export class DiagnosticFilterer {
         let result = this.getDiagnostics();
 
         //clean up
-        delete this.byFile;
+        this.byFile = {};
         delete this.rootDir;
         delete this.filters;
 
@@ -108,7 +112,7 @@ export class DiagnosticFilterer {
             let fileDiagnostics = this.byFile[filePath];
             for (let i = 0; i < fileDiagnostics.length; i++) {
                 let diagnostic = fileDiagnostics[i];
-                if (filter.codes.includes(diagnostic.code)) {
+                if (filter.codes.includes(diagnostic.code!)) {
                     //remove this diagnostic
                     fileDiagnostics.splice(i, 1);
                     //repeat this loop iteration (with the new item at this index)
@@ -123,7 +127,7 @@ export class DiagnosticFilterer {
         let globalIgnoreCodes: (number | string)[] = [...config1.ignoreErrorCodes ?? []];
         let diagnosticFilters = [...config1.diagnosticFilters ?? []];
 
-        let result = [];
+        let result: Array<{ src?: string; codes: (number | string)[] }> = [];
 
         for (let filter of diagnosticFilters as any[]) {
             if (typeof filter === 'number') {
@@ -151,6 +155,6 @@ export class DiagnosticFilterer {
                 codes: globalIgnoreCodes
             });
         }
-        return result as Array<{ src?: string; codes: (number | string)[] }>;
+        return result;
     }
 }
diff --git a/src/DiagnosticSeverityAdjuster.ts b/src/DiagnosticSeverityAdjuster.ts
index a9defc9f..b651f30b 100644
--- a/src/DiagnosticSeverityAdjuster.ts
+++ b/src/DiagnosticSeverityAdjuster.ts
@@ -16,6 +16,11 @@ export class DiagnosticSeverityAdjuster {
 
     public createSeverityMap(diagnosticSeverityOverrides: BsConfig['diagnosticSeverityOverrides']): Map<string, DiagnosticSeverity> {
         const map = new Map<string, DiagnosticSeverity>();
+
+        if (!diagnosticSeverityOverrides) {
+            return map;
+        }
+
         Object.keys(diagnosticSeverityOverrides).forEach(key => {
             const value = diagnosticSeverityOverrides[key];
             switch (value) {
diff --git a/src/FunctionScope.spec.ts b/src/FunctionScope.spec.ts
index e7b6e1e1..a60b242b 100644
--- a/src/FunctionScope.spec.ts
+++ b/src/FunctionScope.spec.ts
@@ -1,6 +1,7 @@
 import { expect } from './chai-config.spec';
 
 import { FunctionScope } from './FunctionScope';
+import type { FunctionExpression } from './parser/Expression';
 import { Program } from './Program';
 
 describe('FunctionScope', () => {
@@ -9,7 +10,7 @@ describe('FunctionScope', () => {
     let program: Program;
     beforeEach(() => {
         program = new Program({ rootDir: rootDir });
-        scope = new FunctionScope(null);
+        scope = new FunctionScope(null as any as FunctionExpression);
     });
 
     afterEach(() => {
diff --git a/src/FunctionScope.ts b/src/FunctionScope.ts
index b825db4c..b4296cf9 100644
--- a/src/FunctionScope.ts
+++ b/src/FunctionScope.ts
@@ -22,7 +22,7 @@ export class FunctionScope {
     /**
      * The parent scope of this scope
      */
-    public parentScope: FunctionScope;
+    public parentScope: FunctionScope | undefined;
     public variableDeclarations = [] as VariableDeclaration[];
     public labelStatements = [] as LabelDeclaration[];
 
diff --git a/src/LanguageServer.spec.ts b/src/LanguageServer.spec.ts
index 6b69a899..00fa20f3 100644
--- a/src/LanguageServer.spec.ts
+++ b/src/LanguageServer.spec.ts
@@ -18,6 +18,7 @@ import { createVisitor, WalkMode } from './astUtils/visitors';
 import { tempDir, rootDir } from './testHelpers.spec';
 import { URI } from 'vscode-uri';
 import { BusyStatusTracker } from './BusyStatusTracker';
+import type { BscFile } from '.';
 
 const sinon = createSandbox();
 
@@ -119,7 +120,7 @@ describe('LanguageServer', () => {
             ${additionalXmlContents}
             <script type="text/brightscript" uri="${name}.brs" />
         </component>`;
-        program.setFile(filePath, contents);
+        return program.setFile(filePath, contents);
     }
 
     function addScriptFile(name: string, contents: string, extension = 'brs') {
@@ -226,7 +227,7 @@ describe('LanguageServer', () => {
         it('sends diagnostics that were triggered by the program instead of vscode', async () => {
             server['connection'] = server['createConnection']();
             await server['createProject'](workspacePath);
-            let stub: SinonStub;
+            let stub: SinonStub | undefined;
             const promise = new Promise((resolve) => {
                 stub = sinon.stub(connection, 'sendDiagnostics').callsFake(resolve as any);
             });
@@ -238,7 +239,7 @@ describe('LanguageServer', () => {
             `);
             program.validate();
             await promise;
-            expect(stub.called).to.be.true;
+            expect(stub!.called).to.be.true;
         });
     });
 
@@ -498,6 +499,7 @@ describe('LanguageServer', () => {
 
     describe('onSignatureHelp', () => {
         let callDocument: TextDocument;
+        let importingXmlFile: BscFile;
         const functionFileBaseName = 'buildAwesome';
         const funcDefinitionLine = 'function buildAwesome(confirm = true as Boolean)';
         beforeEach(async () => {
@@ -515,8 +517,8 @@ describe('LanguageServer', () => {
                         m.buildAwesome()
                     end if
                 end sub
-            `);
-            addXmlFile(name, `<script type="text/brightscript" uri="${functionFileBaseName}.bs" />`);
+            `)!;
+            importingXmlFile = addXmlFile(name, `<script type="text/brightscript" uri="${functionFileBaseName}.bs" />`);
         });
 
         it('should return the expected signature info when documentation is included', async () => {
@@ -585,11 +587,26 @@ describe('LanguageServer', () => {
             const signature = result.signatures[0];
             expect(signature.label).to.equal(classMethodDefinitionLine);
         });
+
+        it('should return "null" as signature and parameter when used on something with no signature', async () => {
+            const result = await server['onSignatureHelp']({
+                textDocument: {
+                    uri: importingXmlFile.pkgPath
+                },
+                position: util.createPosition(0, 5)
+            });
+
+            console.dir(result);
+
+            expect(result.signatures.length).to.equal(0);
+            expect(result.activeSignature).to.equal(null);
+            expect(result.activeParameter).to.equal(null);
+        });
     });
 
     describe('onReferences', () => {
         let functionDocument: TextDocument;
-        let referenceFileUris = [];
+        let referenceFileUris: string[] = [];
 
         beforeEach(async () => {
             server['connection'] = server['createConnection']();
@@ -601,7 +618,7 @@ describe('LanguageServer', () => {
                 function buildAwesome()
                     return 42
                 end function
-            `);
+            `)!;
 
             for (let i = 0; i < 5; i++) {
                 let name = `CallComponent${i}`;
@@ -612,7 +629,7 @@ describe('LanguageServer', () => {
                             buildAwesome()
                         end if
                     end sub
-                `);
+                `)!;
 
                 addXmlFile(name, `<script type="text/brightscript" uri="${functionFileBaseName}.brs" />`);
                 referenceFileUris.push(document.uri);
@@ -673,7 +690,7 @@ describe('LanguageServer', () => {
                 function buildAwesome()
                     return 42
                 end function
-            `);
+            `)!;
 
             const name = `CallComponent`;
             referenceDocument = addScriptFile(name, `
@@ -685,7 +702,7 @@ describe('LanguageServer', () => {
                         m.top.observeFieldScope("loadFinished", "buildAwesome")
                     end if
                 end sub
-            `);
+            `)!;
 
             addXmlFile(name, `<script type="text/brightscript" uri="${functionFileBaseName}.brs" />`);
         });
@@ -755,7 +772,7 @@ describe('LanguageServer', () => {
                         return 42
                     end function
                 end class
-            `, 'bs');
+            `, 'bs')!;
 
             const name = `CallComponent`;
             referenceDocument = addScriptFile(name, `
@@ -763,7 +780,7 @@ describe('LanguageServer', () => {
                     build = new Build()
                     build.awesome()
                 end sub
-            `);
+            `)!;
 
             addXmlFile(name, `<script type="text/brightscript" uri="${functionFileBaseName}.bs" />`);
 
@@ -799,13 +816,13 @@ describe('LanguageServer', () => {
                 function buildAwesome()
                     return 42
                 end function
-            `);
+            `)!;
 
             // We run the check twice as the first time is with it not cached and second time is with it cached
             for (let i = 0; i < 2; i++) {
-                const symbols = await server.onDocumentSymbol({
+                const symbols = (await server.onDocumentSymbol({
                     textDocument: document
-                });
+                }))!;
                 expect(symbols.length).to.equal(2);
                 expect(symbols[0].name).to.equal('pi');
                 expect(symbols[1].name).to.equal('buildAwesome');
@@ -823,18 +840,18 @@ describe('LanguageServer', () => {
                         return 42
                     end function
                 end class
-            `, 'bs');
+            `, 'bs')!;
 
             // We run the check twice as the first time is with it not cached and second time is with it cached
             for (let i = 0; i < 2; i++) {
-                const symbols = await server['onDocumentSymbol']({
+                const symbols = (await server['onDocumentSymbol']({
                     textDocument: document
-                });
+                }))!;
 
                 expect(symbols.length).to.equal(1);
                 const classSymbol = symbols[0];
                 expect(classSymbol.name).to.equal('MyFirstClass');
-                const classChildrenSymbols = classSymbol.children;
+                const classChildrenSymbols = classSymbol.children!;
                 expect(classChildrenSymbols.length).to.equal(2);
                 expect(classChildrenSymbols[0].name).to.equal('pi');
                 expect(classChildrenSymbols[1].name).to.equal('buildAwesome');
@@ -852,19 +869,19 @@ describe('LanguageServer', () => {
                         return 42
                     end function
                 end namespace
-            `, 'bs');
+            `, 'bs')!;
             program.validate();
 
             // We run the check twice as the first time is with it not cached and second time is with it cached
             for (let i = 0; i < 2; i++) {
-                const symbols = await server['onDocumentSymbol']({
+                const symbols = (await server['onDocumentSymbol']({
                     textDocument: document
-                });
+                }))!;
 
                 expect(symbols.length).to.equal(1);
                 const namespaceSymbol = symbols[0];
                 expect(namespaceSymbol.name).to.equal('MyFirstNamespace');
-                const classChildrenSymbols = namespaceSymbol.children;
+                const classChildrenSymbols = namespaceSymbol.children!;
                 expect(classChildrenSymbols.length).to.equal(2);
                 expect(classChildrenSymbols[0].name).to.equal('MyFirstNamespace.pi');
                 expect(classChildrenSymbols[1].name).to.equal('MyFirstNamespace.buildAwesome');
@@ -1047,10 +1064,10 @@ describe('LanguageServer', () => {
                 fsExtra.outputFileSync(s`${rootDir}/bsconfig.json`, '');
                 server.run();
                 await server['syncProjects']();
-                const result = await server.onExecuteCommand({
+                const result = (await server.onExecuteCommand({
                     command: CustomCommands.TranspileFile,
                     arguments: [s`${rootDir}/source/main.bs`]
-                });
+                }))!;
                 expect(
                     trim(result?.code)
                 ).to.eql(trim`
@@ -1075,7 +1092,7 @@ describe('LanguageServer', () => {
                 server.projects[0].builder.program.plugins.add({
                     name: 'test-plugin',
                     beforeProgramTranspile: (program, entries, editor) => {
-                        const file = program.getFile('source/main.bs');
+                        const file = program.getFile('source/main.bs')!;
                         if (isBrsFile(file)) {
                             file.ast.walk(createVisitor({
                                 LiteralExpression: (expression) => {
@@ -1091,10 +1108,10 @@ describe('LanguageServer', () => {
                     afterProgramTranspile: afterSpy
                 });
 
-                const result = await server.onExecuteCommand({
+                const result = (await server.onExecuteCommand({
                     command: CustomCommands.TranspileFile,
                     arguments: [s`${rootDir}/source/main.bs`]
-                });
+                }))!;
                 expect(
                     trim(result?.code)
                 ).to.eql(trim`
diff --git a/src/LanguageServer.ts b/src/LanguageServer.ts
index 3035709a..e3aab7aa 100644
--- a/src/LanguageServer.ts
+++ b/src/LanguageServer.ts
@@ -50,7 +50,7 @@ import type { BusyStatus } from './BusyStatusTracker';
 import { BusyStatusTracker } from './BusyStatusTracker';
 
 export class LanguageServer {
-    private connection = undefined as Connection;
+    private connection = undefined as any as Connection;
 
     public projects = [] as Project[];
 
@@ -84,7 +84,7 @@ export class LanguageServer {
         return createConnection(ProposedFeatures.all);
     }
 
-    private loggerSubscription: () => void;
+    private loggerSubscription: (() => void) | undefined;
 
     private keyedThrottler = new KeyedThrottler(this.debounceTimeout);
 
@@ -245,7 +245,7 @@ export class LanguageServer {
         };
     }
 
-    private initialProjectsCreated: Promise<any>;
+    private initialProjectsCreated: Promise<any> | undefined;
 
     /**
      * Ask the client for the list of `files.exclude` patterns. Useful when determining if we should process a file
@@ -740,7 +740,7 @@ export class LanguageServer {
         //clone the diagnostics for each code action, since certain diagnostics can have circular reference properties that kill the language server if serialized
         for (const codeAction of codeActions) {
             if (codeAction.diagnostics) {
-                codeAction.diagnostics = codeAction.diagnostics.map(x => util.toDiagnostic(x, params.textDocument.uri));
+                codeAction.diagnostics = codeAction.diagnostics?.map(x => util.toDiagnostic(x, params.textDocument.uri));
             }
         }
         return codeActions;
@@ -1214,7 +1214,7 @@ export class LanguageServer {
 
             const activeSignature = signatures.length > 0 ? 0 : null;
 
-            const activeParameter = activeSignature >= 0 ? signatures[activeSignature]?.index : null;
+            const activeParameter = activeSignature !== null ? signatures[activeSignature]?.index : null;
 
             let results: SignatureHelp = {
                 signatures: signatures.map((s) => s.signature),
@@ -1244,7 +1244,7 @@ export class LanguageServer {
             await Promise.all(this.getProjects().map(project => {
                 return project.builder.program.getReferences(srcPath, position);
             })),
-            c => c
+            c => c ?? []
         );
         return results.filter((r) => r);
     }
@@ -1260,7 +1260,7 @@ export class LanguageServer {
 
     @AddStackToErrorMessage
     @TrackBusyStatus
-    private async onFullSemanticTokens(params: SemanticTokensParams) {
+    private async onFullSemanticTokens(params: SemanticTokensParams): Promise<SemanticTokens | undefined> {
         await this.waitAllProjectFirstRuns();
         //wait for the file to settle (in case there are multiple file changes in quick succession)
         await this.keyedThrottler.onIdleOnce(util.uriToPath(params.textDocument.uri), true);
@@ -1272,9 +1272,11 @@ export class LanguageServer {
             //find the first program that has this file, since it would be incredibly inefficient to generate semantic tokens for the same file multiple times.
             if (project.builder.program.hasFile(srcPath)) {
                 let semanticTokens = project.builder.program.getSemanticTokens(srcPath);
-                return {
-                    data: encodeSemanticTokens(semanticTokens)
-                } as SemanticTokens;
+                if (semanticTokens !== undefined) {
+                    return {
+                        data: encodeSemanticTokens(semanticTokens)
+                    } as SemanticTokens;
+                }
             }
         }
     }
diff --git a/src/Logger.spec.ts b/src/Logger.spec.ts
index 60248539..208a9268 100644
--- a/src/Logger.spec.ts
+++ b/src/Logger.spec.ts
@@ -109,14 +109,14 @@ describe('Logger', () => {
         it('calls action even if logLevel is wrong', () => {
             logger.logLevel = LogLevel.error;
             const spy = sinon.spy();
-            logger.time(LogLevel.info, null, spy);
+            logger.time(LogLevel.info, [], spy);
             expect(spy.called).to.be.true;
         });
 
         it('runs timer when loglevel is right', () => {
             logger.logLevel = LogLevel.log;
             const spy = sinon.spy();
-            logger.time(LogLevel.log, null, spy);
+            logger.time(LogLevel.log, [], spy);
             expect(spy.called).to.be.true;
         });
 
@@ -126,13 +126,13 @@ describe('Logger', () => {
                 return true;
             });
             expect(
-                logger.time(LogLevel.log, null, spy)
+                logger.time(LogLevel.log, [], spy)
             ).to.be.true;
             expect(spy.called).to.be.true;
         });
 
         it('gives callable pause and resume functions even when not running timer', () => {
-            logger.time(LogLevel.info, null, (pause, resume) => {
+            logger.time(LogLevel.info, [], (pause, resume) => {
                 pause();
                 resume();
             });
diff --git a/src/Logger.ts b/src/Logger.ts
index cac73702..088c2104 100644
--- a/src/Logger.ts
+++ b/src/Logger.ts
@@ -22,7 +22,7 @@ export class Logger {
         logLevel?: LogLevel,
         public prefix?: string
     ) {
-        this.logLevel = logLevel;
+        this.logLevel = logLevel ?? LogLevel.log;
     }
 
     public get logLevel() {
@@ -45,7 +45,7 @@ export class Logger {
         if (this._logLevel === LogLevel.trace) {
             method = console.trace;
         }
-        let finalArgs = [];
+        let finalArgs: any[] = [];
         //evaluate any functions to get their values.
         //This allows more complicated values to only be evaluated if this log level is active
         for (let arg of args) {
diff --git a/src/PluginInterface.spec.ts b/src/PluginInterface.spec.ts
index 47fd4fea..1e839829 100644
--- a/src/PluginInterface.spec.ts
+++ b/src/PluginInterface.spec.ts
@@ -1,3 +1,5 @@
+/* eslint-disable @typescript-eslint/prefer-ts-expect-error */
+/* eslint-disable @typescript-eslint/ban-ts-comment */
 import { expect } from './chai-config.spec';
 import * as sinon from 'sinon';
 import { Logger } from './Logger';
@@ -16,8 +18,10 @@ describe('PluginInterface', () => {
             name: 'allows adding a plugin',
             beforePublish: beforePublish
         };
+        //@ts-ignore the current definition of `emit` doesn't like this third argument
         pluginInterface.emit('beforePublish', undefined, []);
         pluginInterface.add(plugin);
+        //@ts-ignore the current definition of `emit` doesn't like this third argument
         pluginInterface.emit('beforePublish', undefined, []);
         expect(beforePublish.callCount).to.equal(1);
     });
@@ -39,6 +43,7 @@ describe('PluginInterface', () => {
         };
         pluginInterface.add(plugin);
         pluginInterface.add(plugin);
+        //@ts-ignore the current definition of `emit` doesn't like this third argument
         pluginInterface.emit('beforePublish', undefined, []);
         expect(beforePublish.callCount).to.equal(1);
         pluginInterface.remove(plugin);
@@ -52,9 +57,11 @@ describe('PluginInterface', () => {
             beforePublish: beforePublish
         };
         pluginInterface.add(plugin);
+        //@ts-ignore the current definition of `emit` doesn't like this third argument
         pluginInterface.emit('beforePublish', undefined, []);
         expect(beforePublish.callCount).to.equal(1);
         pluginInterface.remove(plugin);
+        //@ts-ignore the current definition of `emit` doesn't like this third argument
         pluginInterface.emit('beforePublish', undefined, []);
         expect(beforePublish.callCount).to.equal(1);
     });
diff --git a/src/PluginInterface.ts b/src/PluginInterface.ts
index 773e2bbc..e25de31d 100644
--- a/src/PluginInterface.ts
+++ b/src/PluginInterface.ts
@@ -43,7 +43,7 @@ export default class PluginInterface<T extends CompilerPlugin = CompilerPlugin>
     /**
      * Should plugin errors cause the program to fail, or should they be caught and simply logged
      */
-    private suppressErrors: boolean;
+    private suppressErrors: boolean | undefined;
 
     /**
      * Call `event` on plugins
diff --git a/src/Program.spec.ts b/src/Program.spec.ts
index 04b40598..5b2eb101 100644
--- a/src/Program.spec.ts
+++ b/src/Program.spec.ts
@@ -21,6 +21,7 @@ import { isBrsFile } from './astUtils/reflection';
 import type { LiteralExpression } from './parser/Expression';
 import type { AstEditor } from './astUtils/AstEditor';
 import { tempDir, rootDir, stagingDir } from './testHelpers.spec';
+import type { BsDiagnostic } from './interfaces';
 
 let sinon = sinonImport.createSandbox();
 
@@ -267,7 +268,7 @@ describe('Program', () => {
                 message: 'message',
                 file: undefined,
                 range: undefined
-            }];
+            }] as any as BsDiagnostic[];
             program.addDiagnostics(expected);
             const actual = (program as any).diagnostics;
             expect(actual).to.deep.equal(expected);
@@ -720,7 +721,7 @@ describe('Program', () => {
 
     describe('reloadFile', () => {
         it('picks up new files in a scope when an xml file is loaded', () => {
-            program.options.ignoreErrorCodes.push(1013);
+            program.options.ignoreErrorCodes!.push(1013);
             program.setFile('components/component1.xml', trim`
                 <?xml version="1.0" encoding="utf-8" ?>
                 <component name="HeroScene" extends="Scene">
@@ -766,7 +767,7 @@ describe('Program', () => {
         });
 
         it('reloads referenced fles when xml file changes', () => {
-            program.options.ignoreErrorCodes.push(1013);
+            program.options.ignoreErrorCodes!.push(1013);
             program.setFile('components/component1.brs', '');
 
             let xmlFile = program.setFile('components/component1.xml', trim`
@@ -1735,19 +1736,19 @@ describe('Program', () => {
     });
 
     it('does not create map by default', async () => {
-        fsExtra.ensureDirSync(program.options.stagingDir);
+        fsExtra.ensureDirSync(program.options.stagingDir!);
         program.setFile('source/main.brs', `
             sub main()
             end sub
         `);
         program.validate();
-        await program.transpile([], program.options.stagingDir);
+        await program.transpile([], program.options.stagingDir!);
         expect(fsExtra.pathExistsSync(s`${stagingDir}/source/main.brs`)).is.true;
         expect(fsExtra.pathExistsSync(s`${stagingDir}/source/main.brs.map`)).is.false;
     });
 
     it('creates sourcemap for brs and xml files', async () => {
-        fsExtra.ensureDirSync(program.options.stagingDir);
+        fsExtra.ensureDirSync(program.options.stagingDir!);
         program.setFile('source/main.brs', `
             sub main()
             end sub
@@ -1770,27 +1771,27 @@ describe('Program', () => {
             dest: s`components/comp1.xml`
         }];
         program.options.sourceMap = true;
-        await program.transpile(filePaths, program.options.stagingDir);
+        await program.transpile(filePaths, program.options.stagingDir!);
 
         expect(fsExtra.pathExistsSync(s`${stagingDir}/source/main.brs.map`)).is.true;
         expect(fsExtra.pathExistsSync(s`${stagingDir}/components/comp1.xml.map`)).is.true;
     });
 
     it('copies the bslib.brs file', async () => {
-        fsExtra.ensureDirSync(program.options.stagingDir);
+        fsExtra.ensureDirSync(program.options.stagingDir!);
         program.validate();
 
-        await program.transpile([], program.options.stagingDir);
+        await program.transpile([], program.options.stagingDir!);
 
         expect(fsExtra.pathExistsSync(s`${stagingDir}/source/bslib.brs`)).is.true;
     });
 
     it('copies the bslib.brs file to optionally specified directory', async () => {
-        fsExtra.ensureDirSync(program.options.stagingDir);
+        fsExtra.ensureDirSync(program.options.stagingDir!);
         program.options.bslibDestinationDir = 'source/opt';
         program.validate();
 
-        await program.transpile([], program.options.stagingDir);
+        await program.transpile([], program.options.stagingDir!);
 
         expect(fsExtra.pathExistsSync(s`${stagingDir}/source/opt/bslib.brs`)).is.true;
     });
@@ -1873,7 +1874,7 @@ describe('Program', () => {
         }, {
             src: s`${rootDir}/source/main.bs`,
             dest: 'source/main.bs'
-        }], program.options.stagingDir);
+        }], program.options.stagingDir!);
 
         //entries should now be in alphabetic order
         expect(
@@ -1960,7 +1961,7 @@ describe('Program', () => {
                     print "hello world"
                 end sub
             `);
-            let literalExpression: LiteralExpression;
+            let literalExpression: LiteralExpression | undefined;
             //replace all strings with "goodbye world"
             program.plugins.add({
                 name: 'TestPlugin',
@@ -1989,7 +1990,7 @@ describe('Program', () => {
             );
 
             //our literalExpression should have been restored to its original value
-            expect(literalExpression.token.text).to.eql('"hello world"');
+            expect(literalExpression!.token.text).to.eql('"hello world"');
         });
 
         it('handles AstEditor for beforeProgramTranspile', async () => {
@@ -1998,7 +1999,7 @@ describe('Program', () => {
                     print "hello world"
                 end sub
             `);
-            let literalExpression: LiteralExpression;
+            let literalExpression: LiteralExpression | undefined;
             //replace all strings with "goodbye world"
             program.plugins.add({
                 name: 'TestPlugin',
@@ -2025,7 +2026,7 @@ describe('Program', () => {
             );
 
             //our literalExpression should have been restored to its original value
-            expect(literalExpression.token.text).to.eql('"hello world"');
+            expect(literalExpression!.token.text).to.eql('"hello world"');
         });
 
         it('copies bslib.brs when no ropm version was found', async () => {
@@ -2046,7 +2047,7 @@ describe('Program', () => {
                     print SOURCE_LINE_NUM
                 end sub
             `);
-            await program.transpile([], program.options.stagingDir);
+            await program.transpile([], program.options.stagingDir!);
             expect(trimMap(
                 fsExtra.readFileSync(s`${stagingDir}/source/logger.brs`).toString()
             )).to.eql(trim`
@@ -2062,7 +2063,7 @@ describe('Program', () => {
                     print "logInfo"
                 end sub
             `);
-            await program.transpile([], program.options.stagingDir);
+            await program.transpile([], program.options.stagingDir!);
             expect(trimMap(
                 fsExtra.readFileSync(s`${stagingDir}/source/logger.brs`).toString()
             )).to.eql(trim`
@@ -2078,7 +2079,7 @@ describe('Program', () => {
                 <component name="Component1" extends="Scene">
                 </component>
             `);
-            await program.transpile([], program.options.stagingDir);
+            await program.transpile([], program.options.stagingDir!);
             expect(trimMap(
                 fsExtra.readFileSync(s`${stagingDir}/components/Component1.xml`).toString()
             )).to.eql(trim`
@@ -2096,7 +2097,7 @@ describe('Program', () => {
                 <component name="Component1" extends="Scene">
                 </component>
             `);
-            await program.transpile([], program.options.stagingDir);
+            await program.transpile([], program.options.stagingDir!);
             expect(trimMap(
                 fsExtra.readFileSync(s`${stagingDir}/components/Component1.xml`).toString()
             )).to.eql(trim`
diff --git a/src/Program.ts b/src/Program.ts
index 5cb8cbe0..a6e36a3a 100644
--- a/src/Program.ts
+++ b/src/Program.ts
@@ -991,7 +991,7 @@ export class Program {
     /**
      * Get semantic tokens for the specified file
      */
-    public getSemanticTokens(srcPath: string) {
+    public getSemanticTokens(srcPath: string): SemanticToken[] | undefined {
         const file = this.getFile(srcPath);
         if (file) {
             const result = [] as SemanticToken[];
diff --git a/src/ProgramBuilder.spec.ts b/src/ProgramBuilder.spec.ts
index 9f240341..63ac78bd 100644
--- a/src/ProgramBuilder.spec.ts
+++ b/src/ProgramBuilder.spec.ts
@@ -274,7 +274,7 @@ describe('ProgramBuilder', () => {
 
         let diagnostics = createBsDiagnostic('p1', ['m1']);
         let f1 = diagnostics[0].file as BrsFile;
-        f1.fileContents = null;
+        (f1.fileContents as any) = null;
         sinon.stub(builder, 'getDiagnostics').returns(diagnostics);
 
         sinon.stub(builder.program, 'getFile').returns(f1);
@@ -292,7 +292,7 @@ describe('ProgramBuilder', () => {
         let diagnostics = createBsDiagnostic('p1', ['m1']);
         sinon.stub(builder, 'getDiagnostics').returns(diagnostics);
 
-        sinon.stub(builder.program, 'getFile').returns(null);
+        sinon.stub(builder.program, 'getFile').returns(null as any);
 
         let printStub = sinon.stub(diagnosticUtils, 'printDiagnostic');
 
@@ -351,8 +351,8 @@ describe('ProgramBuilder', () => {
 });
 
 function createBsDiagnostic(filePath: string, messages: string[]): BsDiagnostic[] {
-    let file = new BrsFile(filePath, filePath, null);
-    let diagnostics = [];
+    let file = new BrsFile(filePath, filePath, null as any);
+    let diagnostics: BsDiagnostic[] = [];
     for (let message of messages) {
         let d = createDiagnostic(file, 1, message);
         d.file = file;
diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts
index fb7a915d..282506c2 100644
--- a/src/Scope.spec.ts
+++ b/src/Scope.spec.ts
@@ -9,7 +9,7 @@ import PluginInterface from './PluginInterface';
 import { expectDiagnostics, expectZeroDiagnostics, trim } from './testHelpers.spec';
 import { Logger } from './Logger';
 import type { BrsFile } from './files/BrsFile';
-import type { FunctionStatement, NamespaceStatement } from './parser/Statement';
+import type { NamespaceStatement } from './parser/Statement';
 import type { OnScopeValidateEvent } from './interfaces';
 
 describe('Scope', () => {
@@ -30,7 +30,7 @@ describe('Scope', () => {
     it('getEnumMemberFileLink does not crash on undefined name', () => {
         program.setFile('source/main.bs', ``);
         const scope = program.getScopesForFile('source/main.bs')[0];
-        scope.getEnumMemberFileLink(null);
+        scope.getEnumMemberFileLink(null as any);
         //test passes if this doesn't explode
     });
 
@@ -1481,7 +1481,7 @@ describe('Scope', () => {
             `);
             program.setFile(s`components/child.brs`, ``);
             program.validate();
-            let childScope = program.getComponentScope('child');
+            let childScope = program.getComponentScope('child')!;
             expect(childScope.getAllCallables().map(x => x.callable.name)).not.to.include('parentSub');
 
             program.setFile('components/parent.xml', trim`
@@ -1542,7 +1542,7 @@ describe('Scope', () => {
                     end function
                 end namespace
             `);
-            delete ((file.ast.statements[0] as NamespaceStatement).body.statements[0] as FunctionStatement).name;
+            delete ((file.ast.statements[0] as NamespaceStatement).body.statements[0] as any).name;
             program.validate();
             program['scopes']['source'].buildNamespaceLookup();
         });
diff --git a/src/Scope.ts b/src/Scope.ts
index 3804ccec..7302a1f3 100644
--- a/src/Scope.ts
+++ b/src/Scope.ts
@@ -917,8 +917,8 @@ export class Scope {
     }
 
     private validateClasses() {
-        let validator = new BsClassValidator();
-        validator.validate(this);
+        let validator = new BsClassValidator(this);
+        validator.validate();
         this.diagnostics.push(...validator.diagnostics);
     }
 
diff --git a/src/Stopwatch.ts b/src/Stopwatch.ts
index 9c743102..2866e9a2 100644
--- a/src/Stopwatch.ts
+++ b/src/Stopwatch.ts
@@ -6,7 +6,7 @@ export class Stopwatch {
     /**
      * The number of milliseconds when the stopwatch was started.
      */
-    private startTime: number;
+    private startTime: number | undefined;
     start() {
         this.startTime = performance.now();
     }
@@ -17,7 +17,7 @@ export class Stopwatch {
         this.startTime = undefined;
     }
     reset() {
-        this.totalMilliseconds = undefined;
+        this.totalMilliseconds = 0;
         this.startTime = undefined;
     }
     getDurationText() {
diff --git a/src/SymbolTable.spec.ts b/src/SymbolTable.spec.ts
index 8267c682..0a4b2b44 100644
--- a/src/SymbolTable.spec.ts
+++ b/src/SymbolTable.spec.ts
@@ -13,36 +13,36 @@ describe('SymbolTable', () => {
 
     it('is case insensitive', () => {
         const st = new SymbolTable('Child');
-        st.addSymbol('foo', null, new StringType());
-        expect(st.getSymbol('FOO').length).eq(1);
-        expect(st.getSymbol('FOO')[0].type.toString()).eq('string');
+        st.addSymbol('foo', null as any, new StringType());
+        expect(st.getSymbol('FOO')!.length).eq(1);
+        expect(st.getSymbol('FOO')![0].type.toString()).eq('string');
     });
 
     it('stores all previous symbols', () => {
         const st = new SymbolTable('Child');
-        st.addSymbol('foo', null, new StringType());
-        st.addSymbol('foo', null, new IntegerType());
-        expect(st.getSymbol('FOO').length).eq(2);
+        st.addSymbol('foo', null as any, new StringType());
+        st.addSymbol('foo', null as any, new IntegerType());
+        expect(st.getSymbol('FOO')!.length).eq(2);
     });
 
 
     it('reads from parent symbol table if not found in current', () => {
         const st = new SymbolTable('Child', () => parent);
-        parent.addSymbol('foo', null, new StringType());
-        expect(st.getSymbol('foo')[0].type.toString()).eq('string');
+        parent.addSymbol('foo', null as any, new StringType());
+        expect(st.getSymbol('foo')![0].type.toString()).eq('string');
     });
 
     it('reads from current table if it exists', () => {
         const st = new SymbolTable('Child', () => parent);
-        parent.addSymbol('foo', null, new StringType());
-        st.addSymbol('foo', null, new IntegerType());
-        expect(st.getSymbol('foo')[0].type.toString()).eq('integer');
+        parent.addSymbol('foo', null as any, new StringType());
+        st.addSymbol('foo', null as any, new IntegerType());
+        expect(st.getSymbol('foo')![0].type.toString()).eq('integer');
     });
 
     it('correct checks if a symbol is in the table using hasSymbol', () => {
         const child = new SymbolTable('Child', () => parent);
-        parent.addSymbol('foo', null, new StringType());
-        child.addSymbol('bar', null, new IntegerType());
+        parent.addSymbol('foo', null as any, new StringType());
+        child.addSymbol('bar', null as any, new IntegerType());
         expect(parent.hasSymbol('foo')).to.be.true;
         expect(parent.hasSymbol('bar')).to.be.false;
         expect(child.hasSymbol('foo')).to.be.true;
@@ -54,25 +54,25 @@ describe('SymbolTable', () => {
 
         it('adds each symbol to the table', () => {
             const st = new SymbolTable('Child');
-            st.addSymbol('foo', null, new StringType());
+            st.addSymbol('foo', null as any, new StringType());
             const otherTable = new SymbolTable('OtherTable');
-            otherTable.addSymbol('bar', null, new IntegerType());
-            otherTable.addSymbol('foo', null, new IntegerType());
+            otherTable.addSymbol('bar', null as any, new IntegerType());
+            otherTable.addSymbol('foo', null as any, new IntegerType());
             st.mergeSymbolTable(otherTable);
         });
     });
 
     it('searches siblings before parents', () => {
-        parent.addSymbol('alpha', null, new StringType());
+        parent.addSymbol('alpha', null as any, new StringType());
 
         const child = new SymbolTable('Child', () => parent);
 
         const sibling = new SymbolTable('Sibling');
         child.addSibling(sibling);
-        sibling.addSymbol('alpha', null, new BooleanType());
+        sibling.addSymbol('alpha', null as any, new BooleanType());
 
         expect(
-            child.getSymbol('alpha').map(x => x.type.toTypeString())
+            child.getSymbol('alpha')!.map(x => x.type.toTypeString())
         ).to.eql([
             'boolean'
         ]);
diff --git a/src/SymbolTable.ts b/src/SymbolTable.ts
index b2e1c53a..959486cc 100644
--- a/src/SymbolTable.ts
+++ b/src/SymbolTable.ts
@@ -40,7 +40,7 @@ export class SymbolTable {
     /**
      * The parent SymbolTable (if there is one)
      */
-    public get parent() {
+    public get parent(): SymbolTable | undefined {
         return this.parentProviders[this.parentProviders.length - 1]?.();
     }
 
@@ -80,9 +80,9 @@ export class SymbolTable {
      * @param searchParent should we look to our parent if we don't have the symbol?
      * @returns An array of BscSymbols - one for each time this symbol had a type implicitly defined
      */
-    getSymbol(name: string, searchParent = true): BscSymbol[] {
+    getSymbol(name: string, searchParent = true): BscSymbol[] | undefined {
         const key = name.toLowerCase();
-        let result: BscSymbol[];
+        let result: BscSymbol[] | undefined;
         // look in our map first
         if ((result = this.symbolMap.get(key))) {
             return result;
@@ -107,7 +107,7 @@ export class SymbolTable {
         if (!this.symbolMap.has(key)) {
             this.symbolMap.set(key, []);
         }
-        this.symbolMap.get(key).push({
+        this.symbolMap.get(key)?.push({
             name: name,
             range: range,
             type: type
@@ -157,4 +157,4 @@ export interface BscSymbol {
 /**
  * A function that returns a symbol table.
  */
-export type SymbolTableProvider = () => SymbolTable;
+export type SymbolTableProvider = () => SymbolTable | undefined;
diff --git a/src/XmlScope.spec.ts b/src/XmlScope.spec.ts
index a8141c60..1821534a 100644
--- a/src/XmlScope.spec.ts
+++ b/src/XmlScope.spec.ts
@@ -40,7 +40,7 @@ describe('XmlScope', () => {
                 <component name="Child" extends="Parent">
                 </component>
             `);
-            let childScope = program.getComponentScope('Child');
+            let childScope = program.getComponentScope('Child')!;
 
             program.validate();
 
@@ -86,7 +86,7 @@ describe('XmlScope', () => {
                 </component>
             `);
             program.validate();
-            expect(program.getComponentScope('Child').getOwnFiles()[0]).to.equal(xmlFile);
+            expect(program.getComponentScope('Child')!.getOwnFiles()[0]).to.equal(xmlFile);
         });
     });
 
@@ -113,7 +113,7 @@ describe('XmlScope', () => {
                 end sub
             `);
             program.validate();
-            let childScope = program.getComponentScope('child');
+            let childScope = program.getComponentScope('child')!;
             expectDiagnostics(childScope, [{
                 ...DiagnosticMessages.xmlFunctionNotFound('func2'),
                 range: Range.create(4, 24, 4, 29)
@@ -157,7 +157,7 @@ describe('XmlScope', () => {
                 end sub
             `);
             program.validate();
-            expectDiagnostics(program.getComponentScope('child'), [{
+            expectDiagnostics(program.getComponentScope('child')!, [{
                 ...DiagnosticMessages.xmlInvalidFieldType('no'),
                 range: Range.create(4, 33, 4, 35)
             }, {
diff --git a/src/astUtils/AstEditor.spec.ts b/src/astUtils/AstEditor.spec.ts
index c1e1bda2..31da12a1 100644
--- a/src/astUtils/AstEditor.spec.ts
+++ b/src/astUtils/AstEditor.spec.ts
@@ -124,7 +124,7 @@ describe('AstEditor', () => {
 
     it('restores array after being removed', () => {
         editor.removeFromArray(obj.hobbies, 0);
-        editor.setProperty(obj, 'hobbies', undefined);
+        editor.setProperty(obj, 'hobbies', undefined as any);
         expect(obj.hobbies).to.be.undefined;
         editor.undoAll();
         expect(obj.hobbies).to.eql(['gaming', 'reading', 'cycling']);
@@ -139,7 +139,7 @@ describe('AstEditor', () => {
         editor.removeFromArray(obj.hobbies, 0);
         editor.removeFromArray(obj.hobbies, 0);
         editor.removeFromArray(obj.hobbies, 0);
-        editor.setProperty(obj, 'hobbies', undefined);
+        editor.setProperty(obj, 'hobbies', undefined as any);
 
         expect(obj).to.eql({
             name: 'bob',
@@ -294,9 +294,9 @@ describe('AstEditor', () => {
 
     it('edit handles missing functions', () => {
         //missing undo
-        editor.edit((data) => { }, undefined);
+        editor.edit((data) => { }, undefined as any);
         //missing edit
-        editor.edit(undefined, (data) => { });
+        editor.edit(undefined as any, (data) => { });
 
         //test passes if no exceptions were thrown
     });
diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts
index b353b587..91d0bdfc 100644
--- a/src/astUtils/reflection.ts
+++ b/src/astUtils/reflection.ts
@@ -23,15 +23,15 @@ import type { AstNode, Expression, Statement } from '../parser/AstNode';
 
 // File reflection
 
-export function isBrsFile(file: (BscFile | File)): file is BrsFile {
+export function isBrsFile(file: BscFile | File | undefined): file is BrsFile {
     return file?.constructor.name === 'BrsFile';
 }
 
-export function isXmlFile(file: (BscFile)): file is XmlFile {
+export function isXmlFile(file: BscFile | undefined): file is XmlFile {
     return file?.constructor.name === 'XmlFile';
 }
 
-export function isXmlScope(scope: (Scope)): scope is XmlScope {
+export function isXmlScope(scope: Scope | undefined): scope is XmlScope {
     return scope?.constructor.name === 'XmlScope';
 }
 
diff --git a/src/astUtils/stackedVisitor.spec.ts b/src/astUtils/stackedVisitor.spec.ts
index 1b0c1d0b..3b978aa1 100644
--- a/src/astUtils/stackedVisitor.spec.ts
+++ b/src/astUtils/stackedVisitor.spec.ts
@@ -43,7 +43,7 @@ describe('createStackedVisitor', () => {
             assert(stack !== undefined, 'stack is undefined');
             actual.push(`${stack.length ? stack.map(e => e.id).join('/') + '/' : ''}${item.id}`);
         });
-        visitStruct(test1Struct, undefined, stackedVisitor);
+        visitStruct(test1Struct, undefined as any, stackedVisitor);
         expect(actual).to.deep.equal([
             '1',
             '1/2',
@@ -73,7 +73,7 @@ describe('createStackedVisitor', () => {
                 assert(stack !== undefined, 'stack is undefined');
                 actual.push(`<${stack.map(e => e.id).join('/')}:${popped.id}`);
             });
-        visitStruct(test1Struct, undefined, stackedVisitor);
+        visitStruct(test1Struct, undefined as any, stackedVisitor);
         expect(actual).to.deep.equal([
             '>1:1',
             '>1/3:3',
diff --git a/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts b/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts
index f5e83ada..7accea57 100644
--- a/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts
+++ b/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts
@@ -93,7 +93,7 @@ describe('CodeActionsProcessor', () => {
                 util.createRange(1, 5, 1, 5)
             );
             expect(
-                codeActions[0].edit.changes[URI.file(s`${rootDir}/components/comp1.xml`).toString()][0].range
+                codeActions[0].edit!.changes![URI.file(s`${rootDir}/components/comp1.xml`).toString()][0].range
             ).to.eql(
                 util.createRange(1, 51, 1, 51)
             );
diff --git a/src/bscPlugin/hover/HoverProcessor.ts b/src/bscPlugin/hover/HoverProcessor.ts
index 951d6d9d..17a7d820 100644
--- a/src/bscPlugin/hover/HoverProcessor.ts
+++ b/src/bscPlugin/hover/HoverProcessor.ts
@@ -17,7 +17,7 @@ export class HoverProcessor {
     }
 
     public process() {
-        let hover: Hover;
+        let hover: Hover | undefined;
         if (isBrsFile(this.event.file)) {
             hover = this.getBrsFileHover(this.event.file);
         } else if (isXmlFile(this.event.file)) {
@@ -40,7 +40,7 @@ export class HoverProcessor {
         return parts.join('\n');
     }
 
-    private getBrsFileHover(file: BrsFile): Hover {
+    private getBrsFileHover(file: BrsFile): Hover | undefined {
         const scope = this.event.scopes[0];
         const fence = (code: string) => util.mdFence(code, 'brightscript');
         //get the token at the position
@@ -56,7 +56,7 @@ export class HoverProcessor {
 
         //throw out invalid tokens and the wrong kind of tokens
         if (!token || !hoverTokenTypes.includes(token.kind)) {
-            return null;
+            return undefined;
         }
 
         const expression = file.getClosestExpression(this.event.position);
@@ -127,6 +127,9 @@ export class HoverProcessor {
      */
     private getTokenDocumentation(tokens: Token[], token?: Token) {
         const comments = [] as Token[];
+        if (!token) {
+            return undefined;
+        }
         const idx = tokens?.indexOf(token);
         if (!idx || idx === -1) {
             return undefined;
@@ -150,7 +153,7 @@ export class HoverProcessor {
         }
     }
 
-    private getXmlFileHover(file: XmlFile) {
+    private getXmlFileHover(file: XmlFile): Hover | undefined {
         //TODO add xml hovers
         return undefined;
     }
diff --git a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts
index fc323282..7dc8702e 100644
--- a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts
+++ b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts
@@ -25,7 +25,7 @@ describe('BrsFileSemanticTokensProcessor', () => {
             expectZeroDiagnostics(program);
         }
         const result = util.sortByRange(
-            program.getSemanticTokens(file.srcPath)
+            program.getSemanticTokens(file.srcPath)!
         );
 
         //sort modifiers
diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts
index d345e78a..dc73097e 100644
--- a/src/bscPlugin/validation/BrsFileValidator.spec.ts
+++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts
@@ -44,11 +44,11 @@ describe('BrsFileValidator', () => {
                 end class
             end namespace
         `);
-        const namespace = ast.findChild<NamespaceStatement>(isNamespaceStatement);
-        const deltaClass = namespace.findChild<ClassStatement>(isClassStatement);
+        const namespace = ast.findChild<NamespaceStatement>(isNamespaceStatement)!;
+        const deltaClass = namespace.findChild<ClassStatement>(isClassStatement)!;
         expect(deltaClass.parent).to.equal(namespace.body);
 
-        const charlie = (deltaClass.parentClassName.expression as DottedGetExpression);
+        const charlie = (deltaClass.parentClassName!.expression as DottedGetExpression);
         expect(charlie.parent).to.equal(deltaClass.parentClassName);
 
         const bravo = charlie.obj as DottedGetExpression;
diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts
index bace8cff..15d56c53 100644
--- a/src/bscPlugin/validation/BrsFileValidator.ts
+++ b/src/bscPlugin/validation/BrsFileValidator.ts
@@ -152,7 +152,7 @@ export class BrsFileValidator {
      *  - inside a namespace
      * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
      */
-    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => Range) {
+    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
         //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
         if (isNamespaceStatement(statement.parent?.parent) || (isBody(statement.parent) && !statement.parent?.parent)) {
             return;
diff --git a/src/diagnosticUtils.ts b/src/diagnosticUtils.ts
index bf88de75..0a883503 100644
--- a/src/diagnosticUtils.ts
+++ b/src/diagnosticUtils.ts
@@ -13,7 +13,7 @@ export function getPrintDiagnosticOptions(options: BsConfig) {
 
     let emitFullPaths = options?.emitFullPaths === true;
 
-    let diagnosticLevel = options?.diagnosticLevel || 'warn';
+    let diagnosticLevel = options?.diagnosticLevel ?? 'warn';
 
     let diagnosticSeverityMap = {} as Record<string, DiagnosticSeverity>;
     diagnosticSeverityMap.info = DiagnosticSeverity.Information;
@@ -56,7 +56,7 @@ export function getPrintDiagnosticOptions(options: BsConfig) {
 export function printDiagnostic(
     options: ReturnType<typeof getPrintDiagnosticOptions>,
     severity: DiagnosticSeverity,
-    filePath: string,
+    filePath: string | undefined,
     lines: string[],
     diagnostic: BsDiagnostic,
     relatedInformation?: Array<{ range: Range; filePath: string; message: string }>
@@ -150,7 +150,7 @@ export function getDiagnosticLine(diagnostic: BsDiagnostic, diagnosticLine: stri
 /**
  * Given a diagnostic, compute the range for the squiggly
  */
-export function getDiagnosticSquigglyText(line: string, startCharacter: number, endCharacter: number) {
+export function getDiagnosticSquigglyText(line: string | undefined, startCharacter: number | undefined, endCharacter: number | undefined) {
     let squiggle: string;
     //fill the entire line
     if (
diff --git a/src/files/BrsFile.Class.spec.ts b/src/files/BrsFile.Class.spec.ts
index dad7a61c..63ad7f8f 100644
--- a/src/files/BrsFile.Class.spec.ts
+++ b/src/files/BrsFile.Class.spec.ts
@@ -112,7 +112,7 @@ describe('BrsFile BrighterScript classes', () => {
         expectZeroDiagnostics(program);
         let duckClass = file.parser.references.classStatements.find(x => x.name.text.toLowerCase() === 'duck');
         expect(duckClass).to.exist;
-        expect(duckClass.memberMap['move']).to.exist;
+        expect(duckClass!.memberMap['move']).to.exist;
     });
 
     it('supports various namespace configurations', () => {
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index 9a71d26a..6cc4ec7e 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -1,7 +1,7 @@
 import { assert, expect } from '../chai-config.spec';
 import * as sinonImport from 'sinon';
 import { CompletionItemKind, Position, Range } from 'vscode-languageserver';
-import type { Callable, CommentFlag, VariableDeclaration } from '../interfaces';
+import type { BsDiagnostic, Callable, CommentFlag, VariableDeclaration } from '../interfaces';
 import { Program } from '../Program';
 import { BooleanType } from '../types/BooleanType';
 import { DynamicType } from '../types/DynamicType';
@@ -156,10 +156,10 @@ describe('BrsFile', () => {
     });
 
     it('allows adding diagnostics', () => {
-        const expected = [{
+        const expected: BsDiagnostic[] = [{
             message: 'message',
-            file: undefined,
-            range: undefined
+            file: undefined as any,
+            range: undefined as any
         }];
         file.addDiagnostics(expected);
         expectDiagnostics(file, expected);
@@ -1334,10 +1334,10 @@ describe('BrsFile', () => {
             `);
             expect(file.callables.length).to.equal(2);
             expect(file.callables[0].name).to.equal('DoA');
-            expect(file.callables[0].nameRange.start.line).to.equal(1);
+            expect(file.callables[0].nameRange!.start.line).to.equal(1);
 
             expect(file.callables[1].name).to.equal('DoA');
-            expect(file.callables[1].nameRange.start.line).to.equal(5);
+            expect(file.callables[1].nameRange!.start.line).to.equal(5);
         });
 
         it('finds function call line and column numbers', () => {
@@ -2331,7 +2331,7 @@ describe('BrsFile', () => {
             testTranspile(
                 'sub main()\n    name = "john \nend sub',
                 'sub main()\n    name = "john "\nend sub',
-                null,
+                null as any,
                 'source/main.bs',
                 false
             );
@@ -2559,7 +2559,7 @@ describe('BrsFile', () => {
                     person = {}
                     stuff = []
                 end sub
-        `, null, 'trim');
+        `, null as any, 'trim');
         });
 
         it('does not add leading or trailing newlines', () => {
@@ -2612,8 +2612,8 @@ describe('BrsFile', () => {
                         kind: token.kind,
                         start: Position.create(
                             //convert source-map 1-based line to token 0-based line
-                            originalPosition.line - 1,
-                            originalPosition.column
+                            originalPosition.line! - 1,
+                            originalPosition.column!
                         )
                     };
                 });
@@ -3398,7 +3398,7 @@ describe('BrsFile', () => {
             `);
             const parser = file['_parser'];
             //clear the private _parser instance
-            file['_parser'] = undefined;
+            file['_parser'] = undefined as any;
 
             //force the file to get a new instance of parser
             const newParser = file.parser;
@@ -3514,7 +3514,7 @@ describe('BrsFile', () => {
                 end sub
             `);
             program.validate();
-            sinon.stub(util, 'getAllDottedGetParts').returns(null);
+            sinon.stub(util, 'getAllDottedGetParts').returns(null as any);
             // print alpha.be|ta
             expect(program.getDefinition(file.srcPath, Position.create(2, 34))).to.eql([]);
         });
diff --git a/src/files/XmlFile.spec.ts b/src/files/XmlFile.spec.ts
index 610a9e50..104c299e 100644
--- a/src/files/XmlFile.spec.ts
+++ b/src/files/XmlFile.spec.ts
@@ -59,13 +59,13 @@ describe('XmlFile', () => {
             program.plugins.add({
                 name: 'allows modifying the parsed XML model',
                 afterFileParse: () => {
-                    let child = file.parser.ast.component.children.children[0];
+                    let child = file.parser.ast.component!.children.children[0];
                     expect(child.attributes).to.have.lengthOf(4);
-                    child.setAttribute('text', undefined);
-                    expect(child.getAttribute('id').value.text).to.equal('one');
+                    child.setAttribute('text', undefined as any);
+                    expect(child.getAttribute('id')!.value.text).to.equal('one');
                     expect(child.attributes).to.have.lengthOf(3);
-                    child.setAttribute('text3', undefined);
-                    expect(child.getAttribute('id').value.text).to.equal('one');
+                    child.setAttribute('text3', undefined as any);
+                    expect(child.getAttribute('id')!.value.text).to.equal('one');
                     expect(child.attributes).to.have.lengthOf(2);
                 }
             });
@@ -489,10 +489,10 @@ describe('XmlFile', () => {
     });
 
     it('allows adding diagnostics', () => {
-        const expected = [{
+        const expected: BsDiagnostic[] = [{
             message: 'message',
-            file: undefined,
-            range: undefined
+            file: undefined as any,
+            range: undefined as any
         }];
         file.addDiagnostics(expected);
         expectDiagnostics(file, expected);
@@ -829,7 +829,7 @@ describe('XmlFile', () => {
                     <script uri="SimpleScene.brs" type="text/brightscript" />
                     <script type="text/brightscript" uri="pkg:/source/bslib.brs" />
                 </component>
-            `, null, 'components/comp.xml');
+            `, null as any, 'components/comp.xml');
         });
 
         it('returns the XML unmodified if needsTranspiled is false', () => {
@@ -1003,7 +1003,7 @@ describe('XmlFile', () => {
             `);
             program.validate();
             expectZeroDiagnostics(program);
-            const scope = program.getComponentScope('ChildComponent');
+            const scope = program.getComponentScope('ChildComponent')!;
             expect([...scope.namespaceLookup.keys()].sort()).to.eql([
                 'lib',
                 'parent'
@@ -1214,7 +1214,7 @@ describe('XmlFile', () => {
                 <component name="comp1">
                 </component>
             `);
-            expect(program.getComponent('comp1').file.pkgPath).to.equal(comp2.pkgPath);
+            expect(program.getComponent('comp1')!.file.pkgPath).to.equal(comp2.pkgPath);
 
             //add comp1. it should become the main component with this name
             const comp1 = program.setFile('components/comp1.xml', trim`
@@ -1222,11 +1222,11 @@ describe('XmlFile', () => {
                 <component name="comp1" extends="Group">
                 </component>
             `);
-            expect(program.getComponent('comp1').file.pkgPath).to.equal(comp1.pkgPath);
+            expect(program.getComponent('comp1')!.file.pkgPath).to.equal(comp1.pkgPath);
 
             //remove comp1, comp2 should be the primary again
             program.removeFile(s`${rootDir}/components/comp1.xml`);
-            expect(program.getComponent('comp1').file.pkgPath).to.equal(comp2.pkgPath);
+            expect(program.getComponent('comp1')!.file.pkgPath).to.equal(comp2.pkgPath);
 
             //add comp3
             program.setFile('components/comp3.xml', trim`
@@ -1235,7 +1235,7 @@ describe('XmlFile', () => {
                 </component>
             `);
             //...the 2nd file should still be main
-            expect(program.getComponent('comp1').file.pkgPath).to.equal(comp2.pkgPath);
+            expect(program.getComponent('comp1')!.file.pkgPath).to.equal(comp2.pkgPath);
         });
     });
 });
diff --git a/src/parser/AstNode.spec.ts b/src/parser/AstNode.spec.ts
index d774e5e9..0f997f0c 100644
--- a/src/parser/AstNode.spec.ts
+++ b/src/parser/AstNode.spec.ts
@@ -36,10 +36,10 @@ describe('AstNode', () => {
                 `);
             program.validate();
             expectZeroDiagnostics(program);
-            const delta = file.ast.findChildAtPosition<DottedGetExpression>(util.createPosition(3, 52));
+            const delta = file.ast.findChildAtPosition<DottedGetExpression>(util.createPosition(3, 52))!;
             expect(delta.name.text).to.eql('delta');
 
-            const foxtrot = file.ast.findChildAtPosition<DottedGetExpression>(util.createPosition(3, 71));
+            const foxtrot = file.ast.findChildAtPosition<DottedGetExpression>(util.createPosition(3, 71))!;
             expect(foxtrot.name.text).to.eql('foxtrot');
         });
     });
@@ -131,7 +131,7 @@ describe('AstNode', () => {
             const secondStatement = (file.ast.statements[0] as FunctionStatement).func.body.statements[1];
             const foxtrot = file.ast.findChild((node) => {
                 return isDottedGetExpression(node) && node.name?.text === 'foxtrot';
-            });
+            })!;
             expect(
                 foxtrot.findAncestor(isPrintStatement)
             ).to.equal(secondStatement);
@@ -146,7 +146,7 @@ describe('AstNode', () => {
             `);
             const foxtrot = file.ast.findChild((node) => {
                 return isDottedGetExpression(node) && node.name?.text === 'foxtrot';
-            });
+            })!;
             expect(
                 foxtrot.findAncestor(isClassStatement)
             ).to.be.undefined;
@@ -162,7 +162,7 @@ describe('AstNode', () => {
             const firstStatement = (file.ast.statements[0] as FunctionStatement).func.body.statements[0];
             const foxtrot = file.ast.findChild((node) => {
                 return isDottedGetExpression(node) && node.name?.text === 'foxtrot';
-            });
+            })!;
             expect(
                 foxtrot.findAncestor(node => firstStatement)
             ).to.equal(firstStatement);
diff --git a/src/parser/AstNode.ts b/src/parser/AstNode.ts
index d97cf6ad..7df01402 100644
--- a/src/parser/AstNode.ts
+++ b/src/parser/AstNode.ts
@@ -47,8 +47,13 @@ export abstract class AstNode {
             if (node.symbolTable) {
                 return node.symbolTable;
             }
-            node = node.parent;
+            node = node.parent!;
         }
+
+        //justification: we are following a chain of nodes until we get to one with a SymbolTable,
+        //and the top-level node will always have a SymbolTable. So we'll never hit this undefined,
+        //but it is not so easy to convince the typechecker of this.
+        return undefined as any;
     }
 
     /**
@@ -56,7 +61,7 @@ export abstract class AstNode {
      * @param matcher a function called for each node. If you return true, this function returns the specified node. If you return a node, that node is returned. all other return values continue the loop
      *                The function's second parameter is a cancellation token. If you'd like to short-circuit the walk, call `cancellationToken.cancel()`, then this function will return `undefined`
      */
-    public findAncestor<TNode extends AstNode = AstNode>(matcher: (node: AstNode, cancellationToken: CancellationTokenSource) => boolean | AstNode | undefined | void): TNode {
+    public findAncestor<TNode extends AstNode = AstNode>(matcher: (node: AstNode, cancellationToken: CancellationTokenSource) => boolean | AstNode | undefined | void): TNode | undefined {
         let node = this.parent;
 
         const cancel = new CancellationTokenSource();
@@ -78,9 +83,9 @@ export abstract class AstNode {
      * Find the first child where the matcher evaluates to true.
      * @param matcher a function called for each node. If you return true, this function returns the specified node. If you return a node, that node is returned. all other return values continue the loop
      */
-    public findChild<TNode extends AstNode = AstNode>(matcher: (node: AstNode, cancellationSource) => boolean | AstNode | undefined | void, options?: WalkOptions) {
+    public findChild<TNode extends AstNode = AstNode>(matcher: (node: AstNode, cancellationSource) => boolean | AstNode | undefined | void, options?: WalkOptions): TNode | undefined {
         const cancel = new CancellationTokenSource();
-        let result: AstNode;
+        let result: AstNode | undefined;
         this.walk((node) => {
             const matcherValue = matcher(node, cancel);
             if (matcherValue) {
@@ -98,7 +103,7 @@ export abstract class AstNode {
     /**
      * FInd the deepest child that includes the given position
      */
-    public findChildAtPosition<TNodeType extends AstNode = AstNode>(position: Position, options?: WalkOptions): TNodeType {
+    public findChildAtPosition<TNodeType extends AstNode = AstNode>(position: Position, options?: WalkOptions): TNodeType | undefined {
         return this.findChild<TNodeType>((node) => {
             //if the current node includes this range, keep that node
             if (util.rangeContains(node.range, position)) {
@@ -126,7 +131,7 @@ export abstract class Statement extends AstNode {
     /**
      * Annotations for this statement
      */
-    public annotations: AnnotationExpression[];
+    public annotations: AnnotationExpression[] | undefined;
 }
 
 
diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts
index 7cf3800a..acab57dc 100644
--- a/src/parser/Expression.ts
+++ b/src/parser/Expression.ts
@@ -80,7 +80,7 @@ export class CallExpression extends Expression {
     }
 
     transpile(state: BrsTranspileState, nameOverride?: string) {
-        let result = [];
+        let result: Array<string | SourceNode> = [];
 
         //transpile the name
         if (nameOverride) {
@@ -130,7 +130,7 @@ export class FunctionExpression extends Expression implements TypedefProvider {
         super();
         if (this.returnTypeToken) {
             this.returnType = util.tokenToBscType(this.returnTypeToken);
-        } else if (this.functionType.text.toLowerCase() === 'sub') {
+        } else if (this.functionType?.text.toLowerCase() === 'sub') {
             this.returnType = new VoidType();
         } else {
             this.returnType = DynamicType.instance;
diff --git a/src/parser/Parser.Class.spec.ts b/src/parser/Parser.Class.spec.ts
index f6919b89..1ec7669a 100644
--- a/src/parser/Parser.Class.spec.ts
+++ b/src/parser/Parser.Class.spec.ts
@@ -196,10 +196,10 @@ describe('parser class', () => {
             expect(diagnostics).to.be.empty;
             expect(statements[0]).instanceof(ClassStatement);
             let field = (statements[0] as ClassStatement).body[0] as FieldStatement;
-            expect(field.accessModifier.kind).to.equal(TokenKind.Public);
-            expect(field.name.text).to.equal('firstName');
-            expect(field.as.text).to.equal('as');
-            expect(field.type.text).to.equal('string');
+            expect(field.accessModifier!.kind).to.equal(TokenKind.Public);
+            expect(field.name!.text).to.equal('firstName');
+            expect(field.as!.text).to.equal('as');
+            expect(field.type!.text).to.equal('string');
         });
 
         it('can be solely an identifier', () => {
@@ -211,7 +211,7 @@ describe('parser class', () => {
             let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
             expect(diagnostics).to.be.lengthOf(0);
             let cls = statements[0] as ClassStatement;
-            expect(cls.fields[0].name.text).to.equal('firstName');
+            expect(cls.fields[0].name!.text).to.equal('firstName');
         });
 
         it('malformed field does not impact leading and trailing fields', () => {
@@ -224,8 +224,8 @@ describe('parser class', () => {
                 `);
             let { statements } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
             let cls = statements[0] as ClassStatement;
-            expect(cls.fields[0].name.text).to.equal('firstName');
-            expect(cls.fields[cls.fields.length - 1].name.text).to.equal('lastName');
+            expect(cls.fields[0].name!.text).to.equal('firstName');
+            expect(cls.fields[cls.fields.length - 1].name!.text).to.equal('lastName');
         });
 
         it(`detects missing type after 'as' keyword`, () => {
@@ -237,7 +237,7 @@ describe('parser class', () => {
             let { diagnostics, statements } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
             expect(diagnostics.length).to.be.greaterThan(0);
             let cls = statements[0] as ClassStatement;
-            expect(cls.fields[0].name.text).to.equal('middleName');
+            expect(cls.fields[0].name!.text).to.equal('middleName');
             expect(diagnostics[0].code).to.equal(DiagnosticMessages.expectedIdentifierAfterKeyword('as').code);
         });
 
@@ -269,7 +269,7 @@ describe('parser class', () => {
             expect(theClass).to.be.instanceof(ClassStatement);
             let method = theClass.methods[0];
             expect(method.name.text).to.equal('getName');
-            expect(method.accessModifier.text).to.equal('public');
+            expect(method.accessModifier!.text).to.equal('public');
             expect(method.func).to.exist;
         });
 
@@ -285,7 +285,7 @@ describe('parser class', () => {
             expect(diagnostics).to.be.lengthOf(0);
             let theClass = statements[0] as ClassStatement;
             let method = theClass.methods[0];
-            expect(method.accessModifier.text).to.equal('public');
+            expect(method.accessModifier!.text).to.equal('public');
             expect(method.func).to.exist;
         });
 
@@ -366,8 +366,8 @@ describe('parser class', () => {
         let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
         expect(diagnostics[0]?.message).to.not.exist;
         let stmt = (statements[1] as ClassStatement);
-        expect(stmt.extendsKeyword.text).to.equal('extends');
-        expect(stmt.parentClassName.getName(ParseMode.BrighterScript)).to.equal('Person');
+        expect(stmt.extendsKeyword!.text).to.equal('extends');
+        expect(stmt.parentClassName!.getName(ParseMode.BrighterScript)).to.equal('Person');
     });
 
     it('catches missing identifier after "extends" keyword', () => {
diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts
index 70c023e8..264962dc 100644
--- a/src/parser/Parser.spec.ts
+++ b/src/parser/Parser.spec.ts
@@ -750,13 +750,13 @@ describe('parser', () => {
                         expectCommentWithText(ifStmt.thenBranch.statements[1], `'comment 2`);
                         expectCommentWithText(ifStmt.thenBranch.statements[3], `'comment 3`);
 
-                        let elseIfBranch = ifStmt.elseBranch;
+                        let elseIfBranch = ifStmt.elseBranch!;
                         if (isIfStatement(elseIfBranch)) {
                             expectCommentWithText(elseIfBranch.thenBranch.statements[0], `'comment 4`);
                             expectCommentWithText(elseIfBranch.thenBranch.statements[1], `'comment 5`);
                             expectCommentWithText(elseIfBranch.thenBranch.statements[3], `'comment 6`);
 
-                            let elseBranch = elseIfBranch.elseBranch;
+                            let elseBranch = elseIfBranch.elseBranch!;
                             if (isBlock(elseBranch)) {
                                 expectCommentWithText(elseBranch.statements[0], `'comment 7`);
                                 expectCommentWithText(elseBranch.statements[1], `'comment 8`);
@@ -1022,15 +1022,15 @@ describe('parser', () => {
             expect(statements[0]).to.be.instanceof(FunctionStatement);
             let fn = statements[0] as FunctionStatement;
             expect(fn.annotations).to.exist;
-            expect(fn.annotations[0]).to.be.instanceof(AnnotationExpression);
-            expect(fn.annotations[0].nameToken.text).to.equal('meta1');
-            expect(fn.annotations[0].name).to.equal('meta1');
+            expect(fn.annotations![0]).to.be.instanceof(AnnotationExpression);
+            expect(fn.annotations![0].nameToken.text).to.equal('meta1');
+            expect(fn.annotations![0].name).to.equal('meta1');
 
             expect(statements[1]).to.be.instanceof(FunctionStatement);
             fn = statements[1] as FunctionStatement;
             expect(fn.annotations).to.exist;
-            expect(fn.annotations[0]).to.be.instanceof(AnnotationExpression);
-            expect(fn.annotations[0].nameToken.text).to.equal('meta2');
+            expect(fn.annotations![0]).to.be.instanceof(AnnotationExpression);
+            expect(fn.annotations![0].nameToken.text).to.equal('meta2');
         });
 
         it('attaches annotations inside a function body', () => {
@@ -1046,7 +1046,7 @@ describe('parser', () => {
             let stat = fnStatements[0];
             expect(stat).to.exist;
             expect(stat.annotations?.length).to.equal(1);
-            expect(stat.annotations[0]).to.be.instanceof(AnnotationExpression);
+            expect(stat.annotations![0]).to.be.instanceof(AnnotationExpression);
         });
 
         it('attaches multiple annotations to next statement', () => {
@@ -1060,10 +1060,10 @@ describe('parser', () => {
             expect(statements[0]).to.be.instanceof(FunctionStatement);
             let fn = statements[0] as FunctionStatement;
             expect(fn.annotations).to.exist;
-            expect(fn.annotations.length).to.equal(3);
-            expect(fn.annotations[0]).to.be.instanceof(AnnotationExpression);
-            expect(fn.annotations[1]).to.be.instanceof(AnnotationExpression);
-            expect(fn.annotations[2]).to.be.instanceof(AnnotationExpression);
+            expect(fn.annotations!.length).to.equal(3);
+            expect(fn.annotations![0]).to.be.instanceof(AnnotationExpression);
+            expect(fn.annotations![1]).to.be.instanceof(AnnotationExpression);
+            expect(fn.annotations![2]).to.be.instanceof(AnnotationExpression);
         });
 
         it('allows annotations with parameters', () => {
@@ -1075,9 +1075,9 @@ describe('parser', () => {
             expect(diagnostics[0]?.message).not.to.exist;
             let fn = statements[0] as FunctionStatement;
             expect(fn.annotations).to.exist;
-            expect(fn.annotations[0]).to.be.instanceof(AnnotationExpression);
-            expect(fn.annotations[0].nameToken.text).to.equal('meta1');
-            expect(fn.annotations[0].call).to.be.instanceof(CallExpression);
+            expect(fn.annotations![0]).to.be.instanceof(AnnotationExpression);
+            expect(fn.annotations![0].nameToken.text).to.equal('meta1');
+            expect(fn.annotations![0].call).to.be.instanceof(CallExpression);
         });
 
         it('attaches annotations to a class', () => {
@@ -1092,7 +1092,7 @@ describe('parser', () => {
             expect(diagnostics[0]?.message).not.to.exist;
             let cs = statements[0] as ClassStatement;
             expect(cs.annotations?.length).to.equal(1);
-            expect(cs.annotations[0]).to.be.instanceof(AnnotationExpression);
+            expect(cs.annotations![0]).to.be.instanceof(AnnotationExpression);
         });
 
         it('attaches annotations to multiple clases', () => {
@@ -1113,12 +1113,12 @@ describe('parser', () => {
             expect(diagnostics[0]?.message).not.to.exist;
             let cs = statements[0] as ClassStatement;
             expect(cs.annotations?.length).to.equal(1);
-            expect(cs.annotations[0]).to.be.instanceof(AnnotationExpression);
-            expect(cs.annotations[0].name).to.equal('meta1');
+            expect(cs.annotations![0]).to.be.instanceof(AnnotationExpression);
+            expect(cs.annotations![0].name).to.equal('meta1');
             let cs2 = statements[1] as ClassStatement;
             expect(cs2.annotations?.length).to.equal(1);
-            expect(cs2.annotations[0]).to.be.instanceof(AnnotationExpression);
-            expect(cs2.annotations[0].name).to.equal('meta2');
+            expect(cs2.annotations![0]).to.be.instanceof(AnnotationExpression);
+            expect(cs2.annotations![0].name).to.equal('meta2');
         });
 
         it('attaches annotations to a namespaced class', () => {
@@ -1136,7 +1136,7 @@ describe('parser', () => {
             let ns = statements[0] as NamespaceStatement;
             let cs = ns.body.statements[0] as ClassStatement;
             expect(cs.annotations?.length).to.equal(1);
-            expect(cs.annotations[0]).to.be.instanceof(AnnotationExpression);
+            expect(cs.annotations![0]).to.be.instanceof(AnnotationExpression);
         });
 
         it('attaches annotations to a namespaced class - multiple', () => {
@@ -1160,12 +1160,12 @@ describe('parser', () => {
             let ns = statements[0] as NamespaceStatement;
             let cs = ns.body.statements[0] as ClassStatement;
             expect(cs.annotations?.length).to.equal(1);
-            expect(cs.annotations[0]).to.be.instanceof(AnnotationExpression);
-            expect(cs.annotations[0].name).to.equal('meta1');
+            expect(cs.annotations![0]).to.be.instanceof(AnnotationExpression);
+            expect(cs.annotations![0].name).to.equal('meta1');
             let cs2 = ns.body.statements[1] as ClassStatement;
             expect(cs2.annotations?.length).to.equal(1);
-            expect(cs2.annotations[0]).to.be.instanceof(AnnotationExpression);
-            expect(cs2.annotations[0].name).to.equal('meta2');
+            expect(cs2.annotations![0]).to.be.instanceof(AnnotationExpression);
+            expect(cs2.annotations![0].name).to.equal('meta2');
 
         });
 
@@ -1185,7 +1185,7 @@ describe('parser', () => {
             let cs = statements[0] as ClassStatement;
             let stat = cs.body[0];
             expect(stat.annotations?.length).to.equal(1);
-            expect(stat.annotations[0]).to.be.instanceof(AnnotationExpression);
+            expect(stat.annotations![0]).to.be.instanceof(AnnotationExpression);
         });
 
         it('attaches annotations to a class methods', () => {
@@ -1204,7 +1204,7 @@ describe('parser', () => {
             let cs = statements[0] as ClassStatement;
             let stat = cs.body[1];
             expect(stat.annotations?.length).to.equal(1);
-            expect(stat.annotations[0]).to.be.instanceof(AnnotationExpression);
+            expect(stat.annotations![0]).to.be.instanceof(AnnotationExpression);
         });
         it('attaches annotations to a class methods, fields and constructor', () => {
             let { statements, diagnostics } = parse(`
@@ -1230,16 +1230,16 @@ describe('parser', () => {
             expect(diagnostics[0]?.message).not.to.exist;
             let cs = statements[0] as ClassStatement;
             expect(cs.annotations?.length).to.equal(2);
-            expect(cs.annotations[0]).to.be.instanceof(AnnotationExpression);
+            expect(cs.annotations![0]).to.be.instanceof(AnnotationExpression);
             let stat1 = cs.body[0];
             let stat2 = cs.body[1];
             let f1 = cs.body[2];
             expect(stat1.annotations?.length).to.equal(2);
-            expect(stat1.annotations[0]).to.be.instanceof(AnnotationExpression);
+            expect(stat1.annotations![0]).to.be.instanceof(AnnotationExpression);
             expect(stat2.annotations?.length).to.equal(2);
-            expect(stat2.annotations[0]).to.be.instanceof(AnnotationExpression);
+            expect(stat2.annotations![0]).to.be.instanceof(AnnotationExpression);
             expect(f1.annotations?.length).to.equal(2);
-            expect(f1.annotations[0]).to.be.instanceof(AnnotationExpression);
+            expect(f1.annotations![0]).to.be.instanceof(AnnotationExpression);
         });
 
         it('ignores annotations on commented out lines', () => {
@@ -1274,18 +1274,18 @@ describe('parser', () => {
             expect(statements[0]).to.be.instanceof(FunctionStatement);
             let fn = statements[0] as FunctionStatement;
             expect(fn.annotations).to.exist;
-            expect(fn.annotations[0].getArguments()).to.deep.equal([]);
+            expect(fn.annotations![0].getArguments()).to.deep.equal([]);
 
             expect(statements[1]).to.be.instanceof(FunctionStatement);
             fn = statements[1] as FunctionStatement;
             expect(fn.annotations).to.exist;
-            expect(fn.annotations[0]).to.be.instanceof(AnnotationExpression);
-            expect(fn.annotations[0].getArguments()).to.deep.equal([
+            expect(fn.annotations![0]).to.be.instanceof(AnnotationExpression);
+            expect(fn.annotations![0].getArguments()).to.deep.equal([
                 'arg', 2, true,
                 { prop: 'value' }, [1, 2],
                 null
             ]);
-            let allArgs = fn.annotations[0].getArguments(false);
+            let allArgs = fn.annotations![0].getArguments(false);
             expect(allArgs.pop()).to.be.instanceOf(FunctionExpression);
         });
 
@@ -1302,7 +1302,7 @@ describe('parser', () => {
             expect(statements[0]).to.be.instanceof(FunctionStatement);
             let fn = statements[0] as FunctionStatement;
             expect(fn.annotations).to.exist;
-            expect(fn.annotations[0].getArguments()).to.deep.equal([-100]);
+            expect(fn.annotations![0].getArguments()).to.deep.equal([-100]);
         });
     });
 });
@@ -1310,7 +1310,7 @@ describe('parser', () => {
 function parse(text: string, mode?: ParseMode) {
     let { tokens } = Lexer.scan(text);
     return Parser.parse(tokens, {
-        mode: mode
+        mode: mode!
     });
 }
 
diff --git a/src/parser/Statement.spec.ts b/src/parser/Statement.spec.ts
index ba5a2262..a9812004 100644
--- a/src/parser/Statement.spec.ts
+++ b/src/parser/Statement.spec.ts
@@ -58,12 +58,12 @@ describe('Statement', () => {
                 end namespace
             `);
             program.validate();
-            let node = program.getFile<BrsFile>('source/main.brs').ast.findChild<NamespaceStatement>(isNamespaceStatement);
-            while (node.findChild(isNamespaceStatement)) {
-                node = node.findChild<NamespaceStatement>(isNamespaceStatement);
+            let node = program.getFile<BrsFile>('source/main.brs')!.ast.findChild<NamespaceStatement>(isNamespaceStatement);
+            while (node!.findChild(isNamespaceStatement)) {
+                node = node!.findChild<NamespaceStatement>(isNamespaceStatement);
             }
-            expect(node.getName(ParseMode.BrighterScript)).to.equal('NameA.NameB');
-            expect(node.getName(ParseMode.BrightScript)).to.equal('NameA_NameB');
+            expect(node!.getName(ParseMode.BrighterScript)).to.equal('NameA.NameB');
+            expect(node!.getName(ParseMode.BrightScript)).to.equal('NameA_NameB');
         });
     });
 
diff --git a/src/parser/TranspileState.ts b/src/parser/TranspileState.ts
index 025f76d7..379791eb 100644
--- a/src/parser/TranspileState.ts
+++ b/src/parser/TranspileState.ts
@@ -54,12 +54,12 @@ export class TranspileState {
     /**
      * Shorthand for creating a new source node
      */
-    public sourceNode(locatable: { range?: Range }, code: string | SourceNode | Array<string | SourceNode>): SourceNode | undefined {
+    public sourceNode(locatable: { range?: Range }, code: string | SourceNode | Array<string | SourceNode>): SourceNode {
         return new SourceNode(
             //convert 0-based range line to 1-based SourceNode line
-            locatable.range.start.line + 1,
+            locatable.range ? locatable.range.start.line + 1 : null,
             //range and SourceNode character are both 0-based, so no conversion necessary
-            locatable.range.start.character,
+            locatable.range ? locatable.range.start.character : null,
             this.srcPath,
             code
         );
@@ -73,9 +73,9 @@ export class TranspileState {
     public tokenToSourceNode(token: { range?: Range; text: string }) {
         return new SourceNode(
             //convert 0-based range line to 1-based SourceNode line
-            token.range.start.line + 1,
+            token.range ? token.range.start.line + 1 : null,
             //range and SourceNode character are both 0-based, so no conversion necessary
-            token.range.start.character,
+            token.range ? token.range.start.character : null,
             this.srcPath,
             token.text
         );
diff --git a/src/preprocessor/Preprocessor.ts b/src/preprocessor/Preprocessor.ts
index 1762827c..f3f37b8d 100644
--- a/src/preprocessor/Preprocessor.ts
+++ b/src/preprocessor/Preprocessor.ts
@@ -17,7 +17,7 @@ export class Preprocessor implements CC.Visitor {
     /** The set of errors encountered when pre-processing conditional compilation directives. */
     public diagnostics = [] as Diagnostic[];
 
-    public processedTokens: Token[];
+    public processedTokens: Token[] = [];
 
     /**
      * Filters the tokens contained within a set of chunks based on a set of constants.
@@ -169,11 +169,10 @@ export class Preprocessor implements CC.Visitor {
     }
 
     /**
-     * Resolves a token to a JavaScript boolean value, or throws an error.
-     * @param token the token to resolve to either `true`, `false`, or an error
-     * @throws if attempting to reference an undefined `#const` or if `token` is neither `true`, `false`, nor an identifier.
+     * Resolves a token to a JavaScript boolean value, or logs a diagnostic error.
+     * @param token the token to resolve to either `true`, `false`, or `undefined`
      */
-    public evaluateCondition(token: Token): boolean {
+    public evaluateCondition(token: Token): boolean | undefined {
         switch (token.kind) {
             case TokenKind.True:
                 return true;
diff --git a/src/preprocessor/PreprocessorParser.ts b/src/preprocessor/PreprocessorParser.ts
index 980065e7..662cef5b 100644
--- a/src/preprocessor/PreprocessorParser.ts
+++ b/src/preprocessor/PreprocessorParser.ts
@@ -7,16 +7,16 @@ import { DiagnosticMessages } from '../DiagnosticMessages';
 
 /** * Parses `Tokens` into chunks of tokens, excluding conditional compilation directives. */
 export class PreprocessorParser {
-    public diagnostics: Diagnostic[];
+    public diagnostics: Diagnostic[] = [];
 
-    public tokens: Token[];
+    public tokens: Token[] = [];
 
     private current = 0;
 
     /**
      * an array of chunks (conditional compilation directives and the associated BrightScript)
      */
-    public chunks: CC.Chunk[];
+    public chunks: CC.Chunk[] = [];
 
     /**
      * Parses an array of tokens into an array of "chunks" - conditional compilation directives and their
diff --git a/src/types/FunctionType.ts b/src/types/FunctionType.ts
index 5dc5358c..4e8909bd 100644
--- a/src/types/FunctionType.ts
+++ b/src/types/FunctionType.ts
@@ -9,9 +9,9 @@ export class FunctionType implements BscType {
     }
 
     /**
-     * The name of the function for this type. Can be null
+     * The name of the function for this type. Can be undefined
      */
-    public name: string;
+    public name: string | undefined;
 
     /**
      * Determines if this is a sub or not
@@ -65,7 +65,7 @@ export class FunctionType implements BscType {
     }
 
     public toString() {
-        let paramTexts = [];
+        let paramTexts: string[] = [];
         for (let param of this.params) {
             paramTexts.push(`${param.name}${param.isOptional ? '?' : ''} as ${param.type.toString()}`);
         }
diff --git a/src/types/InterfaceType.ts b/src/types/InterfaceType.ts
index dfbafb00..77a92b3b 100644
--- a/src/types/InterfaceType.ts
+++ b/src/types/InterfaceType.ts
@@ -9,20 +9,21 @@ export class InterfaceType implements BscType {
     }
 
     /**
-     * The name of the interface. Can be null.
+     * The name of the interface. Can be undefined.
      */
-    public name: string;
+    public name: string | undefined;
 
     public isAssignableTo(targetType: BscType) {
         //we must have all of the members of the target type, and they must be equivalent types
         if (isInterfaceType(targetType)) {
             for (const [targetMemberName, targetMemberType] of targetType.members) {
+                const ourMemberType = this.members.get(targetMemberName);
                 //we don't have the target member
-                if (!this.members.has(targetMemberName)) {
+                if (!ourMemberType) {
                     return false;
                 }
                 //our member's type is not assignable to the target member type
-                if (!this.members.get(targetMemberName).isAssignableTo(targetMemberType)) {
+                if (!ourMemberType.isAssignableTo(targetMemberType)) {
                     return false;
                 }
             }
diff --git a/src/util.spec.ts b/src/util.spec.ts
index bc8f4faf..c82333c8 100644
--- a/src/util.spec.ts
+++ b/src/util.spec.ts
@@ -8,6 +8,7 @@ import { createSandbox } from 'sinon';
 import { DiagnosticMessages } from './DiagnosticMessages';
 import { tempDir, rootDir } from './testHelpers.spec';
 import { Program } from './Program';
+import type { BsDiagnostic } from '.';
 
 const sinon = createSandbox();
 
@@ -43,11 +44,11 @@ describe('util', () => {
         it('does not crash when diagnostic is missing location information', () => {
             const program = new Program({});
             const file = program.setFile('source/main.brs', '');
-            const diagnostic = {
+            const diagnostic: BsDiagnostic = {
                 file: file,
                 message: 'crash',
                 //important part of the test. range must be missing
-                range: undefined
+                range: undefined as any
             };
 
             file.commentFlags.push({
@@ -85,7 +86,7 @@ describe('util', () => {
             fsExtra.outputFileSync(s`${rootDir}/grandparent.json`, `{"extends": "greatgrandparent.json"}`);
             fsExtra.outputFileSync(s`${rootDir}/greatgrandparent.json`, `{}`);
             expect(
-                util.loadConfigFile(s`${rootDir}/child.json`)._ancestors.map(x => s(x))
+                util.loadConfigFile(s`${rootDir}/child.json`)?._ancestors?.map(x => s(x))
             ).to.eql([
                 s`${rootDir}/child.json`,
                 s`${rootDir}/parent.json`,
@@ -98,7 +99,7 @@ describe('util', () => {
             fsExtra.outputFileSync(s`${rootDir}/child.json`, `{}`);
             let config = util.loadConfigFile(s`${rootDir}/child.json`);
             expect(
-                config._ancestors.map(x => s(x))
+                config?._ancestors?.map(x => s(x))
             ).to.eql([
                 s`${rootDir}/child.json`
             ]);
@@ -114,7 +115,7 @@ describe('util', () => {
                 ]
             };
             util.resolvePathsRelativeTo(config, 'plugins', s`${rootDir}/config`);
-            expect(config.plugins.map(p => (p ? util.pathSepNormalize(p, '/') : undefined))).to.deep.equal([
+            expect(config?.plugins?.map(p => (p ? util.pathSepNormalize(p, '/') : undefined))).to.deep.equal([
                 `${rootDir}/config/plugins.js`,
                 `${rootDir}/config/scripts/plugins.js`,
                 `${rootDir}/scripts/plugins.js`,
@@ -129,11 +130,11 @@ describe('util', () => {
                     'bsplugin',
                     '../config/plugins.js',
                     'bsplugin',
-                    undefined
+                    undefined as any
                 ]
             };
             util.resolvePathsRelativeTo(config, 'plugins', s`${process.cwd()}/config`);
-            expect(config.plugins.map(p => (p ? util.pathSepNormalize(p, '/') : undefined))).to.deep.equal([
+            expect(config?.plugins?.map(p => (p ? util.pathSepNormalize(p, '/') : undefined))).to.deep.equal([
                 s`${process.cwd()}/config/plugins.js`,
                 'bsplugin'
             ].map(p => util.pathSepNormalize(p, '/')));
@@ -469,10 +470,10 @@ describe('util', () => {
     describe('comparePositionToRange', () => {
         it('does not crash on undefined props', () => {
             expect(
-                util.comparePositionToRange(null, util.createRange(0, 0, 0, 0))
+                util.comparePositionToRange(undefined, util.createRange(0, 0, 0, 0))
             ).to.eql(0);
             expect(
-                util.comparePositionToRange(util.createPosition(1, 1), null)
+                util.comparePositionToRange(util.createPosition(1, 1), undefined)
             ).to.eql(0);
         });
 
@@ -606,10 +607,10 @@ describe('util', () => {
     describe('rangesIntersect', () => {
         it('does not crash on undefined range', () => {
             expect(
-                util.rangesIntersect(null, util.createRange(0, 0, 0, 0))
+                util.rangesIntersect(undefined, util.createRange(0, 0, 0, 0))
             ).to.be.false;
             expect(
-                util.rangesIntersect(util.createRange(0, 0, 0, 0), null)
+                util.rangesIntersect(util.createRange(0, 0, 0, 0), undefined)
             ).to.be.false;
         });
 
@@ -705,10 +706,10 @@ describe('util', () => {
     describe('rangesIntersectOrTouch', () => {
         it('does not crash on undefined range', () => {
             expect(
-                util.rangesIntersectOrTouch(null, util.createRange(0, 0, 0, 0))
+                util.rangesIntersectOrTouch(undefined, util.createRange(0, 0, 0, 0))
             ).to.be.false;
             expect(
-                util.rangesIntersectOrTouch(util.createRange(0, 0, 0, 0), null)
+                util.rangesIntersectOrTouch(util.createRange(0, 0, 0, 0), undefined)
             ).to.be.false;
         });
 
@@ -864,11 +865,11 @@ describe('util', () => {
             expect(
                 util.toDiagnostic({
                     ...DiagnosticMessages.cannotFindName('someVar'),
-                    file: undefined,
+                    file: undefined as any,
                     range: util.createRange(1, 2, 3, 4),
                     relatedInformation: [{
                         message: 'Alpha',
-                        location: undefined
+                        location: undefined as any
                     }]
                 }, 'u/r/i').relatedInformation
             ).to.eql([{
@@ -883,7 +884,7 @@ describe('util', () => {
             expect(
                 util.toDiagnostic({
                     ...DiagnosticMessages.cannotFindName('someVar'),
-                    file: undefined,
+                    file: undefined as any,
                     range: util.createRange(1, 2, 3, 4),
                     relatedInformation: [{
                         message: 'Alpha',
@@ -892,9 +893,9 @@ describe('util', () => {
                         )
                     }, {
                         message: 'Beta',
-                        location: undefined
+                        location: undefined as any
                     }]
-                }, undefined).relatedInformation
+                }, undefined as any).relatedInformation
             ).to.eql([{
                 message: 'Alpha',
                 location: util.createLocation(
diff --git a/src/util.ts b/src/util.ts
index 4253f72a..414d8add 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -4,7 +4,7 @@ import type { ParseError } from 'jsonc-parser';
 import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser';
 import * as path from 'path';
 import { rokuDeploy, DefaultFiles, standardizePath as rokuDeployStandardizePath } from 'roku-deploy';
-import type { Diagnostic, Position, Range, Location } from 'vscode-languageserver';
+import type { Diagnostic, Position, Range, Location, DiagnosticRelatedInformation } from 'vscode-languageserver';
 import { URI } from 'vscode-uri';
 import * as xml2js from 'xml2js';
 import type { BsConfig } from './BsConfig';
@@ -34,7 +34,7 @@ import type { SGAttribute } from './parser/SGTypes';
 import * as requireRelative from 'require-relative';
 import type { BrsFile } from './files/BrsFile';
 import type { XmlFile } from './files/XmlFile';
-import type { Expression, Statement } from './parser/AstNode';
+import type { AstNode, Expression, Statement } from './parser/AstNode';
 
 export class Util {
     public clearConsole() {
@@ -78,7 +78,7 @@ export class Util {
      * Determine if this path is a directory
      */
     public isDirectorySync(dirPath: string | undefined) {
-        return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
+        return dirPath !== undefined && fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
     }
 
     /**
@@ -164,7 +164,7 @@ export class Util {
      * @param configFilePath the relative or absolute path to a brighterscript config json file
      * @param parentProjectPaths a list of parent config files. This is used by this method to recursively build the config list
      */
-    public loadConfigFile(configFilePath: string, parentProjectPaths?: string[], cwd = process.cwd()) {
+    public loadConfigFile(configFilePath: string | undefined, parentProjectPaths?: string[], cwd = process.cwd()): BsConfig | undefined {
         if (configFilePath) {
             //if the config file path starts with question mark, then it's optional. return undefined if it doesn't exist
             if (configFilePath.startsWith('?')) {
@@ -263,7 +263,7 @@ export class Util {
      * @param targetCwd the cwd where the work should be performed
      * @param callback a function to call when the cwd has been changed to `targetCwd`
      */
-    public cwdWork<T>(targetCwd: string | null | undefined, callback: () => T) {
+    public cwdWork<T>(targetCwd: string | null | undefined, callback: () => T): T {
         let originalCwd = process.cwd();
         if (targetCwd) {
             process.chdir(targetCwd);
@@ -285,7 +285,8 @@ export class Util {
         if (err) {
             throw err;
         } else {
-            return result;
+            //justification: `result` is set as long as `err` is not set and vice versa
+            return result!;
         }
     }
 
@@ -309,7 +310,7 @@ export class Util {
             result.project = config.project;
         }
         if (result.project) {
-            let configFile = this.loadConfigFile(result.project, null, config?.cwd);
+            let configFile = this.loadConfigFile(result.project, undefined, config?.cwd);
             result = Object.assign(result, configFile);
         }
         //override the defaults with the specified options
@@ -510,7 +511,7 @@ export class Util {
      * |  bbb | bb   |  bbb |  b   | bbb  |    bb |  bb   |     b | a     |
      * ```
      */
-    public rangesIntersect(a: Range, b: Range) {
+    public rangesIntersect(a: Range | undefined, b: Range | undefined) {
         //stop if the either range is misisng
         if (!a || !b) {
             return false;
@@ -539,7 +540,7 @@ export class Util {
      * |  bbb | bb   |  bbb |  b   | bbb  |    bb |  bb   |     b | a     |
      * ```
      */
-    public rangesIntersectOrTouch(a: Range, b: Range) {
+    public rangesIntersectOrTouch(a: Range | undefined, b: Range | undefined) {
         //stop if the either range is misisng
         if (!a || !b) {
             return false;
@@ -566,7 +567,7 @@ export class Util {
         return this.comparePositionToRange(position, range) === 0;
     }
 
-    public comparePositionToRange(position: Position, range: Range) {
+    public comparePositionToRange(position: Position | undefined, range: Range | undefined) {
         //stop if the either range is misisng
         if (!position || !range) {
             return 0;
@@ -732,7 +733,7 @@ export class Util {
             //this diagnostic is affected by this flag
             if (diagnostic.range && this.rangeContains(flag.affectedRange, diagnostic.range.start)) {
                 //if the flag acts upon this diagnostic's code
-                if (flag.codes === null || flag.codes.includes(diagnosticCode)) {
+                if (flag.codes === null || (diagnosticCode !== undefined && flag.codes.includes(diagnosticCode))) {
                     return true;
                 }
             }
@@ -742,7 +743,7 @@ export class Util {
     /**
      * Walks up the chain to find the closest bsconfig.json file
      */
-    public async findClosestConfigFile(currentPath: string) {
+    public async findClosestConfigFile(currentPath: string): Promise<string | undefined> {
         //make the path absolute
         currentPath = path.resolve(
             path.normalize(
@@ -750,7 +751,7 @@ export class Util {
             )
         );
 
-        let previousPath: string;
+        let previousPath: string | undefined;
         //using ../ on the root of the drive results in the same file path, so that's how we know we reached the top
         while (previousPath !== currentPath) {
             previousPath = currentPath;
@@ -789,8 +790,8 @@ export class Util {
      * @param array the array to flatMap over
      * @param callback a function that is called for every array item
      */
-    public flatMap<T, R>(array: T[], callback: (arg: T) => R) {
-        return Array.prototype.concat.apply([], array.map(callback)) as never as R;
+    public flatMap<T, R>(array: T[], callback: (arg: T) => R[]): R[] {
+        return Array.prototype.concat.apply([], array.map(callback));
     }
 
     /**
@@ -867,9 +868,10 @@ export class Util {
     /**
      * Find a script import that the current position touches, or undefined if not found
      */
-    public getScriptImportAtPosition(scriptImports: FileReference[], position: Position) {
+    public getScriptImportAtPosition(scriptImports: FileReference[], position: Position): FileReference | undefined {
         let scriptImport = scriptImports.find((x) => {
-            return x.filePathRange.start.line === position.line &&
+            return x.filePathRange &&
+                x.filePathRange.start.line === position.line &&
                 //column between start and end
                 position.character >= x.filePathRange.start.character &&
                 position.character <= x.filePathRange.end.character;
@@ -895,7 +897,7 @@ export class Util {
         return string.split(/\r?\n/g);
     }
 
-    public getTextForRange(string: string | string[], range: Range) {
+    public getTextForRange(string: string | string[], range: Range): string {
         let lines: string[];
         if (Array.isArray(string)) {
             lines = string;
@@ -917,7 +919,9 @@ export class Util {
             rangeLines.push(lines[i]);
         }
         const lastLine = rangeLines.pop();
-        rangeLines.push(lastLine.substring(0, endCharacter));
+        if (lastLine !== undefined) {
+            rangeLines.push(lastLine.substring(0, endCharacter));
+        }
         return rangeLines.join('\n');
     }
 
@@ -967,9 +971,9 @@ export class Util {
      * Given a list of ranges, create a range that starts with the first non-null lefthand range, and ends with the first non-null
      * righthand range. Returns undefined if none of the items have a range.
      */
-    public createBoundingRange(...locatables: Array<{ range?: Range }>) {
-        let leftmostRange: Range;
-        let rightmostRange: Range;
+    public createBoundingRange(...locatables: Array<{ range?: Range }>): Range | undefined {
+        let leftmostRange: Range | undefined;
+        let rightmostRange: Range | undefined;
 
         for (let i = 0; i < locatables.length; i++) {
             //set the leftmost non-null-range item
@@ -994,7 +998,10 @@ export class Util {
             }
         }
         if (leftmostRange) {
-            return this.createRangeFromPositions(leftmostRange.start, rightmostRange.end);
+            //if we don't have a rightmost range, use the leftmost range for both the start and end
+            return this.createRangeFromPositions(
+                leftmostRange.start,
+                rightmostRange ? rightmostRange.end : leftmostRange.end);
         } else {
             return undefined;
         }
@@ -1115,7 +1122,7 @@ export class Util {
     /**
      * Load and return the list of plugins
      */
-    public loadPlugins(cwd: string, pathOrModules: string[], onError?: (pathOrModule: string, err: Error) => void) {
+    public loadPlugins(cwd: string, pathOrModules: string[], onError?: (pathOrModule: string, err: Error) => void): CompilerPlugin[] {
         const logger = new Logger();
         return pathOrModules.reduce<CompilerPlugin[]>((acc, pathOrModule) => {
             if (typeof pathOrModule === 'string') {
@@ -1123,7 +1130,7 @@ export class Util {
                     const loaded = requireRelative(pathOrModule, cwd);
                     const theExport: CompilerPlugin | CompilerPluginFactory = loaded.default ? loaded.default : loaded;
 
-                    let plugin: CompilerPlugin;
+                    let plugin: CompilerPlugin | undefined;
 
                     // legacy plugins returned a plugin object. If we find that, then add a warning
                     if (typeof theExport === 'object') {
@@ -1133,6 +1140,9 @@ export class Util {
                         // the official plugin format is a factory function that returns a new instance of a plugin.
                     } else if (typeof theExport === 'function') {
                         plugin = theExport();
+                    } else {
+                        //this should never happen; somehow an invalid plugin has made it into here
+                        throw new Error(`TILT: Encountered an invalid plugin: ${String(plugin)}`);
                     }
 
                     if (!plugin.name) {
@@ -1238,7 +1248,7 @@ export class Util {
         let source = bslib.source as string;
 
         //apply the `bslib_` prefix to the functions
-        let match: RegExpExecArray;
+        let match: RegExpExecArray | null;
         const positions = [] as number[];
         const regexp = /^(\s*(?:function|sub)\s+)([a-z0-9_]+)/mg;
         // eslint-disable-next-line no-cond-assign
@@ -1258,7 +1268,7 @@ export class Util {
      * @param diagnostic the diagnostic to clone
      * @param relatedInformationFallbackLocation a default location to use for all `relatedInformation` entries that are missing a location
      */
-    public toDiagnostic(diagnostic: Diagnostic | BsDiagnostic, relatedInformationFallbackLocation: string) {
+    public toDiagnostic(diagnostic: Diagnostic | BsDiagnostic, relatedInformationFallbackLocation: string): Diagnostic {
         return {
             severity: diagnostic.severity,
             range: diagnostic.range,
@@ -1278,7 +1288,7 @@ export class Util {
                 }
                 return clone;
                 //filter out null relatedInformation items
-            }).filter(x => x),
+            }).filter((x): x is DiagnosticRelatedInformation => Boolean(x)),
             code: diagnostic.code,
             source: 'brs'
         };
@@ -1374,7 +1384,7 @@ export class Util {
      */
     public getAllDottedGetParts(node: Expression | Statement): Identifier[] | undefined {
         const parts: Identifier[] = [];
-        let nextPart = node;
+        let nextPart: AstNode | undefined = node;
         while (nextPart) {
             if (isAssignmentStatement(node)) {
                 return [node.name];
@@ -1477,7 +1487,7 @@ export class Util {
 
     public validateTooDeepFile(file: (BrsFile | XmlFile)) {
         //find any files nested too deep
-        let pkgPath = file.pkgPath ?? file.pkgPath.toString();
+        let pkgPath = file.pkgPath ?? (file.pkgPath as any).toString();
         let rootFolder = pkgPath.replace(/^pkg:/, '').split(/[\\\/]/)[0].toLowerCase();
 
         if (isBrsFile(file) && rootFolder !== 'source') {
@@ -1504,7 +1514,7 @@ export class Util {
  * we can't use `object.tag` syntax.
  */
 export function standardizePath(stringParts, ...expressions: any[]) {
-    let result = [];
+    let result: string[] = [];
     for (let i = 0; i < stringParts.length; i++) {
         result.push(stringParts[i], expressions[i]);
     }
diff --git a/src/validators/ClassValidator.ts b/src/validators/ClassValidator.ts
index aa5d11b1..57c0507e 100644
--- a/src/validators/ClassValidator.ts
+++ b/src/validators/ClassValidator.ts
@@ -12,6 +12,7 @@ import { createVisitor, WalkMode } from '../astUtils/visitors';
 import type { BrsFile } from '../files/BrsFile';
 import { TokenKind } from '../lexer/TokenKind';
 import { DynamicType } from '../types/DynamicType';
+import type { BscType } from '../types/BscType';
 
 export class BsClassValidator {
     private scope: Scope;
@@ -19,12 +20,14 @@ export class BsClassValidator {
     /**
      * The key is the namespace-prefixed class name. (i.e. `NameA.NameB.SomeClass` or `CoolClass`)
      */
-    private classes: Map<string, AugmentedClassStatement>;
+    private classes: Map<string, AugmentedClassStatement> = new Map();
 
-    public validate(scope: Scope) {
+    public constructor(scope: Scope) {
         this.scope = scope;
         this.diagnostics = [];
+    }
 
+    public validate() {
         this.findClasses();
         this.findNamespaceNonNamespaceCollisions();
         this.linkClassesWithParents();
@@ -89,7 +92,8 @@ export class BsClassValidator {
     private findNamespaceNonNamespaceCollisions() {
         for (const [className, classStatement] of this.classes) {
             //catch namespace class collision with global class
-            let nonNamespaceClass = this.classes.get(util.getTextAfterFinalDot(className).toLowerCase());
+            let nonNamespaceClassName = util.getTextAfterFinalDot(className)?.toLowerCase();
+            let nonNamespaceClass = this.classes.get(nonNamespaceClassName!);
             const namespace = classStatement.findAncestor<NamespaceStatement>(isNamespaceStatement);
             if (namespace && nonNamespaceClass) {
                 this.diagnostics.push({
@@ -122,7 +126,7 @@ export class BsClassValidator {
             ) {
                 //prevent use of `m.` anywhere before the `super()` call
                 const cancellationToken = new CancellationTokenSource();
-                let superCall: CallExpression;
+                let superCall: CallExpression | undefined;
                 newMethod.func.body.walk(createVisitor({
                     VariableExpression: (expression, parent) => {
                         const expressionNameLower = expression?.name?.text.toLowerCase();
@@ -161,20 +165,26 @@ export class BsClassValidator {
             const names = new Map<string, string>();
             do {
                 const className = cls.getName(ParseMode.BrighterScript);
+                if (!className) {
+                    break;
+                }
                 const lowerClassName = className.toLowerCase();
                 //if we've already seen this class name before, then we have a circular dependency
-                if (names.has(lowerClassName)) {
+                if (lowerClassName && names.has(lowerClassName)) {
                     this.diagnostics.push({
-                        ...DiagnosticMessages.circularReferenceDetected([
-                            ...names.values(),
-                            className
-                        ], this.scope.name),
+                        ...DiagnosticMessages.circularReferenceDetected(
+                            Array.from(names.values()).concat(className), this.scope.name),
                         file: cls.file,
                         range: cls.name.range
                     });
                     break;
                 }
                 names.set(lowerClassName, className);
+
+                if (!cls.parentClass) {
+                    break;
+                }
+
                 cls = cls.parentClass;
             } while (cls);
         }
@@ -188,14 +198,20 @@ export class BsClassValidator {
             for (let statement of classStatement.body) {
                 if (isMethodStatement(statement) || isFieldStatement(statement)) {
                     let member = statement;
-                    let lowerMemberName = member.name.text.toLowerCase();
+                    let memberName = member.name;
+
+                    if (!memberName) {
+                        continue;
+                    }
+
+                    let lowerMemberName = memberName.text.toLowerCase();
 
                     //catch duplicate member names on same class
                     if (methods[lowerMemberName] || fields[lowerMemberName]) {
                         this.diagnostics.push({
-                            ...DiagnosticMessages.duplicateIdentifier(member.name.text),
+                            ...DiagnosticMessages.duplicateIdentifier(memberName.text),
                             file: classStatement.file,
-                            range: member.name.range
+                            range: memberName.range
                         });
                     }
 
@@ -219,20 +235,20 @@ export class BsClassValidator {
 
                         //child field has same name as parent
                         if (isFieldStatement(member)) {
-                            let ancestorMemberType = new DynamicType();
+                            let ancestorMemberType: BscType = new DynamicType();
                             if (isFieldStatement(ancestorAndMember.member)) {
-                                ancestorMemberType = ancestorAndMember.member.getType();
+                                ancestorMemberType = ancestorAndMember.member.getType() ?? new DynamicType();
                             } else if (isMethodStatement(ancestorAndMember.member)) {
                                 ancestorMemberType = ancestorAndMember.member.func.getFunctionType();
                             }
                             const childFieldType = member.getType();
-                            if (!childFieldType.isAssignableTo(ancestorMemberType)) {
+                            if (childFieldType && !childFieldType.isAssignableTo(ancestorMemberType)) {
                                 //flag incompatible child field type to ancestor field type
                                 this.diagnostics.push({
                                     ...DiagnosticMessages.childFieldTypeNotAssignableToBaseProperty(
-                                        classStatement.getName(ParseMode.BrighterScript),
+                                        classStatement.getName(ParseMode.BrighterScript) ?? '',
                                         ancestorAndMember.classStatement.getName(ParseMode.BrighterScript),
-                                        member.name.text,
+                                        memberName.text,
                                         childFieldType.toString(),
                                         ancestorMemberType.toString()
                                     ),
@@ -270,7 +286,7 @@ export class BsClassValidator {
                                 ...DiagnosticMessages.mismatchedOverriddenMemberVisibility(
                                     classStatement.name.text,
                                     ancestorAndMember.member.name?.text,
-                                    member.accessModifier?.text || 'public',
+                                    member.accessModifier?.text ?? 'public',
                                     ancestorAndMember.member.accessModifier?.text || 'public',
                                     ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
                                 ),
@@ -311,7 +327,7 @@ export class BsClassValidator {
                             if (!this.getClassByName(lowerFieldTypeName, currentNamespaceName) && !this.scope.hasInterface(lowerFieldTypeName) && !this.scope.hasEnum(lowerFieldTypeName)) {
                                 this.diagnostics.push({
                                     ...DiagnosticMessages.cannotFindType(fieldTypeName),
-                                    range: statement.type.range,
+                                    range: statement.type?.range ?? statement.range,
                                     file: classStatement.file
                                 });
                             }
@@ -344,7 +360,7 @@ export class BsClassValidator {
         //unlink all classes from their parents so it doesn't mess up the next scope
         for (const [, classStatement] of this.classes) {
             delete classStatement.parentClass;
-            delete classStatement.file;
+            delete (classStatement as any).file;
         }
     }
 
@@ -376,7 +392,7 @@ export class BsClassValidator {
                         relatedInformation: [{
                             location: util.createLocation(
                                 URI.file(alreadyDefinedClass.file.srcPath).toString(),
-                                this.classes.get(lowerName).range
+                                alreadyDefinedClass.range
                             ),
                             message: ''
                         }]
@@ -417,7 +433,7 @@ export class BsClassValidator {
                 let relativeParent = this.classes.get(relativeName.toLowerCase());
                 let absoluteParent = this.classes.get(absoluteName.toLowerCase());
 
-                let parentClass: AugmentedClassStatement;
+                let parentClass: AugmentedClassStatement | undefined;
                 //if we found a relative parent class
                 if (relativeParent) {
                     parentClass = relativeParent;
@@ -437,5 +453,5 @@ export class BsClassValidator {
 
 type AugmentedClassStatement = ClassStatement & {
     file: BscFile;
-    parentClass: AugmentedClassStatement;
+    parentClass: AugmentedClassStatement | undefined;
 };
diff --git a/tsconfig-null-safe.json b/tsconfig-null-safe.json
new file mode 100644
index 00000000..097be82c
--- /dev/null
+++ b/tsconfig-null-safe.json
@@ -0,0 +1,6 @@
+{
+    "extends": "./tsconfig.json",
+    "compilerOptions": {
+        "strictNullChecks": true
+    }
+}


Test patch:
diff --git a/src/files/tests/imports.spec.ts b/src/files/tests/imports.spec.ts
index 2c2af965..79343327 100644
--- a/src/files/tests/imports.spec.ts
+++ b/src/files/tests/imports.spec.ts
@@ -53,8 +53,8 @@ describe('import statements', () => {
         `);
         let files = Object.keys(program.files).map(x => program.getFile(x)).filter(x => !!x).map(x => {
             return {
-                src: x.srcPath,
-                dest: x.pkgPath
+                src: x!.srcPath,
+                dest: x!.pkgPath
             };
         });
         await program.transpile(files, stagingDir);
@@ -247,7 +247,7 @@ describe('import statements', () => {
                 <script type="text/brightscript" uri="pkg:/source/maestro/ioc/IOCMixin.brs" />
                 <script type="text/brightscript" uri="pkg:/source/bslib.brs" />
             </component>
-        `, null, 'components/AuthenticationService.xml');
+        `, null as any, 'components/AuthenticationService.xml');
     });
 
     it('handles malformed imports', () => {
diff --git a/src/parser/tests/Parser.spec.ts b/src/parser/tests/Parser.spec.ts
index f6ec177e..dafd7586 100644
--- a/src/parser/tests/Parser.spec.ts
+++ b/src/parser/tests/Parser.spec.ts
@@ -11,8 +11,8 @@ import type { Range } from 'vscode-languageserver';
 export function token(kind: TokenKind, text?: string): Token {
     return {
         kind: kind,
-        text: text,
-        isReserved: ReservedWords.has((text || '').toLowerCase()),
+        text: text!,
+        isReserved: ReservedWords.has((text ?? '').toLowerCase()),
         range: interpolatedRange,
         leadingWhitespace: ''
     };
diff --git a/src/parser/tests/expression/Call.spec.ts b/src/parser/tests/expression/Call.spec.ts
index 3d238dc2..a92e8209 100644
--- a/src/parser/tests/expression/Call.spec.ts
+++ b/src/parser/tests/expression/Call.spec.ts
@@ -14,7 +14,7 @@ describe('parser call expressions', () => {
     it('parses named function calls', () => {
         const { statements, diagnostics } = Parser.parse([
             identifier('RebootSystem'),
-            { kind: TokenKind.LeftParen, text: '(', range: null },
+            { kind: TokenKind.LeftParen, text: '(', range: null as any },
             token(TokenKind.RightParen, ')'),
             EOF
         ]);
@@ -65,7 +65,7 @@ describe('parser call expressions', () => {
     it('allows closing parentheses on separate line', () => {
         const { statements, diagnostics } = Parser.parse([
             identifier('RebootSystem'),
-            { kind: TokenKind.LeftParen, text: '(', range: null },
+            { kind: TokenKind.LeftParen, text: '(', range: null as any },
             token(TokenKind.Newline, '\\n'),
             token(TokenKind.Newline, '\\n'),
             token(TokenKind.RightParen, ')'),
@@ -128,9 +128,9 @@ describe('parser call expressions', () => {
     it('accepts arguments', () => {
         const { statements, diagnostics } = Parser.parse([
             identifier('add'),
-            { kind: TokenKind.LeftParen, text: '(', range: null },
+            { kind: TokenKind.LeftParen, text: '(', range: null as any },
             token(TokenKind.IntegerLiteral, '1'),
-            { kind: TokenKind.Comma, text: ',', range: null },
+            { kind: TokenKind.Comma, text: ',', range: null as any },
             token(TokenKind.IntegerLiteral, '2'),
             token(TokenKind.RightParen, ')'),
             EOF
diff --git a/src/parser/tests/expression/TemplateStringExpression.spec.ts b/src/parser/tests/expression/TemplateStringExpression.spec.ts
index dbe7363a..b87408d5 100644
--- a/src/parser/tests/expression/TemplateStringExpression.spec.ts
+++ b/src/parser/tests/expression/TemplateStringExpression.spec.ts
@@ -369,7 +369,7 @@ describe('TemplateStringExpression', () => {
             function init()
             end function
         `);
-        const ann = parser.ast.statements[0].annotations[0];
+        const ann = parser.ast.statements[0].annotations![0];
         expect(ann.range).to.eql(util.createRange(1, 12, 3, 14));
     });
 });
diff --git a/src/parser/tests/statement/Dim.spec.ts b/src/parser/tests/statement/Dim.spec.ts
index 5c9b474f..2d3d4dbd 100644
--- a/src/parser/tests/statement/Dim.spec.ts
+++ b/src/parser/tests/statement/Dim.spec.ts
@@ -68,10 +68,10 @@ function validatePass(text: string, dimStatementIndex: number, identifierText: s
     expect(dimStatement).to.exist;
     expect(dimStatement.dimToken).to.exist;
     expect(dimStatement.identifier).to.exist;
-    expect(dimStatement.identifier.text).to.equal(identifierText);
+    expect(dimStatement.identifier!.text).to.equal(identifierText);
     expect(dimStatement.openingSquare).to.exist;
     expect(dimStatement.dimensions).to.exist;
-    expect(dimStatement.dimensions.length).to.equal(dimensionsCount);
+    expect(dimStatement.dimensions!.length).to.equal(dimensionsCount);
     expect(dimStatement.closingSquare).to.exist;
     expect(dimStatement.range).to.exist;
 }
diff --git a/src/parser/tests/statement/Enum.spec.ts b/src/parser/tests/statement/Enum.spec.ts
index 5a11f176..c3d07ca4 100644
--- a/src/parser/tests/statement/Enum.spec.ts
+++ b/src/parser/tests/statement/Enum.spec.ts
@@ -46,7 +46,7 @@ describe('EnumStatement', () => {
         `, { mode: ParseMode.BrighterScript });
 
         expectZeroDiagnostics(parser);
-        expect(parser.ast.statements[0].annotations[0].name).to.eql('someAnnotation');
+        expect(parser.ast.statements[0].annotations![0].name).to.eql('someAnnotation');
     });
 
     it('constructs when missing enum name', () => {
@@ -447,7 +447,7 @@ describe('EnumStatement', () => {
         function expectMemberValueMap(code: string, expected: Record<string, string>) {
             const file = program.setFile<BrsFile>('source/lib.brs', code);
             const cancel = new CancellationTokenSource();
-            let firstEnum: EnumStatement;
+            let firstEnum: EnumStatement | undefined;
             file.ast.walk(statement => {
                 if (isEnumStatement(statement)) {
                     firstEnum = statement;
@@ -458,7 +458,7 @@ describe('EnumStatement', () => {
                 cancel: cancel.token
             });
             expect(firstEnum).to.exist;
-            const values = firstEnum.getMemberValueMap();
+            const values = firstEnum!.getMemberValueMap();
             expect(
                 [...values].reduce((prev, [key, value]) => {
                     prev[key] = value;
diff --git a/src/parser/tests/statement/Function.spec.ts b/src/parser/tests/statement/Function.spec.ts
index d2fd5c12..e6dab558 100644
--- a/src/parser/tests/statement/Function.spec.ts
+++ b/src/parser/tests/statement/Function.spec.ts
@@ -15,7 +15,7 @@ describe('parser', () => {
                 end su
             `);
             const func = parser.ast.findChild<FunctionStatement>(isFunctionStatement);
-            expect(func.func.body).to.exist;
+            expect(func?.func.body).to.exist;
         });
 
         it('recovers when using `end sub` instead of `end function`', () => {
diff --git a/src/parser/tests/statement/Misc.spec.ts b/src/parser/tests/statement/Misc.spec.ts
index 066b080a..cef3684f 100644
--- a/src/parser/tests/statement/Misc.spec.ts
+++ b/src/parser/tests/statement/Misc.spec.ts
@@ -5,6 +5,7 @@ import { DisallowedLocalIdentifiersText, TokenKind } from '../../../lexer/TokenK
 import { Range } from 'vscode-languageserver';
 import type { AAMemberExpression } from '../../Expression';
 import { expectZeroDiagnostics } from '../../../testHelpers.spec';
+import type { Statement } from '../../AstNode';
 
 describe('parser', () => {
     describe('`end` keyword', () => {
@@ -59,7 +60,7 @@ describe('parser', () => {
     });
 
     it('most reserved words are not allowed as local var identifiers', () => {
-        let statementList = [];
+        let statementList: Statement[][] = [];
         [...DisallowedLocalIdentifiersText].filter(x => x === 'if').forEach((disallowedIdentifier) => {
             //use the lexer to generate tokens because there are many different TokenKind types represented in this list
             let { tokens } = Lexer.scan(`
diff --git a/src/parser/tests/statement/ReturnStatement.spec.ts b/src/parser/tests/statement/ReturnStatement.spec.ts
index fe1563f9..0c65b1ea 100644
--- a/src/parser/tests/statement/ReturnStatement.spec.ts
+++ b/src/parser/tests/statement/ReturnStatement.spec.ts
@@ -51,7 +51,7 @@ describe('parser return statements', () => {
             token(TokenKind.Newline, '\\n'),
             token(TokenKind.Return, 'return'),
             identifier('RebootSystem'),
-            { kind: TokenKind.LeftParen, text: '(', range: null },
+            { kind: TokenKind.LeftParen, text: '(', range: null as any },
             token(TokenKind.RightParen, ')'),
             token(TokenKind.Newline, '\\n'),
             token(TokenKind.EndFunction, 'end function'),
diff --git a/src/parser/tests/statement/Throw.spec.ts b/src/parser/tests/statement/Throw.spec.ts
index 4f970ae3..ccb8bf78 100644
--- a/src/parser/tests/statement/Throw.spec.ts
+++ b/src/parser/tests/statement/Throw.spec.ts
@@ -12,7 +12,7 @@ describe('parser ThrowStatement', () => {
             catch
             end try
         `);
-        const throwStatement = (parser.ast.statements[0] as TryCatchStatement).tryBranch.statements[0] as ThrowStatement;
+        const throwStatement = (parser.ast.statements[0] as TryCatchStatement).tryBranch!.statements[0] as ThrowStatement;
         //the statement should still exist and have null expression
         expect(throwStatement).to.exist;
         expect(throwStatement.expression).to.be.instanceof(LiteralExpression);
@@ -26,7 +26,7 @@ describe('parser ThrowStatement', () => {
             end try
         `);
         expect(parser.diagnostics[0]?.message).to.eql(DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword().message);
-        const throwStatement = (parser.ast.statements[0] as TryCatchStatement).tryBranch.statements[0] as ThrowStatement;
+        const throwStatement = (parser.ast.statements[0] as TryCatchStatement).tryBranch!.statements[0] as ThrowStatement;
         //the statement should still exist and have null expression
         expect(throwStatement).to.exist;
         expect(throwStatement.expression).to.not.exist;
diff --git a/src/parser/tests/statement/TryCatch.spec.ts b/src/parser/tests/statement/TryCatch.spec.ts
index b7b237f7..4c35d3db 100644
--- a/src/parser/tests/statement/TryCatch.spec.ts
+++ b/src/parser/tests/statement/TryCatch.spec.ts
@@ -20,9 +20,9 @@ describe('parser try/catch', () => {
         expect(stmt.tryBranch).to.exist.and.ownProperty('statements').to.be.lengthOf(1);
         expect(stmt.catchStatement).to.exist;
         const cstmt = stmt.catchStatement;
-        expect(cstmt.tokens.catch?.text).to.eql('catch');
-        expect(cstmt.exceptionVariable.text).to.eql('e');
-        expect(cstmt.catchBranch).to.exist.and.ownProperty('statements').to.be.lengthOf(1);
+        expect(cstmt!.tokens.catch?.text).to.eql('catch');
+        expect(cstmt!.exceptionVariable!.text).to.eql('e');
+        expect(cstmt!.catchBranch).to.exist.and.ownProperty('statements').to.be.lengthOf(1);
         expect(stmt.tokens.endTry?.text).to.eql('end try');
     });
 
diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts
index a865fd74..891e59d7 100644
--- a/src/testHelpers.spec.ts
+++ b/src/testHelpers.spec.ts
@@ -85,7 +85,7 @@ function cloneDiagnostic(actualDiagnosticInput: BsDiagnostic, expectedDiagnostic
         for (let j = 0; j < actualDiagnostic.relatedInformation.length; j++) {
             actualDiagnostic.relatedInformation[j] = cloneObject(
                 actualDiagnostic.relatedInformation[j],
-                expectedDiagnostic?.relatedInformation[j],
+                expectedDiagnostic?.relatedInformation?.[j],
                 ['location', 'message']
             ) as any;
         }
@@ -191,7 +191,7 @@ export function expectZeroDiagnostics(arg: DiagnosticCollection) {
  * @param diagnosticsCollection a collection of diagnostics
  * @param length if specified, checks the diagnostic count is exactly that amount. If omitted, the collection is just verified as non-empty
  */
-export function expectHasDiagnostics(diagnosticsCollection: DiagnosticCollection, length: number = null) {
+export function expectHasDiagnostics(diagnosticsCollection: DiagnosticCollection, length: number | null = null) {
     const diagnostics = getDiagnostics(diagnosticsCollection);
     if (length) {
         expect(diagnostics).lengthOf(length);
@@ -245,7 +245,7 @@ export function getTestGetTypedef(scopeGetter: () => [program: Program, rootDir:
         return {
             code: (file as BrsFile).getTypedef(),
             map: undefined
-        };
+        } as any as CodeWithSourceMap;
     }, scopeGetter);
 }
 
@@ -307,7 +307,7 @@ export function expectCompletionsIncludes(completions: CompletionItem[], expecte
             //match all existing properties of the expectedItem
             let actualItem = pick(
                 expectedItem,
-                completions.find(x => x.label === expectedItem.label)
+                completions.find(x => x.label === expectedItem.label)!
             );
             expect(actualItem).to.eql(expectedItem);
         }
@@ -325,14 +325,14 @@ export function expectCompletionsExcludes(completions: CompletionItem[], expecte
             //match all existing properties of the expectedItem
             let actualItem = pick(
                 expectedItem,
-                completions.find(x => x.label === expectedItem.label)
+                completions.find(x => x.label === expectedItem.label)!
             );
             expect(actualItem).to.not.eql(expectedItem);
         }
     }
 }
 
-export function expectThrows(callback: () => any, expectedMessage = undefined, failedTestMessage = 'Expected to throw but did not') {
+export function expectThrows(callback: () => any, expectedMessage: string | undefined = undefined, failedTestMessage = 'Expected to throw but did not') {
     let wasExceptionThrown = false;
     try {
         callback();