Repository: rokucommunity/brighterscript
Pull request #1070 (2024-02-12 17:10:09)

Modifies all SG AST constructors to use named properties in objects
Addresses: #1046




Patch:
diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts
index d9384583..f2b83805 100644
--- a/src/Scope.spec.ts
+++ b/src/Scope.spec.ts
@@ -631,7 +631,7 @@ describe('Scope', () => {
                 end function
             `);
             program.validate();
-            expectDiagnostics(program, [
+            expectDiagnosticsIncludes(program, [
                 DiagnosticMessages.cannotFindName('param2'),
                 DiagnosticMessages.cannotFindName('name2'),
                 DiagnosticMessages.cannotFindName('item2'),
diff --git a/src/Scope.ts b/src/Scope.ts
index 0ff957a8..3a97aa94 100644
--- a/src/Scope.ts
+++ b/src/Scope.ts
@@ -8,7 +8,6 @@ import type { CallableContainer, BsDiagnosticWithOrigin, FileReference, Callable
 import type { Program } from './Program';
 import { BsClassValidator } from './validators/ClassValidator';
 import type { NamespaceStatement, ClassStatement, EnumStatement, InterfaceStatement, EnumMemberStatement, ConstStatement } from './parser/Statement';
-import type { NewExpression } from './parser/Expression';
 import { ParseMode } from './parser/Parser';
 import { util } from './util';
 import { globalCallableMap } from './globalCallables';
@@ -1086,19 +1085,6 @@ export class Scope {
 
     }
 
-
-    public getNewExpressions() {
-        let result = [] as AugmentedNewExpression[];
-        this.enumerateBrsFiles((file) => {
-            let expressions = file['_cachedLookups'].newExpressions as AugmentedNewExpression[];
-            for (let expression of expressions) {
-                expression.file = file;
-                result.push(expression);
-            }
-        });
-        return result;
-    }
-
     private validateClasses() {
         let validator = new BsClassValidator(this);
         validator.validate();
@@ -1332,7 +1318,3 @@ export class Scope {
         return items;
     }
 }
-
-interface AugmentedNewExpression extends NewExpression {
-    file: BscFile;
-}
diff --git a/src/astUtils/CachedLookups.ts b/src/astUtils/CachedLookups.ts
index 9cec0408..88048ce4 100644
--- a/src/astUtils/CachedLookups.ts
+++ b/src/astUtils/CachedLookups.ts
@@ -1,4 +1,4 @@
-import type { AALiteralExpression, CallExpression, CallfuncExpression, DottedGetExpression, FunctionExpression, NewExpression, VariableExpression } from '../parser/Expression';
+import type { AALiteralExpression, CallExpression, CallfuncExpression, DottedGetExpression, FunctionExpression, VariableExpression } from '../parser/Expression';
 import type { AssignmentStatement, ClassStatement, ConstStatement, EnumStatement, FunctionStatement, ImportStatement, InterfaceStatement, LibraryStatement, NamespaceStatement } from '../parser/Statement';
 import { Cache } from '../Cache';
 import { WalkMode, createVisitor } from './visitors';
@@ -70,10 +70,6 @@ export class CachedLookups {
         return this.getFromCache<Set<Expression>>('expressions');
     }
 
-    get newExpressions(): NewExpression[] {
-        return this.getFromCache<Array<NewExpression>>('newExpressions');
-    }
-
     get classStatements(): ClassStatement[] {
         return this.getFromCache<Array<ClassStatement>>('classStatements');
     }
@@ -166,7 +162,6 @@ export class CachedLookups {
         const importStatements: ImportStatement[] = [];
         const functionStatements: FunctionStatement[] = [];
         const functionExpressions: FunctionExpression[] = [];
-        const newExpressions: NewExpression[] = [];
 
         const propertyHints: Record<string, string> = {};
         const addPropertyHints = (item: Token | AALiteralExpression) => {
@@ -266,12 +261,6 @@ export class CachedLookups {
                     functionExpressions.push(expression);
                 }
             },
-            NewExpression: e => {
-                newExpressions.push(e);
-                for (const p of e.call.args) {
-                    expressions.add(p);
-                }
-            },
             ExpressionStatement: s => {
                 expressions.add(s.expression);
             },
@@ -348,7 +337,6 @@ export class CachedLookups {
         this.cache.set('importStatements', importStatements);
         this.cache.set('functionStatements', functionStatements);
         this.cache.set('functionExpressions', functionExpressions);
-        this.cache.set('newExpressions', newExpressions);
         this.cache.set('propertyHints', propertyHints);
     }
 }
diff --git a/src/astUtils/creators.ts b/src/astUtils/creators.ts
index c9d3b75a..e6317ce9 100644
--- a/src/astUtils/creators.ts
+++ b/src/astUtils/creators.ts
@@ -189,13 +189,13 @@ export function createSGToken(text: string, range?: Range) {
  * Create an SGAttribute without any ranges
  */
 export function createSGAttribute(keyName: string, value: string) {
-    return new SGAttribute(
-        { text: keyName },
-        { text: '=' },
-        { text: '"' },
-        { text: value },
-        { text: '"' }
-    );
+    return new SGAttribute({
+        key: { text: keyName },
+        equals: { text: '=' },
+        openingQuote: { text: '"' },
+        value: { text: value },
+        closingQuote: { text: '"' }
+    });
 }
 
 export function createSGInterfaceField(id: string, attributes: { type?: string; alias?: string; value?: string; onChange?: string; alwaysNotify?: string } = {}) {
@@ -207,12 +207,12 @@ export function createSGInterfaceField(id: string, attributes: { type?: string;
             createSGAttribute(key, attributes[key])
         );
     }
-    return new SGInterfaceField(
-        { text: '<' },
-        { text: 'field' },
-        attrs,
-        { text: '/>' }
-    );
+    return new SGInterfaceField({
+        startTagOpen: { text: '<' },
+        startTagName: { text: 'field' },
+        attributes: attrs,
+        startTagClose: { text: '/>' }
+    });
 }
 
 export function createSGComponent(name: string, parentName?: string) {
@@ -224,38 +224,38 @@ export function createSGComponent(name: string, parentName?: string) {
             createSGAttribute('extends', parentName)
         );
     }
-    return new SGComponent(
-        { text: '<' },
-        { text: 'component' },
-        attributes,
-        { text: '>' },
-        [],
-        { text: '</' },
-        { text: 'component' },
-        { text: '>' }
-    );
+    return new SGComponent({
+        startTagOpen: { text: '<' },
+        startTagName: { text: 'component' },
+        attributes: attributes,
+        startTagClose: { text: '>' },
+        elements: [],
+        endTagOpen: { text: '</' },
+        endTagName: { text: 'component' },
+        endTagClose: { text: '>' }
+    });
 }
 
 export function createSGInterfaceFunction(functionName: string) {
-    return new SGInterfaceFunction(
-        { text: '<' },
-        { text: 'function' },
-        [createSGAttribute('name', functionName)],
-        { text: '/>' }
-    );
+    return new SGInterfaceFunction({
+        startTagOpen: { text: '<' },
+        startTagName: { text: 'function' },
+        attributes: [createSGAttribute('name', functionName)],
+        startTagClose: { text: '/>' }
+    });
 }
 
 export function createSGInterface() {
-    return new SGInterface(
-        { text: '<' },
-        { text: 'interface' },
-        [],
-        { text: '>' },
-        [],
-        { text: '</' },
-        { text: 'interface' },
-        { text: '>' }
-    );
+    return new SGInterface({
+        startTagOpen: { text: '<' },
+        startTagName: { text: 'interface' },
+        attributes: [],
+        startTagClose: { text: '>' },
+        elements: [],
+        endTagOpen: { text: '</' },
+        endTagName: { text: 'interface' },
+        endTagClose: { text: '>' }
+    });
 }
 
 export function createSGScript(attributes: { type?: string; uri?: string }) {
@@ -265,10 +265,10 @@ export function createSGScript(attributes: { type?: string; uri?: string }) {
             createSGAttribute(key, attributes[key])
         );
     }
-    return new SGScript(
-        { text: '<' },
-        { text: 'script' },
-        attrs,
-        { text: '/>' }
-    );
+    return new SGScript({
+        startTagOpen: { text: '<' },
+        startTagName: { text: 'script' },
+        attributes: attrs,
+        startTagClose: { text: '/>' }
+    });
 }
diff --git a/src/astUtils/reflection.spec.ts b/src/astUtils/reflection.spec.ts
index 1e23edb5..604fc9c9 100644
--- a/src/astUtils/reflection.spec.ts
+++ b/src/astUtils/reflection.spec.ts
@@ -61,7 +61,7 @@ describe('reflection', () => {
         const library = new LibraryStatement({ library: token, filePath: token });
         const namespace = new NamespaceStatement({ namespace: token, nameExpression: createVariableExpression('a', range), body: body, endNamespace: token });
         const cls = new ClassStatement({ class: token, name: ident, body: [], endClass: token });
-        const imports = new ImportStatement({ import: token, filePath: token });
+        const imports = new ImportStatement({ import: token, path: token });
         const catchStmt = new CatchStatement({ catch: token, exceptionVariable: ident, catchBranch: block });
         const tryCatch = new TryCatchStatement({ try: token, tryBranch: block, catchStatement: catchStmt });
         const throwSt = new ThrowStatement({ throw: createToken(TokenKind.Throw) });
diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts
index 20285511..68365c47 100644
--- a/src/bscPlugin/validation/ScopeValidator.spec.ts
+++ b/src/bscPlugin/validation/ScopeValidator.spec.ts
@@ -1729,7 +1729,7 @@ describe('ScopeValidator', () => {
         it('validates when a class member is accessed from a class directly', () => {
             program.setFile('source/util.bs', `
                 class Klass
-                      name as string
+                    name as string
                 end class
 
                 sub doStuff()
@@ -1759,6 +1759,22 @@ describe('ScopeValidator', () => {
                 DiagnosticMessages.itemCannotBeUsedAsVariable('Alpha.Klass')
             ]);
         });
+
+        it('validates when new is is used on a class instance', () => {
+            program.setFile('source/util.bs', `
+                class Klass
+                    name as string
+                end class
+
+                sub doStuff(someKlass as Klass)
+                    print new someKlass()
+                end sub
+            `);
+            program.validate();
+            expectDiagnostics(program, [
+                DiagnosticMessages.expressionIsNotConstructable('someKlass')
+            ]);
+        });
     });
 
     describe('returnTypeMismatch', () => {
diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts
index 8ce6f5e2..cdf04469 100644
--- a/src/bscPlugin/validation/ScopeValidator.ts
+++ b/src/bscPlugin/validation/ScopeValidator.ts
@@ -13,7 +13,7 @@ import type { BRSComponentData } from '../../roku-types';
 import type { Token } from '../../lexer/Token';
 import { AstNodeKind, type AstNode } from '../../parser/AstNode';
 import type { Expression } from '../../parser/AstNode';
-import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression } from '../../parser/Expression';
+import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression } from '../../parser/Expression';
 import { CallExpression } from '../../parser/Expression';
 import { createVisitor } from '../../astUtils/visitors';
 import type { BscType } from '../../types/BscType';
@@ -108,6 +108,9 @@ export class ScopeValidator {
                     },
                     AssignmentStatement: (assignStmt) => {
                         this.validateAssignmentStatement(file, assignStmt);
+                    },
+                    NewExpression: (newExpr) => {
+                        this.validateNewExpression(file, newExpr);
                     }
                 });
                 const segmentsToWalkForValidation = (thisFileHasChanges || !hasChangeInfo)
@@ -732,6 +735,28 @@ export class ScopeValidator {
         return true;
     }
 
+    /**
+     * Find all "new" statements in the program,
+     * and make sure we can find a class with that name
+     */
+    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
+        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
+        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
+        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
+
+        if (!newableClass) {
+            //try and find functions with this name.
+            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
+
+            this.addMultiScopeDiagnostic({
+                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
+                file: file,
+                range: newExpression.className.range
+            });
+
+        }
+    }
+
     /**
      * Adds a diagnostic to the first scope for this key. Prevents duplicate diagnostics
      * for diagnostics where scope isn't important. (i.e. CreateObject validations)
diff --git a/src/files/BrsFile.Class.spec.ts b/src/files/BrsFile.Class.spec.ts
index 6e63f5c9..10dee0f3 100644
--- a/src/files/BrsFile.Class.spec.ts
+++ b/src/files/BrsFile.Class.spec.ts
@@ -1201,7 +1201,7 @@ describe('BrsFile BrighterScript classes', () => {
         `);
         program.validate();
         expectDiagnostics(program, [
-            DiagnosticMessages.expressionIsNotConstructable('sub')
+            DiagnosticMessages.expressionIsNotConstructable('quack')
         ]);
     });
 
@@ -1599,8 +1599,9 @@ describe('BrsFile BrighterScript classes', () => {
             end sub
         `);
         program.validate();
-        expectDiagnostics(program, [
-            DiagnosticMessages.cannotFindName('Duck')
+        expectDiagnosticsIncludes(program, [
+            DiagnosticMessages.cannotFindName('Duck'),
+            DiagnosticMessages.expressionIsNotConstructable('Duck')
         ]);
     });
 
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index 93b1a389..c2ce4e9d 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -3,7 +3,6 @@ import * as sinonImport from 'sinon';
 import { Position, Range } from 'vscode-languageserver';
 import type { BsDiagnostic, Callable, CommentFlag, VariableDeclaration } from '../interfaces';
 import { Program } from '../Program';
-import { BooleanType } from '../types/BooleanType';
 import { DynamicType } from '../types/DynamicType';
 import { TypedFunctionType } from '../types/TypedFunctionType';
 import { IntegerType } from '../types/IntegerType';
@@ -217,7 +216,7 @@ describe('BrsFile', () => {
         file.parser.ast.statements.push(
             new ImportStatement({
                 import: createToken(TokenKind.Import),
-                filePath: createToken(TokenKind.StringLiteral, 'pkg:/source/lib.brs')
+                path: createToken(TokenKind.StringLiteral, 'pkg:/source/lib.brs')
             })
         );
         expect(file.ownScriptImports).to.be.empty;
@@ -1222,29 +1221,6 @@ describe('BrsFile', () => {
             expect(file.callables[1].nameRange!.start.line).to.equal(5);
         });
 
-        it('finds function call line and column numbers', () => {
-            let file = new BrsFile({
-                srcPath: 'absolute_path/file.brs',
-                destPath: 'relative_path/file.brs',
-                program: program
-            });
-            file.parse(`
-                function DoA()
-                    DoB("a")
-                end function
-                function DoB(a as string)
-                    DoC()
-                end function
-            `);
-            expect(file.functionCalls.length).to.equal(2);
-
-            expect(file.functionCalls[0].range).to.eql(Range.create(2, 20, 2, 28));
-            expect(file.functionCalls[0].nameRange).to.eql(Range.create(2, 20, 2, 23));
-
-            expect(file.functionCalls[1].range).to.eql(Range.create(5, 20, 5, 25));
-            expect(file.functionCalls[1].nameRange).to.eql(Range.create(5, 20, 5, 23));
-        });
-
         it('finds function calls that are unfinished', () => {
             let file = new BrsFile({
                 srcPath: 'absolute_path/file.brs',
@@ -1266,13 +1242,6 @@ describe('BrsFile', () => {
                 DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
                 DiagnosticMessages.expectedNewlineOrColon()
             ]);
-            expect(file.functionCalls.length).to.equal(2);
-
-            expect(file.functionCalls[0].range).to.eql(Range.create(2, 20, 2, 27));
-            expect(file.functionCalls[0].nameRange).to.eql(Range.create(2, 20, 2, 23));
-
-            expect(file.functionCalls[1].range).to.eql(Range.create(5, 20, 5, 24));
-            expect(file.functionCalls[1].nameRange).to.eql(Range.create(5, 20, 5, 23));
         });
 
         it('sanitizes brs errors', () => {
@@ -1463,83 +1432,6 @@ describe('BrsFile', () => {
         });
     });
 
-    describe('findCallableInvocations', () => {
-        it('finds arguments with literal values', () => {
-            let file = new BrsFile({
-                srcPath: 'absolute_path/file.brs',
-                destPath: 'relative_path/file.brs',
-                program: program
-            });
-            file.parse(`
-                function Sum()
-                    DoSomething("name", 12, true)
-                end function
-            `);
-            expect(file.functionCalls.length).to.equal(1);
-
-            const argsMap = file.functionCalls[0].args.map(arg => {
-                // disregard arg.expression, etc.
-                return { type: arg.type, range: arg.range, text: arg.text };
-            });
-            expect(argsMap).to.eql([{
-                type: StringType.instance,
-                range: util.createRange(2, 32, 2, 38),
-                text: '"name"'
-            }, {
-                type: IntegerType.instance,
-                range: util.createRange(2, 40, 2, 42),
-                text: '12'
-            }, {
-                type: BooleanType.instance,
-                range: util.createRange(2, 44, 2, 48),
-                text: 'true'
-            }]);
-        });
-
-        it('finds function calls nested inside statements', () => {
-            program.setFile(`source/main.brs`, `
-                sub main()
-                    if true then
-                        DoesNotExist(1, 2)
-                    end if
-                end sub
-            `);
-            program.validate();
-            expectDiagnostics(program, [
-                DiagnosticMessages.cannotFindName('DoesNotExist')
-            ]);
-        });
-
-        it('finds arguments with variable values', () => {
-            let file = new BrsFile({
-                srcPath: 'absolute_path/file.brs',
-                destPath: 'relative_path/file.brs',
-                program: program
-            });
-            file.parse(`
-                function Sum()
-                    count = 1
-                    name = "John"
-                    isAlive = true
-                    DoSomething(count, name, isAlive)
-                end function
-            `);
-            expect(file.functionCalls.length).to.equal(1);
-            expect(file.functionCalls[0].args[0]).deep.include({
-                type: new DynamicType(),
-                text: 'count'
-            });
-            expect(file.functionCalls[0].args[1]).deep.include({
-                type: new DynamicType(),
-                text: 'name'
-            });
-            expect(file.functionCalls[0].args[2]).deep.include({
-                type: new DynamicType(),
-                text: 'isAlive'
-            });
-        });
-    });
-
     describe('findCallables', () => {
         //this test is to help with code coverage
         it('skips top-level statements', () => {
diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts
index a67eba32..e14df3a5 100644
--- a/src/files/BrsFile.ts
+++ b/src/files/BrsFile.ts
@@ -7,12 +7,12 @@ import chalk from 'chalk';
 import * as path from 'path';
 import { DiagnosticCodeMap, diagnosticCodes, DiagnosticMessages } from '../DiagnosticMessages';
 import { FunctionScope } from '../FunctionScope';
-import type { Callable, CallableArg, CallableParam, CommentFlag, FunctionCall, BsDiagnostic, FileReference, FileLink, SerializedCodeFile, NamespaceContainer } from '../interfaces';
+import type { Callable, CallableParam, CommentFlag, BsDiagnostic, FileReference, FileLink, SerializedCodeFile, NamespaceContainer } from '../interfaces';
 import type { Token } from '../lexer/Token';
 import { Lexer } from '../lexer/Lexer';
 import { TokenKind, AllowedLocalIdentifiers } from '../lexer/TokenKind';
 import { Parser, ParseMode } from '../parser/Parser';
-import type { FunctionExpression, VariableExpression } from '../parser/Expression';
+import type { FunctionExpression } from '../parser/Expression';
 import type { ClassStatement, NamespaceStatement, MethodStatement, FieldStatement } from '../parser/Statement';
 import type { Program } from '../Program';
 import { DynamicType } from '../types/DynamicType';
@@ -21,7 +21,7 @@ import { BrsTranspileState } from '../parser/BrsTranspileState';
 import { Preprocessor } from '../preprocessor/Preprocessor';
 import { LogLevel } from '../Logger';
 import { serializeError } from 'serialize-error';
-import { isMethodStatement, isClassStatement, isDottedGetExpression, isFunctionStatement, isLiteralExpression, isNamespaceStatement, isStringType, isVariableExpression, isImportStatement, isFieldStatement, isFunctionExpression, isBrsFile, isEnumStatement, isConstStatement, isAnyReferenceType } from '../astUtils/reflection';
+import { isMethodStatement, isClassStatement, isDottedGetExpression, isFunctionStatement, isNamespaceStatement, isVariableExpression, isImportStatement, isFieldStatement, isFunctionExpression, isBrsFile, isEnumStatement, isConstStatement, isAnyReferenceType } from '../astUtils/reflection';
 import { createVisitor, WalkMode } from '../astUtils/visitors';
 import type { DependencyGraph } from '../DependencyGraph';
 import { CommentFlagProcessor } from '../CommentFlagProcessor';
@@ -199,12 +199,12 @@ export class BrsFile implements BscFile {
             const result = [] as FileReference[];
             for (const statement of this._cachedLookups?.importStatements ?? []) {
                 //register import statements
-                if (isImportStatement(statement) && statement.tokens.filePath) {
+                if (isImportStatement(statement) && statement.tokens.path) {
                     result.push({
-                        filePathRange: statement.tokens.filePath.range,
+                        filePathRange: statement.tokens.path.range,
                         destPath: util.getPkgPathFromTarget(this.destPath, statement.filePath),
                         sourceFile: this,
-                        text: statement.tokens.filePath.text
+                        text: statement.tokens.path.text
                     });
                 }
             }
@@ -621,103 +621,6 @@ export class BrsFile implements BscFile {
 
     }
 
-    get functionCalls(): FunctionCall[] {
-        return this.cache.getOrAdd(`BrsFile_functionCalls`, () => {
-            const functionCalls = [];
-            //for every function in the file
-            for (let func of this._cachedLookups.functionExpressions) {
-                //for all function calls in this function
-                for (let expression of func.callExpressions) {
-                    if (
-                        //filter out dotted function invocations (i.e. object.doSomething()) (not currently supported. TODO support it)
-                        (expression.callee as any).obj ||
-                        //filter out method calls on method calls for now (i.e. getSomething().getSomethingElse())
-                        (expression.callee as any).callee ||
-                        //filter out callees without a name (immediately-invoked function expressions)
-                        !(expression.callee as any).tokens?.name
-                    ) {
-                        continue;
-                    }
-                    //callee is the name of the function being called
-                    let callee = expression.callee as VariableExpression;
-
-                    let functionName = callee.tokens.name.text;
-                    let columnIndexBegin = callee.range.start.character;
-                    let columnIndexEnd = callee.range.end.character;
-
-                    let args = [] as CallableArg[];
-                    //TODO convert if stmts to use instanceof instead
-                    for (let arg of expression.args as any) {
-
-                        //is a literal parameter value
-                        if (isLiteralExpression(arg)) {
-                            args.push({
-                                range: arg.range,
-                                type: arg.getType(),
-                                text: arg.tokens.value.text,
-                                expression: arg,
-                                typeToken: undefined
-                            });
-
-                            //is variable being passed into argument
-                        } else if (arg.tokens?.name) {
-                            args.push({
-                                range: arg.range,
-                                //TODO - look up the data type of the actual variable
-                                type: new DynamicType(),
-                                text: arg.tokens.name.text,
-                                expression: arg,
-                                typeToken: undefined
-                            });
-
-                        } else if (arg.value) {
-                            let text = '';
-                            /* istanbul ignore next: TODO figure out why value is undefined sometimes */
-                            if (arg.value.value) {
-                                text = arg.value.value.toString();
-                            }
-                            let callableArg = {
-                                range: arg.range,
-                                //TODO not sure what to do here
-                                type: new DynamicType(), // util.valueKindToBrsType(arg.value.kind),
-                                text: text,
-                                expression: arg,
-                                typeToken: undefined
-                            };
-                            //wrap the value in quotes because that's how it appears in the code
-                            if (isStringType(callableArg.type)) {
-                                callableArg.text = '"' + callableArg.text + '"';
-                            }
-                            args.push(callableArg);
-
-                        } else {
-                            args.push({
-                                range: arg.range,
-                                type: new DynamicType(),
-                                //TODO get text from other types of args
-                                text: '',
-                                expression: arg,
-                                typeToken: undefined
-                            });
-                        }
-                    }
-                    let functionCall: FunctionCall = {
-                        range: expression.range,
-                        functionScope: this.getFunctionScopeAtPosition(callee.range.start),
-                        file: this,
-                        name: functionName,
-                        nameRange: util.createRange(callee.range.start.line, columnIndexBegin, callee.range.start.line, columnIndexEnd),
-                        //TODO keep track of parameters
-                        args: args,
-                        expression: expression
-                    };
-                    functionCalls.push(functionCall);
-                }
-            }
-            return functionCalls;
-        });
-    }
-
     /**
      * Find the function scope at the given position.
      * @param position the position used to find the deepest scope that contains it
diff --git a/src/files/XmlFile.ts b/src/files/XmlFile.ts
index 26dfee10..da17b6f7 100644
--- a/src/files/XmlFile.ts
+++ b/src/files/XmlFile.ts
@@ -3,7 +3,7 @@ import type { CodeWithSourceMap } from 'source-map';
 import { SourceNode } from 'source-map';
 import type { Location, Position, Range } from 'vscode-languageserver';
 import { DiagnosticCodeMap, diagnosticCodes } from '../DiagnosticMessages';
-import type { Callable, BsDiagnostic, FileReference, FunctionCall, CommentFlag, SerializedCodeFile } from '../interfaces';
+import type { Callable, BsDiagnostic, FileReference, CommentFlag, SerializedCodeFile } from '../interfaces';
 import type { Program } from '../Program';
 import util from '../util';
 import { standardizePath as s } from '../util';
@@ -211,9 +211,6 @@ export class XmlFile implements BscFile {
     //TODO implement the xml CDATA parsing, which would populate this list
     public callables = [] as Callable[];
 
-    //TODO implement the xml CDATA parsing, which would populate this list
-    public functionCalls = [] as FunctionCall[];
-
     public functionScopes = [] as FunctionScope[];
 
     /**
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 13d73bb1..725c4ff1 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -2,7 +2,6 @@ import type { Range, Diagnostic, CodeAction, SemanticTokenTypes, SemanticTokenMo
 import type { Scope } from './Scope';
 import type { BrsFile } from './files/BrsFile';
 import type { XmlFile } from './files/XmlFile';
-import type { FunctionScope } from './FunctionScope';
 import type { TypedFunctionType } from './types/TypedFunctionType';
 import type { ParseMode } from './parser/Parser';
 import type { Program } from './Program';
@@ -19,7 +18,6 @@ import type { FileFactory } from './files/Factory';
 import type { LazyFileData } from './files/LazyFileData';
 import type { SymbolTable } from './SymbolTable';
 import type { SymbolTypeFlag } from './SymbolTableFlag';
-import type { CallExpression } from './parser/Expression';
 import { createToken } from './astUtils/creators';
 import { TokenKind } from './lexer/TokenKind';
 
@@ -80,30 +78,6 @@ export interface Callable {
     functionStatement: FunctionStatement;
 }
 
-export interface FunctionCall {
-    /**
-     * The full range of this function call (from the start of the function name to its closing paren)
-     */
-    range: Range;
-    expression: CallExpression;
-    functionScope: FunctionScope;
-    file: BscFile;
-    name: string;
-    args: CallableArg[];
-    nameRange: Range;
-}
-
-/**
- * An argument for an expression call.
- */
-export interface CallableArg {
-    text: string;
-    type: BscType;
-    typeToken: Token;
-    range: Range;
-    expression: Expression;
-}
-
 export interface CallableParam {
     name: string;
     type: BscType;
@@ -883,3 +857,10 @@ export interface NamespaceContainer {
     namespaces?: Map<string, NamespaceContainer>;
     symbolTable: SymbolTable;
 }
+
+/**
+ * Use Writable<T> to remove readonly flag from properties in T
+ * Be careful!
+ */
+export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
+export type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts
index 36613aa2..601e07d6 100644
--- a/src/parser/Expression.ts
+++ b/src/parser/Expression.ts
@@ -46,12 +46,12 @@ export class BinaryExpression extends Expression {
         this.right = options.right;
         this.range = util.createBoundingRange(this.left, this.tokens.operator, this.right);
     }
-    public tokens: {
-        operator: Token;
+    readonly tokens: {
+        readonly operator: Token;
     };
 
-    public left: Expression;
-    public right: Expression;
+    public readonly left: Expression;
+    public readonly right: Expression;
 
     public readonly kind = AstNodeKind.BinaryExpression;
 
@@ -117,7 +117,7 @@ export class CallExpression extends Expression {
 
     readonly callee: Expression;
     readonly args: Expression[];
-    public tokens: {
+    readonly tokens: {
         /**
          * Can either be `(`, or `?(` for optional chaining - defaults to '('
          */
@@ -208,7 +208,7 @@ export class FunctionExpression extends Expression implements TypedefProvider {
 
         //if there's a body, and it doesn't have a SymbolTable, assign one
         if (this.body && !this.body.symbolTable) {
-            this.body.symbolTable = new SymbolTable(`Function Body`);
+            this.body.symbolTable = new SymbolTable(`Block`, () => this.getSymbolTable());
         }
         this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
     }
@@ -216,23 +216,17 @@ export class FunctionExpression extends Expression implements TypedefProvider {
     public readonly kind = AstNodeKind.FunctionExpression;
 
     readonly parameters: FunctionParameterExpression[];
-    public body: Block;
-    public returnTypeExpression?: TypeExpression;
+    public readonly body: Block;
+    public readonly returnTypeExpression?: TypeExpression;
 
     readonly tokens: {
-        functionType?: Token;
-        endFunctionType?: Token;
-        leftParen?: Token;
-        rightParen?: Token;
-        as?: Token;
+        readonly functionType?: Token;
+        readonly endFunctionType?: Token;
+        readonly leftParen?: Token;
+        readonly rightParen?: Token;
+        readonly as?: Token;
     };
 
-    /**
-     * The list of function calls that are declared within this function scope. This excludes CallExpressions
-     * declared in child functions
-     */
-    public callExpressions = [] as CallExpression[];
-
     /**
      * If this function is part of a FunctionStatement, this will be set. Otherwise this will be undefined
      */
@@ -414,14 +408,14 @@ export class FunctionParameterExpression extends Expression {
 
     public readonly kind = AstNodeKind.FunctionParameterExpression;
 
-    public tokens: {
-        name: Identifier;
-        equals?: Token;
-        as?: Token;
+    readonly tokens: {
+        readonly name: Identifier;
+        readonly equals?: Token;
+        readonly as?: Token;
     };
 
-    public defaultValue?: Expression;
-    public typeExpression?: TypeExpression;
+    public readonly defaultValue?: Expression;
+    public readonly typeExpression?: TypeExpression;
 
     public getType(options: GetTypeOptions) {
         const paramType = this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime, typeChain: undefined }) ??
@@ -510,8 +504,8 @@ export class DottedGetExpression extends Expression {
     }
 
     readonly tokens: {
-        name: Identifier;
-        dot?: Token;
+        readonly name: Identifier;
+        readonly dot?: Token;
     };
     readonly obj: Expression;
 
@@ -582,12 +576,12 @@ export class XmlAttributeGetExpression extends Expression {
 
     public readonly kind = AstNodeKind.XmlAttributeGetExpression;
 
-    public tokens: {
+    public readonly tokens: {
         name: Identifier;
         at?: Token;
     };
 
-    readonly obj: Expression;
+    public readonly obj: Expression;
 
     public readonly range: Range;
 
@@ -630,16 +624,16 @@ export class IndexedGetExpression extends Expression {
 
     public readonly kind = AstNodeKind.IndexedGetExpression;
 
-    public obj: Expression;
-    public indexes: Expression[];
+    public readonly obj: Expression;
+    public readonly indexes: Expression[];
 
-    public tokens: {
+    readonly tokens: {
         /**
          * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.` - defaults to '[' in transpile
          */
-        openingSquare?: Token;
-        closingSquare?: Token;
-        questionDot?: Token; //  ? or ?.
+        readonly openingSquare?: Token;
+        readonly closingSquare?: Token;
+        readonly questionDot?: Token; //  ? or ?.
     };
 
     public readonly range: Range;
@@ -699,11 +693,11 @@ export class GroupingExpression extends Expression {
         this.range = util.createBoundingRange(this.tokens.leftParen, this.expression, this.tokens.rightParen);
     }
 
-    readonly tokens: {
-        leftParen?: Token;
-        rightParen?: Token;
+    public readonly tokens: {
+        readonly leftParen?: Token;
+        readonly rightParen?: Token;
     };
-    public expression: Expression;
+    public readonly expression: Expression;
 
     public readonly kind = AstNodeKind.GroupingExpression;
 
@@ -741,8 +735,8 @@ export class LiteralExpression extends Expression {
         };
     }
 
-    public tokens: {
-        value: Token;
+    public readonly tokens: {
+        readonly value: Token;
     };
 
     public readonly kind = AstNodeKind.LiteralExpression;
@@ -796,11 +790,11 @@ export class EscapedCharCodeLiteralExpression extends Expression {
 
     public readonly kind = AstNodeKind.EscapedCharCodeLiteralExpression;
 
-    public tokens: {
-        value: Token & { charCode: number };
+    public readonly tokens: {
+        readonly value: Token & { charCode: number };
     };
 
-    readonly range: Range;
+    public readonly range: Range;
 
     transpile(state: BrsTranspileState) {
         return [
@@ -828,11 +822,11 @@ export class ArrayLiteralExpression extends Expression {
         this.range = util.createBoundingRange(this.tokens.open, ...this.elements, this.tokens.close);
     }
 
-    readonly elements: Array<Expression | CommentStatement>;
+    public readonly elements: Array<Expression | CommentStatement>;
 
-    readonly tokens: {
-        open?: Token;
-        close?: Token;
+    public readonly tokens: {
+        readonly open?: Token;
+        readonly close?: Token;
     };
 
     public readonly kind = AstNodeKind.ArrayLiteralExpression;
@@ -919,17 +913,16 @@ export class AAMemberExpression extends Expression {
 
     public readonly kind = AstNodeKind.AAMemberExpression;
 
-    public range: Range;
+    public readonly range: Range;
 
-    public tokens: {
-        key: Token;
-        colon?: Token;
-        comma?: Token;
+    public readonly tokens: {
+        readonly key: Token;
+        readonly colon?: Token;
+        readonly comma?: Token;
     };
 
     /** The expression evaluated to determine the member's initial value. */
-    public value: Expression;
-
+    public readonly value: Expression;
 
     transpile(state: BrsTranspileState) {
         //TODO move the logic from AALiteralExpression loop into this function
@@ -961,13 +954,12 @@ export class AALiteralExpression extends Expression {
         this.range = util.createBoundingRange(this.tokens.open, ...this.elements, this.tokens.close);
     }
 
-    readonly elements: Array<AAMemberExpression | CommentStatement>;
-    readonly tokens: {
-        open?: Token;
-        close?: Token;
+    public readonly elements: Array<AAMemberExpression | CommentStatement>;
+    public readonly tokens: {
+        readonly open?: Token;
+        readonly close?: Token;
     };
 
-
     public readonly kind = AstNodeKind.AALiteralExpression;
 
     public readonly range: Range;
@@ -1073,10 +1065,10 @@ export class UnaryExpression extends Expression {
 
     public readonly range: Range;
 
-    public tokens: {
-        operator: Token;
+    public readonly tokens: {
+        readonly operator: Token;
     };
-    public right: Expression;
+    public readonly right: Expression;
 
     transpile(state: BrsTranspileState) {
         let separatingWhitespace: string;
@@ -1116,8 +1108,8 @@ export class VariableExpression extends Expression {
         this.range = this.tokens.name?.range;
     }
 
-    readonly tokens: {
-        name: Identifier;
+    public readonly tokens: {
+        readonly name: Identifier;
     };
 
     public readonly kind = AstNodeKind.VariableExpression;
@@ -1182,8 +1174,8 @@ export class SourceLiteralExpression extends Expression {
 
     public readonly kind = AstNodeKind.SourceLiteralExpression;
 
-    readonly tokens: {
-        value: Token;
+    public readonly tokens: {
+        readonly value: Token;
     };
 
     /**
@@ -1285,10 +1277,10 @@ export class NewExpression extends Expression {
 
     public readonly range: Range;
 
-    readonly tokens: {
-        new?: Token;
+    public readonly tokens: {
+        readonly new?: Token;
     };
-    readonly call: CallExpression;
+    public readonly call: CallExpression;
 
     /**
      * The name of the class to initialize (with optional namespace prefixed)
@@ -1358,14 +1350,14 @@ export class CallfuncExpression extends Expression {
         );
     }
 
-    readonly callee: Expression;
-    readonly args: Expression[];
+    public readonly callee: Expression;
+    public readonly args: Expression[];
 
-    readonly tokens: {
-        operator: Token;
-        methodName: Identifier;
-        openingParen?: Token;
-        closingParen?: Token;
+    public readonly tokens: {
+        readonly operator: Token;
+        readonly methodName: Identifier;
+        readonly openingParen?: Token;
+        readonly closingParen?: Token;
     };
 
     public readonly kind = AstNodeKind.CallfuncExpression;
@@ -1459,7 +1451,7 @@ export class TemplateStringQuasiExpression extends Expression {
         );
     }
 
-    readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>;
+    public readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>;
     public readonly kind = AstNodeKind.TemplateStringQuasiExpression;
 
     readonly range: Range;
@@ -1513,12 +1505,12 @@ export class TemplateStringExpression extends Expression {
 
     public readonly kind = AstNodeKind.TemplateStringExpression;
 
-    readonly tokens: {
-        openingBacktick?: Token;
-        closingBacktick?: Token;
+    public readonly tokens: {
+        readonly openingBacktick?: Token;
+        readonly closingBacktick?: Token;
     };
-    readonly quasis: TemplateStringQuasiExpression[];
-    readonly expressions: Expression[];
+    public readonly quasis: TemplateStringQuasiExpression[];
+    public readonly expressions: Expression[];
 
     public readonly range: Range;
 
@@ -1622,14 +1614,14 @@ export class TaggedTemplateStringExpression extends Expression {
 
     public readonly kind = AstNodeKind.TaggedTemplateStringExpression;
 
-    public tokens: {
-        tagName: Identifier;
-        openingBacktick?: Token;
-        closingBacktick?: Token;
+    public readonly tokens: {
+        readonly tagName: Identifier;
+        readonly openingBacktick?: Token;
+        readonly closingBacktick?: Token;
     };
 
-    readonly quasis: TemplateStringQuasiExpression[];
-    readonly expressions: Expression[];
+    public readonly quasis: TemplateStringQuasiExpression[];
+    public readonly expressions: Expression[];
 
     public readonly range: Range;
 
@@ -1707,9 +1699,9 @@ export class AnnotationExpression extends Expression {
 
     public readonly kind = AstNodeKind.AnnotationExpression;
 
-    readonly tokens: {
-        at: Token;
-        name: Token;
+    public readonly tokens: {
+        readonly at: Token;
+        readonly name: Token;
     };
 
     public get range() {
@@ -1720,7 +1712,7 @@ export class AnnotationExpression extends Expression {
         );
     }
 
-    public name: string;
+    public readonly name: string;
 
     public call: CallExpression;
 
@@ -1782,16 +1774,16 @@ export class TernaryExpression extends Expression {
 
     public readonly kind = AstNodeKind.TernaryExpression;
 
-    public range: Range;
+    public readonly range: Range;
 
-    readonly tokens: {
-        questionMark?: Token;
-        colon?: Token;
+    public readonly tokens: {
+        readonly questionMark?: Token;
+        readonly colon?: Token;
     };
 
-    readonly test: Expression;
-    readonly consequent?: Expression;
-    readonly alternate?: Expression;
+    public readonly test: Expression;
+    public readonly consequent?: Expression;
+    public readonly alternate?: Expression;
 
     transpile(state: BrsTranspileState) {
         let result = [];
@@ -1884,12 +1876,12 @@ export class NullCoalescingExpression extends Expression {
 
     public readonly range: Range;
 
-    public tokens: {
-        questionQuestion?: Token;
+    public readonly tokens: {
+        readonly questionQuestion?: Token;
     };
 
-    public consequent: Expression;
-    public alternate: Expression;
+    public readonly consequent: Expression;
+    public readonly alternate: Expression;
 
     transpile(state: BrsTranspileState) {
         let result = [];
@@ -1970,8 +1962,8 @@ export class RegexLiteralExpression extends Expression {
     }
 
     public readonly kind = AstNodeKind.RegexLiteralExpression;
-    public tokens: {
-        regexLiteral: Token;
+    public readonly tokens: {
+        readonly regexLiteral: Token;
     };
 
     public get range() {
@@ -2070,9 +2062,9 @@ export class TypeExpression extends Expression implements TypedefProvider {
     /**
      * The standard AST expression that represents the type for this TypeExpression.
      */
-    public expression: Expression;
+    public readonly expression: Expression;
 
-    public range: Range;
+    public readonly range: Range;
 
     public transpile(state: BrsTranspileState): TranspileResult {
         return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
@@ -2124,15 +2116,15 @@ export class TypeCastExpression extends Expression {
 
     public readonly kind = AstNodeKind.TypeCastExpression;
 
-    public obj: Expression;
+    public readonly obj: Expression;
 
-    public tokens: {
-        as?: Token;
+    public readonly tokens: {
+        readonly as?: Token;
     };
 
     public typeExpression?: TypeExpression;
 
-    public range: Range;
+    public readonly range: Range;
 
     public transpile(state: BrsTranspileState): TranspileResult {
         return this.obj.transpile(state);
@@ -2176,16 +2168,16 @@ export class TypedArrayExpression extends Expression {
         );
     }
 
-    public tokens: {
-        leftBracket?: Token;
-        rightBracket?: Token;
+    public readonly tokens: {
+        readonly leftBracket?: Token;
+        readonly rightBracket?: Token;
     };
 
-    public innerType: Expression;
+    public readonly innerType: Expression;
 
     public readonly kind = AstNodeKind.TypedArrayExpression;
 
-    public range: Range;
+    public readonly range: Range;
 
     public transpile(state: BrsTranspileState): TranspileResult {
         return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index e07ab59e..fdd576ef 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -95,7 +95,7 @@ import { Logger } from '../Logger';
 import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDottedGetExpression, isIfStatement, isIndexedGetExpression } from '../astUtils/reflection';
 import { createStringLiteral } from '../astUtils/creators';
 import type { Expression, Statement } from './AstNode';
-import { SymbolTable } from '../SymbolTable';
+import type { DeepWriteable } from '../interfaces';
 
 export class Parser {
     /**
@@ -336,17 +336,19 @@ export class Parser {
     }
 
     private enumMemberStatement() {
-        const statement = new EnumMemberStatement({} as any);
-        statement.tokens.name = this.consume(
+        const name = this.consume(
             DiagnosticMessages.expectedClassFieldIdentifier(),
             TokenKind.Identifier,
             ...AllowedProperties
         ) as Identifier;
+        let equalsToken: Token;
+        let value: Expression;
         //look for `= SOME_EXPRESSION`
         if (this.check(TokenKind.Equal)) {
-            statement.tokens.equals = this.advance();
-            statement.value = this.expression();
+            equalsToken = this.advance();
+            value = this.expression();
         }
+        const statement = new EnumMemberStatement({ name: name, equals: equalsToken, value: value });
         return statement;
     }
 
@@ -523,19 +525,19 @@ export class Parser {
     }
 
     private enumDeclaration(): EnumStatement {
-        const result = new EnumStatement({ name: {} as any, body: [] });
-        this.warnIfNotBrighterScriptMode('enum declarations');
-
-        const parentAnnotations = this.enterAnnotationBlock();
-
-        result.tokens.enum = this.consume(
+        const enumToken = this.consume(
             DiagnosticMessages.expectedKeyword(TokenKind.Enum),
             TokenKind.Enum
         );
+        const nameToken = this.tryIdentifier(...this.allowedLocalIdentifiers);
 
-        result.tokens.name = this.tryIdentifier(...this.allowedLocalIdentifiers);
+        this.warnIfNotBrighterScriptMode('enum declarations');
+
+        const parentAnnotations = this.enterAnnotationBlock();
 
         this.consumeStatementSeparators();
+
+        const body: Array<EnumMemberStatement | CommentStatement> = [];
         //gather up all members
         while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
             try {
@@ -557,7 +559,7 @@ export class Parser {
 
                 if (decl) {
                     this.consumePendingAnnotations(decl);
-                    result.body.push(decl);
+                    body.push(decl);
                 } else {
                     //we didn't find a declaration...flag tokens until next line
                     this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
@@ -576,7 +578,14 @@ export class Parser {
         }
 
         //consume the final `end interface` token
-        result.tokens.endEnum = this.consumeToken(TokenKind.EndEnum);
+        const endEnumToken = this.consumeToken(TokenKind.EndEnum);
+
+        const result = new EnumStatement({
+            enum: enumToken,
+            name: nameToken,
+            body: body,
+            endEnum: endEnumToken
+        });
 
         this.exitAnnotationBlock(parentAnnotations);
         return result;
@@ -655,9 +664,6 @@ export class Parser {
                         override: overrideKeyword
                     });
 
-                    //refer to this statement as parent of the expression
-                    funcDeclaration.func.functionStatement = decl as MethodStatement;
-
                     //fields
                 } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
 
@@ -877,57 +883,47 @@ export class Parser {
 
             this.consumeStatementSeparators(true);
 
-            let func = new FunctionExpression({
-                parameters: params,
-                body: undefined, //body
-                functionType: functionType,
-                endFunctionType: undefined, //ending keyword
-                leftParen: leftParen,
-                rightParen: rightParen,
-                as: asToken,
-                returnTypeExpression: typeExpression
-            });
 
             //support ending the function with `end sub` OR `end function`
-            func.body = this.block();
+            let body = this.block();
             //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
-            if (!func.body) {
-                func.body = new Block({
-                    statements: [],
-                    startingRange: util.createRangeFromPositions(func.range.start, func.range.start)
-                });
-            }
-            func.body.symbolTable = new SymbolTable(`Block: Function '${name?.text ?? ''}'`, () => func.getSymbolTable());
-
-            if (!func.body) {
-                this.diagnostics.push({
-                    ...DiagnosticMessages.callableBlockMissingEndKeyword(functionTypeText),
-                    range: this.peek().range
-                });
-                throw this.lastDiagnosticAsError();
-            }
 
             // consume 'end sub' or 'end function'
-            func.tokens.endFunctionType = this.advance();
+            const endFunctionType = this.advance();
             let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
 
             //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
             //add an error but don't hard-fail so the AST can continue more gracefully
-            if (func.tokens.endFunctionType.kind !== expectedEndKind) {
+            if (endFunctionType.kind !== expectedEndKind) {
                 this.diagnostics.push({
-                    ...DiagnosticMessages.mismatchedEndCallableKeyword(functionTypeText, func.tokens.endFunctionType.text),
-                    range: func.tokens.endFunctionType.range
+                    ...DiagnosticMessages.mismatchedEndCallableKeyword(functionTypeText, endFunctionType.text),
+                    range: endFunctionType.range
+                });
+            }
+
+            if (!body) {
+                body = new Block({
+                    statements: [],
+                    startingRange: util.createBoundingRange(
+                        functionType, name, leftParen, ...params, rightParen, asToken, typeExpression, endFunctionType)
                 });
             }
-            func.callExpressions = this.callExpressions;
+
+            let func = new FunctionExpression({
+                parameters: params,
+                body: body,
+                functionType: functionType,
+                endFunctionType: endFunctionType,
+                leftParen: leftParen,
+                rightParen: rightParen,
+                as: asToken,
+                returnTypeExpression: typeExpression
+            });
 
             if (isAnonymous) {
                 return func;
             } else {
                 let result = new FunctionStatement({ name: name, func: func });
-                func.symbolTable.name += `: '${name?.text}'`;
-                func.functionStatement = result;
-
                 return result;
             }
         } finally {
@@ -1329,7 +1325,6 @@ export class Parser {
 
         let name = this.identifyingExpression();
         //set the current namespace name
-        let result = new NamespaceStatement({ namespace: keyword, nameExpression: name, body: null });
 
         this.globalTerminators.push([TokenKind.EndNamespace]);
         let body = this.body();
@@ -1348,8 +1343,13 @@ export class Parser {
 
         this.namespaceAndFunctionDepth--;
 
-        result.body = body;
-        result.tokens.endNamespace = endKeyword;
+        let result = new NamespaceStatement({
+            namespace: keyword,
+            nameExpression: name,
+            body: body,
+            endNamespace: endKeyword
+        });
+
         //cache the range property so that plugins can't affect it
         result.cacheRange();
         result.body.symbolTable.name += `: namespace '${result.name}'`;
@@ -1461,7 +1461,7 @@ export class Parser {
         let importStatement = new ImportStatement({
             import: this.advance(),
             //grab the next token only if it's a string
-            filePath: this.tryConsume(
+            path: this.tryConsume(
                 DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
                 TokenKind.StringLiteral
             )
@@ -1637,14 +1637,12 @@ export class Parser {
 
     private tryCatchStatement(): TryCatchStatement {
         const tryToken = this.advance();
-        const statement = new TryCatchStatement(
-            { try: tryToken }
-        );
-
+        let endTryToken: Token;
+        let catchStmt: CatchStatement;
         //ensure statement separator
         this.consumeStatementSeparators();
 
-        statement.tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
+        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
 
         const peek = this.peek();
         if (peek.kind !== TokenKind.Catch) {
@@ -1652,35 +1650,38 @@ export class Parser {
                 ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
                 range: this.peek().range
             });
-            //gracefully handle end-try
-            if (peek.kind === TokenKind.EndTry) {
-                statement.tokens.endTry = this.advance();
+        } else {
+            const catchToken = this.advance();
+            const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
+            if (exceptionVarToken) {
+                // force it into an identifier so the AST makes some sense
+                exceptionVarToken.kind = TokenKind.Identifier;
             }
-            return statement;
-        }
-        const catchStmt = new CatchStatement({ catch: this.advance() });
-        statement.catchStatement = catchStmt;
-
-        const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers);
-        if (exceptionVarToken) {
-            // force it into an identifier so the AST makes some sense
-            exceptionVarToken.kind = TokenKind.Identifier;
-            catchStmt.tokens.exceptionVariable = exceptionVarToken as Identifier;
+            //ensure statement sepatator
+            this.consumeStatementSeparators();
+            const catchBranch = this.block(TokenKind.EndTry);
+            catchStmt = new CatchStatement({
+                catch: catchToken,
+                exceptionVariable: exceptionVarToken,
+                catchBranch: catchBranch
+            });
         }
-
-        //ensure statement sepatator
-        this.consumeStatementSeparators();
-
-        catchStmt.catchBranch = this.block(TokenKind.EndTry);
-
         if (this.peek().kind !== TokenKind.EndTry) {
             this.diagnostics.push({
                 ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
                 range: this.peek().range
             });
         } else {
-            statement.tokens.endTry = this.advance();
+            endTryToken = this.advance();
+        }
+
+        const statement = new TryCatchStatement({
+            try: tryToken,
+            tryBranch: tryBranch,
+            catchStatement: catchStmt,
+            endTry: endTryToken
         }
+        );
         return statement;
     }
 
@@ -2855,7 +2856,7 @@ export class Parser {
                 while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
                     // collect comma at end of expression
                     if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
-                        lastAAMember.tokens.comma = this.previous();
+                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
                     }
 
                     //check for comment at the end of the current line
diff --git a/src/parser/SGParser.ts b/src/parser/SGParser.ts
index 75312c98..21fd1a44 100644
--- a/src/parser/SGParser.ts
+++ b/src/parser/SGParser.ts
@@ -122,7 +122,7 @@ export default class SGParser {
         }
 
         if (isSGComponent(root)) {
-            this.ast = new SGAst(prolog, root, root);
+            this.ast = new SGAst({ prologElement: prolog, rootElement: root, componentElement: root });
         } else {
             if (root) {
                 //error: not a component
@@ -131,7 +131,7 @@ export default class SGParser {
                     range: root.tokens.startTagName.range
                 });
             }
-            this.ast = new SGAst(prolog, root);
+            this.ast = new SGAst({ prologElement: prolog, rootElement: root });
         }
     }
 
@@ -141,15 +141,15 @@ export default class SGParser {
         let prolog: SGProlog;
         if (cstProlog?.[0]) {
             const ctx = cstProlog[0].children;
-            prolog = new SGProlog(
+            prolog = new SGProlog({
                 // <?
-                this.createPartialToken(ctx.XMLDeclOpen[0], 0, 2),
+                startTagOpen: this.createPartialToken(ctx.XMLDeclOpen[0], 0, 2),
                 // xml
-                this.createPartialToken(ctx.XMLDeclOpen[0], 2, 3),
-                this.mapAttributes(ctx.attribute),
+                startTagName: this.createPartialToken(ctx.XMLDeclOpen[0], 2, 3),
+                attributes: this.mapAttributes(ctx.attribute),
                 // ?>
-                this.mapToken(ctx.SPECIAL_CLOSE[0])
-            );
+                startTagClose: this.mapToken(ctx.SPECIAL_CLOSE[0])
+            });
         }
 
         let root: SGElement;
@@ -174,15 +174,24 @@ export default class SGParser {
 
         const content = children.content?.[0];
 
-        let childrenContent: SGElement[];
+        let constructorOptions = {
+            startTagOpen: startTagOpen,
+            startTagName: startTagName,
+            attributes: attributes,
+            startTagClose: startTagClose,
+            elements: [],
+            endTagOpen: endTagOpen,
+            endTagName: endTagName,
+            endTagClose: endTagClose
+        };
 
         switch (startTagName.text) {
             case 'component':
-                childrenContent = this.mapElements(content, ['interface', 'script', 'children', 'customization']);
-                return new SGComponent(startTagOpen, startTagName, attributes, startTagClose, childrenContent, endTagOpen, endTagName, endTagClose);
+                constructorOptions.elements = this.mapElements(content, ['interface', 'script', 'children', 'customization']);
+                return new SGComponent(constructorOptions);
             case 'interface':
-                childrenContent = this.mapElements(content, ['field', 'function']);
-                return new SGInterface(startTagOpen, startTagName, attributes, startTagClose, childrenContent, endTagOpen, endTagName, endTagClose);
+                constructorOptions.elements = this.mapElements(content, ['field', 'function']);
+                return new SGInterface(constructorOptions);
             case 'field':
                 if (this.hasElements(content)) {
                     this.diagnostics.push({
@@ -190,7 +199,7 @@ export default class SGParser {
                         ...DiagnosticMessages.xmlUnexpectedChildren(startTagName.text)
                     });
                 }
-                return new SGInterfaceField(startTagOpen, startTagName, attributes, startTagClose, childrenContent, endTagOpen, endTagName, endTagClose);
+                return new SGInterfaceField(constructorOptions);
             case 'function':
                 if (this.hasElements(content)) {
                     this.diagnostics.push({
@@ -198,7 +207,7 @@ export default class SGParser {
                         ...DiagnosticMessages.xmlUnexpectedChildren(startTagName.text)
                     });
                 }
-                return new SGInterfaceFunction(startTagOpen, startTagName, attributes, startTagClose, childrenContent, endTagOpen, endTagName, endTagClose);
+                return new SGInterfaceFunction(constructorOptions);
             case 'script':
                 if (this.hasElements(content)) {
                     this.diagnostics.push({
@@ -206,35 +215,35 @@ export default class SGParser {
                         ...DiagnosticMessages.xmlUnexpectedChildren(startTagName.text)
                     });
                 }
-                const script = new SGScript(startTagOpen, startTagName, attributes, startTagClose, childrenContent, endTagOpen, endTagName, endTagClose);
+                const script = new SGScript(constructorOptions);
                 script.cdata = this.getCdata(content);
                 return script;
             case 'children':
-                childrenContent = this.mapNodes(content);
-                return new SGChildren(startTagOpen, startTagName, attributes, startTagClose, childrenContent, endTagOpen, endTagName, endTagClose);
+                constructorOptions.elements = this.mapNodes(content);
+                return new SGChildren(constructorOptions);
             default:
-                childrenContent = this.mapNodes(content);
-                return new SGNode(startTagOpen, startTagName, attributes, startTagClose, childrenContent, endTagOpen, endTagName, endTagClose);
+                constructorOptions.elements = this.mapNodes(content);
+                return new SGNode(constructorOptions);
         }
     }
 
     mapNode({ children }: ElementCstNode): SGNode {
-        return new SGNode(
+        return new SGNode({
             //<
-            this.mapToken(children.OPEN[0]),
+            startTagOpen: this.mapToken(children.OPEN[0]),
             // TagName
-            this.mapToken(children.Name[0]),
-            this.mapAttributes(children.attribute),
+            startTagName: this.mapToken(children.Name[0]),
+            attributes: this.mapAttributes(children.attribute),
             // > or />
-            this.mapToken((children.SLASH_CLOSE ?? children.START_CLOSE)[0]),
-            this.mapNodes(children.content?.[0]),
+            startTagClose: this.mapToken((children.SLASH_CLOSE ?? children.START_CLOSE)[0]),
+            elements: this.mapNodes(children.content?.[0]),
             // </
-            this.mapToken(children.SLASH_OPEN?.[0]),
+            endTagOpen: this.mapToken(children.SLASH_OPEN?.[0]),
             // TagName
-            this.mapToken(children.END_NAME?.[0]),
+            endTagName: this.mapToken(children.END_NAME?.[0]),
             // >
-            this.mapToken(children.END?.[0])
-        );
+            endTagClose: this.mapToken(children.END?.[0])
+        });
     }
 
     mapElements(content: ContentCstNode, allow: string[]): SGElement[] {
@@ -305,7 +314,7 @@ export default class SGParser {
             for (let i = 0; i < attributes.length; i++) {
 
                 const children = attributes[i].children;
-                const attr = new SGAttribute(this.mapToken(children.Name[0]));
+                const attr = new SGAttribute({ key: this.mapToken(children.Name[0]) });
 
                 if (children.EQUALS) {
                     attr.tokens.equals = this.mapToken(children.EQUALS[0]);
diff --git a/src/parser/SGTypes.ts b/src/parser/SGTypes.ts
index e12d785a..ac07ef8e 100644
--- a/src/parser/SGTypes.ts
+++ b/src/parser/SGTypes.ts
@@ -2,12 +2,6 @@ import { SourceNode } from 'source-map';
 import type { Range } from 'vscode-languageserver';
 import { createSGAttribute, createSGInterface, createSGInterfaceField, createSGInterfaceFunction } from '../astUtils/creators';
 import type { FileReference } from '../interfaces';
-import { BooleanType } from '../types/BooleanType';
-import { DynamicType } from '../types/DynamicType';
-import { FloatType } from '../types/FloatType';
-import { IntegerType } from '../types/IntegerType';
-import { LongIntegerType } from '../types/LongIntegerType';
-import { StringType } from '../types/StringType';
 import util from '../util';
 import type { TranspileState } from './TranspileState';
 
@@ -17,26 +11,28 @@ export interface SGToken {
 }
 
 export class SGAttribute {
-    public constructor(
-        key: SGToken,
-        equals?: SGToken,
-        openingQuote?: SGToken,
-        value?: SGToken,
-        closingQuote?: SGToken
-    ) {
-        this.tokens.key = key;
-        this.tokens.equals = equals;
-        this.tokens.openingQuote = openingQuote;
-        this.tokens.value = value;
-        this.tokens.closingQuote = closingQuote;
-    }
-
-    public tokens = {} as {
+    public constructor(options: {
         key: SGToken;
         equals?: SGToken;
         openingQuote?: SGToken;
         value?: SGToken;
         closingQuote?: SGToken;
+    }) {
+        this.tokens = {
+            key: options.key,
+            equals: options.equals,
+            openingQuote: options.openingQuote,
+            value: options.value,
+            closingQuote: options.closingQuote
+        };
+    }
+
+    public readonly tokens: {
+        readonly key: SGToken;
+        equals?: SGToken;
+        openingQuote?: SGToken;
+        value?: SGToken;
+        closingQuote?: SGToken;
     };
 
     public get key() {
@@ -93,74 +89,70 @@ export class SGAttribute {
     }
 
     public clone() {
-        return new SGAttribute(
-            { ...this.tokens.key },
-            { ...this.tokens.equals },
-            { ...this.tokens.openingQuote },
-            { ...this.tokens.value },
-            { ...this.tokens.closingQuote }
-        );
+        return new SGAttribute(this.tokens);
     }
 }
 
 export class SGElement {
 
-    constructor(
-        startTagOpen: SGToken,
-        startTagName: SGToken,
-        attributes = [] as SGAttribute[],
-        startTagClose?: SGToken,
-        elements = [] as SGElement[],
-        endTagOpen?: SGToken,
-        endTagName?: SGToken,
-        endTagClose?: SGToken
-    ) {
-        this.tokens.startTagOpen = startTagOpen;
-        this.tokens.startTagName = startTagName;
-        this.attributes = attributes;
-        this.tokens.startTagClose = startTagClose;
-        this.elements = elements;
-        this.tokens.endTagOpen = endTagOpen;
-        this.tokens.endTagName = endTagName;
-        this.tokens.endTagClose = endTagClose;
-    }
-
-    public tokens = {} as {
+    constructor(options: {
+        startTagOpen?: SGToken;
+        startTagName: SGToken;
+        attributes?: SGAttribute[];
+        startTagClose?: SGToken;
+        elements?: SGElement[];
+        endTagOpen?: SGToken;
+        endTagName?: SGToken;
+        endTagClose?: SGToken;
+    }) {
+        this.tokens = {
+            startTagOpen: options.startTagOpen,
+            startTagName: options.startTagName,
+            startTagClose: options.startTagClose,
+            endTagOpen: options.endTagOpen,
+            endTagName: options.endTagName,
+            endTagClose: options.endTagClose
+        };
+        this.attributes = options.attributes ?? [];
+        this.elements = options.elements ?? [];
+    }
+
+    public readonly tokens: {
         /**
          * The first portion of the startTag. (i.e. `<` or `<?`)
          */
-        startTagOpen: SGToken;
+        readonly startTagOpen?: SGToken;
         /**
          * The name of the opening tag (i.e. CoolTag in `<CoolTag>`).
          */
-        startTagName: SGToken;
+        readonly startTagName: SGToken;
         /**
-         * The last bit of the startTag (i.e. `/>` for self-closing, `?>` for xml prologue, or `>` for tag with children)
+         * The last bit of the startTag (i.e. `/>` for self-closing, `?>` for xml prolog, or `>` for tag with children)
          */
-        startTagClose?: SGToken;
+        readonly startTagClose?: SGToken;
         /**
          * The endTag opening char `<`
          */
-        endTagOpen?: SGToken;
+        readonly endTagOpen?: SGToken;
         /**
          * The name of the ending tag (i.e. CoolTag in `</CoolTag>`)
          */
-        endTagName?: SGToken;
+        readonly endTagName?: SGToken;
         /**
          * The endTag closing char `>`
          */
-        endTagClose?: SGToken;
+        readonly endTagClose?: SGToken;
     };
 
     /**
      * Array of attributes found on this tag
      */
-    public attributes = [] as SGAttribute[];
+    public readonly attributes = [] as SGAttribute[];
 
     /**
      * The array of direct children AST elements of this AST node
      */
-    public elements = [] as SGElement[];
+    public readonly elements = [] as SGElement[];
 
     public get range() {
         if (!this._range) {
@@ -488,32 +480,6 @@ export enum SGFieldType {
 }
 export const SGFieldTypes = Object.keys(SGFieldType);
 
-export function getBscTypeFromSGFieldType(sgFieldType: string) {
-    switch (sgFieldType) {
-        case SGFieldType.integer:
-        case SGFieldType.int: {
-            return new IntegerType();
-        }
-        case SGFieldType.longinteger: {
-            return new LongIntegerType();
-        }
-        case SGFieldType.float: {
-            return new FloatType();
-        }
-        case SGFieldType.string:
-        case SGFieldType.str: {
-            return new StringType();
-        }
-        case SGFieldType.boolean:
-        case SGFieldType.bool: {
-            return new BooleanType();
-        }
-        default: {
-            return new DynamicType();
-        }
-    }
-}
-
 export class SGInterfaceFunction extends SGElement {
     get name() {
         return this.getAttributeValue('name');
@@ -861,13 +827,20 @@ export interface SGReferences {
 
 export class SGAst {
 
-    constructor(
-        public prologElement?: SGProlog,
-        public rootElement?: SGElement,
-        public componentElement?: SGComponent
-    ) {
+    constructor(options: {
+        prologElement?: SGProlog;
+        rootElement?: SGElement;
+        componentElement?: SGComponent;
+    } = {}) {
+        this.prologElement = options.prologElement;
+        this.rootElement = options.rootElement;
+        this.componentElement = options.componentElement;
     }
 
+    public readonly prologElement?: SGProlog;
+    public readonly rootElement?: SGElement;
+    public readonly componentElement?: SGComponent;
+
     public transpile(state: TranspileState): SourceNode {
         const chunks = [] as SourceNode[];
         //write XML prolog
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 44877b9f..2956821c 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -39,7 +39,7 @@ export class EmptyStatement extends Statement {
     /**
      * Create a negative range to indicate this is an interpolated location
      */
-    public range: Range;
+    public readonly range: Range;
 
     public readonly kind = AstNodeKind.EmptyStatement;
 
@@ -62,10 +62,10 @@ export class Body extends Statement implements TypedefProvider {
         this.statements = options?.statements ?? [];
     }
 
-    public statements: Statement[] = [];
+    public readonly statements: Statement[] = [];
     public readonly kind = AstNodeKind.Body;
 
-    public symbolTable = new SymbolTable('Body', () => this.parent?.getSymbolTable());
+    public readonly symbolTable = new SymbolTable('Body', () => this.parent?.getSymbolTable());
 
     public get range() {
         //this needs to be a getter because the body has its statements pushed to it after being constructed
@@ -148,15 +148,15 @@ export class AssignmentStatement extends Statement {
         this.range = util.createBoundingRange(util.createBoundingRangeFromTokens(this.tokens), this.value);
     }
 
-    public tokens: {
-        equals?: Token;
-        name: Identifier;
-        as?: Token;
+    public readonly tokens: {
+        readonly equals?: Token;
+        readonly name: Identifier;
+        readonly as?: Token;
     };
 
-    public value: Expression;
+    public readonly value: Expression;
 
-    public typeExpression: TypeExpression;
+    public readonly typeExpression: TypeExpression;
 
     public readonly kind = AstNodeKind.AssignmentStatement;
 
@@ -203,7 +203,7 @@ export class Block extends Statement {
         this.statements = options.statements;
         this.startingRange = options.startingRange;
         this.range = util.createBoundingRange(
-            { range: this.startingRange },
+            this.startingRange,
             ...(this.statements ?? [])
         );
     }
@@ -253,6 +253,7 @@ export class Block extends Statement {
             walkArray(this.statements, visitor, options, this);
         }
     }
+
 }
 
 export class ExpressionStatement extends Statement {
@@ -295,13 +296,13 @@ export class CommentStatement extends Statement implements Expression, TypedefPr
         }
     }
 
-    public tokens: {
-        comments: Token[];
+    public readonly tokens: {
+        readonly comments: Token[];
     };
 
     public readonly kind = AstNodeKind.CommentStatement;
 
-    public range: Range;
+    public readonly range: Range;
 
     get text() {
         return this.tokens.comments.map(x => x.text).join('\n');
@@ -345,8 +346,8 @@ export class ExitForStatement extends Statement {
         this.range = this.tokens.exitFor?.range;
     }
 
-    readonly tokens: {
-        exitFor?: Token;
+    public readonly tokens: {
+        readonly exitFor?: Token;
     };
 
     public readonly kind = AstNodeKind.ExitForStatement;
@@ -376,8 +377,8 @@ export class ExitWhileStatement extends Statement {
         this.range = this.tokens.exitWhile?.range;
     }
 
-    readonly tokens: {
-        exitWhile?: Token;
+    public readonly tokens: {
+        readonly exitWhile?: Token;
     };
 
     public readonly kind = AstNodeKind.ExitWhileStatement;
@@ -405,13 +406,16 @@ export class FunctionStatement extends Statement implements TypedefProvider {
             name: options.name
         };
         this.func = options.func;
+        this.func.symbolTable.name += `: '${this.tokens.name?.text}'`;
+        this.func.functionStatement = this;
+
         this.range = this.func.range;
     }
 
-    public tokens: {
-        name: Identifier;
+    public readonly tokens: {
+        readonly name: Identifier;
     };
-    public func: FunctionExpression;
+    public readonly func: FunctionExpression;
 
     public readonly kind = AstNodeKind.FunctionStatement as AstNodeKind;
 
@@ -508,15 +512,15 @@ export class IfStatement extends Statement {
     }
 
     readonly tokens: {
-        if?: Token;
-        then?: Token;
-        else?: Token;
-        endIf?: Token;
+        readonly if?: Token;
+        readonly then?: Token;
+        readonly else?: Token;
+        readonly endIf?: Token;
     };
-    readonly condition: Expression;
-    readonly thenBranch: Block;
-    readonly elseBranch?: IfStatement | Block;
-    readonly isInline?: boolean;
+    public readonly condition: Expression;
+    public readonly thenBranch: Block;
+    public readonly elseBranch?: IfStatement | Block;
+    public readonly isInline?: boolean;
 
     public readonly kind = AstNodeKind.IfStatement;
 
@@ -627,8 +631,10 @@ export class IncrementStatement extends Statement {
         );
     }
 
-    readonly value: Expression;
-    readonly tokens: { operator: Token };
+    public readonly value: Expression;
+    public readonly tokens: {
+        readonly operator: Token;
+    };
 
     public readonly kind = AstNodeKind.IncrementStatement;
 
@@ -682,10 +688,10 @@ export class PrintStatement extends Statement {
             ...(this.expressions ?? [])
         );
     }
-    readonly tokens: {
-        print: Token;
+    public readonly tokens: {
+        readonly print: Token;
     };
-    readonly expressions: Array<Expression | PrintSeparatorTab | PrintSeparatorSpace>;
+    public readonly expressions: Array<Expression | PrintSeparatorTab | PrintSeparatorSpace>;
     public readonly kind = AstNodeKind.PrintStatement;
 
     public readonly range: Range;
@@ -745,17 +751,17 @@ export class DimStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        dim?: Token;
-        name: Identifier;
-        openingSquare?: Token;
-        closingSquare?: Token;
+    public readonly tokens: {
+        readonly dim?: Token;
+        readonly name: Identifier;
+        readonly openingSquare?: Token;
+        readonly closingSquare?: Token;
     };
-    public dimensions: Expression[];
+    public readonly dimensions: Expression[];
 
     public readonly kind = AstNodeKind.DimStatement;
 
-    public range: Range;
+    public readonly range: Range;
 
     public transpile(state: BrsTranspileState) {
         let result = [
@@ -809,9 +815,9 @@ export class GotoStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        goto?: Token;
-        label: Token;
+    public readonly tokens: {
+        readonly goto?: Token;
+        readonly label: Token;
     };
 
     public readonly kind = AstNodeKind.GotoStatement;
@@ -846,9 +852,9 @@ export class LabelStatement extends Statement {
             this.tokens.colon
         );
     }
-    readonly tokens: {
-        name: Token;
-        colon: Token;
+    public readonly tokens: {
+        readonly name: Token;
+        readonly colon: Token;
     };
     public readonly kind = AstNodeKind.LabelStatement;
 
@@ -887,10 +893,10 @@ export class ReturnStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        return: Token;
+    public readonly tokens: {
+        readonly return?: Token;
     };
-    readonly value?: Expression;
+    public readonly value?: Expression;
     public readonly kind = AstNodeKind.ReturnStatement;
 
     public readonly range: Range;
@@ -924,8 +930,8 @@ export class EndStatement extends Statement {
         };
         this.range = this.tokens.end?.range;
     }
-    readonly tokens: {
-        end?: Token;
+    public readonly tokens: {
+        readonly end?: Token;
     };
     public readonly kind = AstNodeKind.EndStatement;
 
@@ -950,8 +956,8 @@ export class StopStatement extends Statement {
         this.tokens = { stop: options?.stop };
         this.range = this.tokens?.stop?.range;
     }
-    readonly tokens: {
-        stop?: Token;
+    public readonly tokens: {
+        readonly stop?: Token;
     };
 
     public readonly kind = AstNodeKind.StopStatement;
@@ -1004,17 +1010,17 @@ export class ForStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        for?: Token;
-        to?: Token;
-        endFor?: Token;
-        step?: Token;
+    public readonly tokens: {
+        readonly for?: Token;
+        readonly to?: Token;
+        readonly endFor?: Token;
+        readonly step?: Token;
     };
 
-    public counterDeclaration: AssignmentStatement;
-    public finalValue: Expression;
-    public body: Block;
-    public increment?: Expression;
+    public readonly counterDeclaration: AssignmentStatement;
+    public readonly finalValue: Expression;
+    public readonly body: Block;
+    public readonly increment?: Expression;
 
     public readonly kind = AstNodeKind.ForStatement;
 
@@ -1108,14 +1114,14 @@ export class ForEachStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        forEach?: Token;
-        item: Token;
-        in?: Token;
-        endFor?: Token;
+    public readonly tokens: {
+        readonly forEach?: Token;
+        readonly item: Token;
+        readonly in?: Token;
+        readonly endFor?: Token;
     };
-    readonly body: Block;
-    readonly target: Expression;
+    public readonly body: Block;
+    public readonly target: Expression;
 
     public readonly kind = AstNodeKind.ForEachStatement;
 
@@ -1188,12 +1194,12 @@ export class WhileStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        while?: Token;
-        endWhile?: Token;
+    public readonly tokens: {
+        readonly while?: Token;
+        readonly endWhile?: Token;
     };
-    readonly condition: Expression;
-    readonly body: Block;
+    public readonly condition: Expression;
+    public readonly body: Block;
 
     public readonly kind = AstNodeKind.WhileStatement;
 
@@ -1258,13 +1264,13 @@ export class DottedSetStatement extends Statement {
             this.value
         );
     }
-    readonly tokens: {
-        name: Identifier;
-        dot?: Token;
+    public readonly tokens: {
+        readonly name: Identifier;
+        readonly dot?: Token;
     };
 
-    readonly obj: Expression;
-    readonly value: Expression;
+    public readonly obj: Expression;
+    public readonly value: Expression;
 
     public readonly kind = AstNodeKind.DottedSetStatement;
 
@@ -1333,13 +1339,13 @@ export class IndexedSetStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        openingSquare?: Token;
-        closingSquare?: Token;
+    public readonly tokens: {
+        readonly openingSquare?: Token;
+        readonly closingSquare?: Token;
     };
-    readonly obj: Expression;
-    readonly indexes: Expression[];
-    readonly value: Expression;
+    public readonly obj: Expression;
+    public readonly indexes: Expression[];
+    public readonly value: Expression;
 
     public readonly kind = AstNodeKind.IndexedSetStatement;
 
@@ -1400,9 +1406,9 @@ export class LibraryStatement extends Statement implements TypedefProvider {
             this.tokens.filePath
         );
     }
-    readonly tokens: {
-        library: Token;
-        filePath?: Token;
+    public readonly tokens: {
+        readonly library: Token;
+        readonly filePath?: Token;
     };
 
     public readonly kind = AstNodeKind.LibraryStatement;
@@ -1451,13 +1457,13 @@ export class NamespaceStatement extends Statement implements TypedefProvider {
         this.symbolTable = new SymbolTable(`NamespaceStatement: '${this.name}'`, () => this.parent?.getSymbolTable());
     }
 
-    readonly tokens: {
-        namespace?: Token;
-        endNamespace?: Token;
+    public readonly tokens: {
+        readonly namespace?: Token;
+        readonly endNamespace?: Token;
     };
 
-    readonly nameExpression: VariableExpression | DottedGetExpression;
-    public body: Body;
+    public readonly nameExpression: VariableExpression | DottedGetExpression;
+    public readonly body: Body;
 
     public readonly kind = AstNodeKind.NamespaceStatement;
 
@@ -1549,40 +1555,40 @@ export class NamespaceStatement extends Statement implements TypedefProvider {
 export class ImportStatement extends Statement implements TypedefProvider {
     constructor(options: {
         import?: Token;
-        filePath: Token;
+        path: Token;
     }) {
         super();
         this.tokens = {
             import: options.import,
-            filePath: options.filePath
+            path: options.path
         };
         this.range = util.createBoundingRange(
             this.tokens.import,
-            this.tokens.filePath
+            this.tokens.path
         );
-        if (this.tokens.filePath) {
+        if (this.tokens.path) {
             //remove quotes
-            this.filePath = this.tokens.filePath.text.replace(/"/g, '');
+            this.filePath = this.tokens.path.text.replace(/"/g, '');
             //adjust the range to exclude the quotes
-            this.tokens.filePath.range = util.createRange(
-                this.tokens.filePath.range.start.line,
-                this.tokens.filePath.range.start.character + 1,
-                this.tokens.filePath.range.end.line,
-                this.tokens.filePath.range.end.character - 1
+            this.tokens.path.range = util.createRange(
+                this.tokens.path.range.start.line,
+                this.tokens.path.range.start.character + 1,
+                this.tokens.path.range.end.line,
+                this.tokens.path.range.end.character - 1
             );
         }
     }
 
-    readonly tokens: {
-        import?: Token;
-        filePath: Token;
+    public readonly tokens: {
+        readonly import?: Token;
+        readonly path: Token;
     };
 
     public readonly kind = AstNodeKind.ImportStatement;
 
-    public range: Range;
+    public readonly range: Range;
 
-    public filePath: string;
+    public readonly filePath: string;
 
     transpile(state: BrsTranspileState) {
         //The xml files are responsible for adding the additional script imports, but
@@ -1591,7 +1597,7 @@ export class ImportStatement extends Statement implements TypedefProvider {
             `'`,
             state.transpileToken(this.tokens.import, 'import'),
             ' ',
-            state.transpileToken(this.tokens.filePath)
+            state.transpileToken(this.tokens.path)
         ];
     }
 
@@ -1603,7 +1609,7 @@ export class ImportStatement extends Statement implements TypedefProvider {
             this.tokens.import?.text ?? 'import',
             ' ',
             //replace any `.bs` extension with `.brs`
-            this.tokens.filePath.text.replace(/\.bs"?$/i, '.brs"')
+            this.tokens.path.text.replace(/\.bs"?$/i, '.brs"')
         ];
     }
 
@@ -1639,19 +1645,19 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
             this.tokens.endInterface
         );
     }
-    public parentInterfaceName?: TypeExpression;
-    public body: Statement[];
+    public readonly parentInterfaceName?: TypeExpression;
+    public readonly body: Statement[];
 
     public readonly kind = AstNodeKind.InterfaceStatement;
 
-    public tokens = {} as {
-        interface?: Token;
-        name: Identifier;
-        extends?: Token;
-        endInterface?: Token;
+    public readonly tokens = {} as {
+        readonly interface?: Token;
+        readonly name: Identifier;
+        readonly extends?: Token;
+        readonly endInterface?: Token;
     };
 
-    public range: Range;
+    public readonly range: Range;
 
     public get fields(): InterfaceFieldStatement[] {
         return this.body.filter(x => isInterfaceFieldStatement(x)) as InterfaceFieldStatement[];
@@ -1830,14 +1836,14 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide
 
     public readonly kind = AstNodeKind.InterfaceFieldStatement;
 
-    public typeExpression?: TypeExpression;
+    public readonly typeExpression?: TypeExpression;
 
-    public range: Range;
+    public readonly range: Range;
 
-    public tokens: {
-        name: Identifier;
-        as: Token;
-        optional?: Token;
+    public readonly tokens: {
+        readonly name: Identifier;
+        readonly as: Token;
+        readonly optional?: Token;
     };
 
     public getLeadingTrivia(): Token[] {
@@ -1942,17 +1948,17 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
         return this.tokens.name.text;
     }
 
-    public tokens: {
-        optional?: Token;
-        functionType: Token;
-        name: Identifier;
-        leftParen?: Token;
-        rightParen?: Token;
-        as?: Token;
+    public readonly tokens: {
+        readonly optional?: Token;
+        readonly functionType: Token;
+        readonly name: Identifier;
+        readonly leftParen?: Token;
+        readonly rightParen?: Token;
+        readonly as?: Token;
     };
 
-    public params: FunctionParameterExpression[];
-    public returnTypeExpression?: TypeExpression;
+    public readonly params: FunctionParameterExpression[];
+    public readonly returnTypeExpression?: TypeExpression;
 
     public get isOptional() {
         return !!this.tokens.optional;
@@ -2083,17 +2089,17 @@ export class ClassStatement extends Statement implements TypedefProvider {
     public readonly kind = AstNodeKind.ClassStatement;
 
 
-    public tokens: {
-        class?: Token;
+    public readonly tokens: {
+        readonly class?: Token;
         /**
          * The name of the class (without namespace prefix)
          */
-        name: Identifier;
-        endClass?: Token;
-        extends?: Token;
+        readonly name: Identifier;
+        readonly endClass?: Token;
+        readonly extends?: Token;
     };
-    readonly body: Statement[];
-    readonly parentClassName: TypeExpression;
+    public readonly body: Statement[];
+    public readonly parentClassName: TypeExpression;
 
 
     public getName(parseMode: ParseMode) {
@@ -2117,9 +2123,9 @@ export class ClassStatement extends Statement implements TypedefProvider {
         return util.concatAnnotationLeadingTrivia(this, this.tokens.class?.leadingTrivia);
     }
 
-    public memberMap = {} as Record<string, MemberStatement>;
-    public methods = [] as MethodStatement[];
-    public fields = [] as FieldStatement[];
+    public readonly memberMap = {} as Record<string, MemberStatement>;
+    public readonly methods = [] as MethodStatement[];
+    public readonly fields = [] as FieldStatement[];
 
     public readonly range: Range;
 
@@ -2520,11 +2526,11 @@ export class MethodStatement extends FunctionStatement {
 
     public readonly kind = AstNodeKind.MethodStatement as AstNodeKind;
 
-    public modifiers: Token[] = [];
+    public readonly modifiers: Token[] = [];
 
-    readonly tokens: {
-        name: Identifier;
-        override?: Token;
+    public readonly tokens: {
+        readonly name: Identifier;
+        readonly override?: Token;
     };
 
     public get accessModifier() {
@@ -2724,16 +2730,16 @@ export class FieldStatement extends Statement implements TypedefProvider {
         );
     }
 
-    readonly tokens: {
-        accessModifier?: Token;
-        name: Identifier;
-        as?: Token;
-        equals?: Token;
-        optional?: Token;
+    public readonly tokens: {
+        readonly accessModifier?: Token;
+        readonly name: Identifier;
+        readonly as?: Token;
+        readonly equals?: Token;
+        readonly optional?: Token;
     };
 
-    public typeExpression?: TypeExpression;
-    public initialValue?: Expression;
+    public readonly typeExpression?: TypeExpression;
+    public readonly initialValue?: Expression;
 
     public readonly kind = AstNodeKind.FieldStatement;
 
@@ -2823,13 +2829,13 @@ export class TryCatchStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        try?: Token;
-        endTry?: Token;
+    public readonly tokens: {
+        readonly try?: Token;
+        readonly endTry?: Token;
     };
 
-    public tryBranch: Block;
-    public catchStatement: CatchStatement;
+    public readonly tryBranch: Block;
+    public readonly catchStatement: CatchStatement;
 
     public readonly kind = AstNodeKind.TryCatchStatement;
 
@@ -2875,16 +2881,16 @@ export class CatchStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        catch?: Token;
-        exceptionVariable?: Identifier;
+    public readonly tokens: {
+        readonly catch?: Token;
+        readonly exceptionVariable?: Identifier;
     };
 
-    public catchBranch?: Block;
+    public readonly catchBranch?: Block;
 
     public readonly kind = AstNodeKind.CatchStatement;
 
-    public range: Range;
+    public readonly range: Range;
 
     public transpile(state: BrsTranspileState): TranspileResult {
         return [
@@ -2918,14 +2924,14 @@ export class ThrowStatement extends Statement {
         );
     }
 
-    readonly tokens: {
-        throw?: Token;
+    public readonly tokens: {
+        readonly throw?: Token;
     };
-    public expression?: Expression;
+    public readonly expression?: Expression;
 
     public readonly kind = AstNodeKind.ThrowStatement;
 
-    public range: Range;
+    public readonly range: Range;
 
     public transpile(state: BrsTranspileState) {
         const result = [
@@ -2967,20 +2973,19 @@ export class EnumStatement extends Statement implements TypedefProvider {
             name: options.name,
             endEnum: options.endEnum
         };
-        this.body = this.body ?? [];
+        this.symbolTable = new SymbolTable('Enum');
+        this.body = options.body ?? [];
     }
 
-    public tokens: {
-        enum?: Token;
-        name: Identifier;
-        endEnum?: Token;
+    public readonly tokens: {
+        readonly enum?: Token;
+        readonly name: Identifier;
+        readonly endEnum?: Token;
     };
-    public body: Array<EnumMemberStatement | CommentStatement>;
+    public readonly body: Array<EnumMemberStatement | CommentStatement>;
 
     public readonly kind = AstNodeKind.EnumStatement;
 
-    public symbolTable = new SymbolTable('Enum');
-
     public get range(): Range {
         return util.createBoundingRange(
             this.tokens.enum,
@@ -3145,11 +3150,11 @@ export class EnumMemberStatement extends Statement implements TypedefProvider {
         this.value = options.value;
     }
 
-    public tokens: {
-        name: Identifier;
-        equals?: Token;
+    public readonly tokens: {
+        readonly name: Identifier;
+        readonly equals?: Token;
     };
-    public value?: Expression;
+    public readonly value?: Expression;
 
     public readonly kind = AstNodeKind.EnumMemberStatement;
 
@@ -3223,16 +3228,16 @@ export class ConstStatement extends Statement implements TypedefProvider {
         this.range = util.createBoundingRange(this.tokens.const, this.tokens.name, this.tokens.equals, this.value);
     }
 
-    public tokens: {
-        const: Token;
-        name: Identifier;
-        equals: Token;
+    public readonly tokens: {
+        readonly const: Token;
+        readonly name: Identifier;
+        readonly equals: Token;
     };
-    public value: Expression;
+    public readonly value: Expression;
 
     public readonly kind = AstNodeKind.ConstStatement;
 
-    public range: Range;
+    public readonly range: Range;
 
     public get name() {
         return this.tokens.name.text;
@@ -3306,14 +3311,14 @@ export class ContinueStatement extends Statement {
         );
     }
 
-    public tokens: {
-        continue?: Token;
-        loopType: Token;
+    public readonly tokens: {
+        readonly continue?: Token;
+        readonly loopType: Token;
     };
 
     public readonly kind = AstNodeKind.ContinueStatement;
 
-    public range: Range;
+    public readonly range: Range;
 
     transpile(state: BrsTranspileState) {
         return [
diff --git a/src/validators/ClassValidator.ts b/src/validators/ClassValidator.ts
index dd8b8304..2f5a02fd 100644
--- a/src/validators/ClassValidator.ts
+++ b/src/validators/ClassValidator.ts
@@ -4,7 +4,6 @@ import type { CallExpression } from '../parser/Expression';
 import { ParseMode } from '../parser/Parser';
 import type { ClassStatement, MethodStatement, NamespaceStatement } from '../parser/Statement';
 import { CancellationTokenSource } from 'vscode-languageserver';
-import util from '../util';
 import { isCallExpression, isFieldStatement, isMethodStatement, isNamespaceStatement } from '../astUtils/reflection';
 import type { BsDiagnostic } from '../interfaces';
 import { createVisitor, WalkMode } from '../astUtils/visitors';
@@ -34,61 +33,10 @@ export class BsClassValidator {
         this.detectCircularReferences();
         this.validateMemberCollisions();
         this.verifyChildConstructor();
-        this.verifyNewExpressions();
 
         this.cleanUp();
     }
 
-    /**
-     * Given a class name optionally prefixed with a namespace name, find the class that matches
-     */
-    private getClassByName(className: string, namespaceName?: string) {
-        let fullName = util.getFullyQualifiedClassName(className, namespaceName);
-        let cls = this.classes.get(fullName.toLowerCase());
-        //if we couldn't find the class by its full namespaced name, look for a global class with that name
-        if (!cls) {
-            cls = this.classes.get(className.toLowerCase());
-        }
-        return cls;
-    }
-
-
-    /**
-     * Find all "new" statements in the program,
-     * and make sure we can find a class with that name
-     */
-    private verifyNewExpressions() {
-        this.scope.enumerateBrsFiles((file) => {
-            // eslint-disable-next-line @typescript-eslint/dot-notation
-            let newExpressions = file['_cachedLookups'].newExpressions;
-            for (let newExpression of newExpressions) {
-                let className = newExpression.className.getName(ParseMode.BrighterScript);
-                const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
-                let newableClass = this.getClassByName(
-                    className,
-                    namespaceName
-                );
-
-                if (!newableClass) {
-                    //try and find functions with this name.
-                    let fullName = util.getFullyQualifiedClassName(className, namespaceName);
-                    let callable = this.scope.getCallableByName(fullName);
-                    //if we found a callable with this name, the user used a "new" keyword in front of a function. add error
-                    if (callable) {
-                        this.diagnostics.push({
-                            ...DiagnosticMessages.expressionIsNotConstructable(callable.isSub ? 'sub' : 'function'),
-                            file: file,
-                            range: newExpression.className.range
-                        });
-
-                    } else {
-                        //could not find a class with this name (handled by ScopeValidator)
-                    }
-                }
-            }
-        });
-    }
-
     private verifyChildConstructor() {
         for (const [, classStatement] of this.classes) {
             const newMethod = classStatement.memberMap.new as MethodStatement;


Test patch:
diff --git a/src/parser/tests/statement/TryCatch.spec.ts b/src/parser/tests/statement/TryCatch.spec.ts
index 6ccb08c6..c74d3866 100644
--- a/src/parser/tests/statement/TryCatch.spec.ts
+++ b/src/parser/tests/statement/TryCatch.spec.ts
@@ -3,6 +3,8 @@ import { Parser } from '../../Parser';
 import { TryCatchStatement } from '../../Statement';
 import { isFunctionExpression } from '../../../astUtils/reflection';
 import type { FunctionExpression } from '../../Expression';
+import { expectDiagnosticsIncludes } from '../../../testHelpers.spec';
+import { DiagnosticMessages } from '../../../DiagnosticMessages';
 
 describe('parser try/catch', () => {
     it('can parse try catch statements', () => {
@@ -142,7 +144,9 @@ describe('parser try/catch', () => {
                 end try
             end sub
         `);
-        expect(parser.diagnostics).to.be.lengthOf(1);
+        expectDiagnosticsIncludes(parser, [
+            DiagnosticMessages.expectedCatchBlockInTryCatch().message
+        ]);
     });
 
     it('recovers from missing catch and end-try when reaching function boundary', () => {
@@ -153,6 +157,9 @@ describe('parser try/catch', () => {
                     print "error"
             end sub
         `);
-        expect(parser.diagnostics).to.be.lengthOf(1);
+        expectDiagnosticsIncludes(parser, [
+            DiagnosticMessages.expectedCatchBlockInTryCatch().message,
+            DiagnosticMessages.expectedEndTryToTerminateTryCatch().message
+        ]);
     });
 });