Repository: rokucommunity/brighterscript
Pull request #656 (2022-08-02 14:45:46)

Scope validation performance boost
Several fixes:
- leverage child-parent linking introduced in #650 to speed up scope validation
- optimize scope validation by eliminating duplicate file checks when possible

While this hasn't restored the original performance lost in 0.53.0, it's a step in the right direction.
![image](https://user-images.githubusercontent.com/2544493/180507485-b69deaa4-bc70-4093-99c6-f34fe8bf0cbb.png)


Breaking changes:
- removed `NamespaceStatement.symbolTable` in favor of `NamespaceStatement.getSymbolTable()` because the symbol table for namespaces actually reside within the `NamespaceStatement.body` property. Since the symbolTable logic was only recently added, it's unlikely that many projects are referencing it. 
- removed the `namespaceName` from several AstNodes, in favor of external consumers needing to call `.findAncestor(isNamespaceStatement)` instead. Note, this relies heavily on the `AstNode.parent` property which is set at the start of `onFileValidate` for each file. This is breaking because previously, the .namespaceName property was available immediately in `afterFileParse`. However, that logic was unsustainable for plugins that manipulate AST, and it's unlikely that many plugins are leveraging that property, so it is worth the breaking change. 

  Here are the nodes with the removed property:
  -  `VariableExpression`
  - `DottedGetExpression`
  - `CallExpression`
  - `FunctionStatement`


TwitchBronBron: Some of the fixes have further improved performance. Still not as good as 0.52.3, but better than when this PR was originally submitted.
![image](https://user-images.githubusercontent.com/2544493/190724389-86c14d9d-b8bf-4408-8d1e-c6932d0f43ca.png)


Patch:
diff --git a/src/LanguageServer.spec.ts b/src/LanguageServer.spec.ts
index 84ab1aa8..fcca4f11 100644
--- a/src/LanguageServer.spec.ts
+++ b/src/LanguageServer.spec.ts
@@ -851,6 +851,7 @@ describe('LanguageServer', () => {
                     end function
                 end namespace
             `, 'bs');
+            program.validate();
 
             // We run the check twice as the first time is with it not cached and second time is with it cached
             for (let i = 0; i < 2; i++) {
@@ -965,6 +966,7 @@ describe('LanguageServer', () => {
                     end class
                 end namespace
             `, 'bs');
+            program.validate();
 
             // We run the check twice as the first time is with it not cached and second time is with it cached
             for (let i = 0; i < 2; i++) {
diff --git a/src/Program.spec.ts b/src/Program.spec.ts
index 3c48956a..7832f150 100644
--- a/src/Program.spec.ts
+++ b/src/Program.spec.ts
@@ -2332,7 +2332,8 @@ describe('Program', () => {
                     <interface>
                         <function name="sayHello"/>
                     </interface>
-                </component>`);
+                </component>
+            `);
             program.validate();
 
             let signatureHelp = (program.getSignatureHelp(`${rootDir}/source/main.bs`, Position.create(3, 36)));
@@ -2352,6 +2353,7 @@ describe('Program', () => {
                     end function
                 end class
             `);
+            program.validate();
             let signatureHelp = (program.getSignatureHelp(`${rootDir}/source/main.bs`, Position.create(2, 34)));
             expectZeroDiagnostics(program);
             expect(signatureHelp[0].signature.label).to.equal('Person(arg1, arg2)');
@@ -2370,6 +2372,7 @@ describe('Program', () => {
                 class Roger extends Person
                 end class
             `);
+            program.validate();
             let signatureHelp = (program.getSignatureHelp(`${rootDir}/source/main.bs`, Position.create(2, 34)));
             expectZeroDiagnostics(program);
             expect(signatureHelp[0].signature.label).to.equal('Roger(arg1, arg2)');
@@ -2386,6 +2389,7 @@ describe('Program', () => {
                     end function
                 end class
             `);
+            program.validate();
             let signatureHelp = (program.getSignatureHelp(`${rootDir}/source/main.bs`, Position.create(2, 34)));
             expectZeroDiagnostics(program);
             expect(signatureHelp[0].index).to.equal(0);
@@ -2398,7 +2402,7 @@ describe('Program', () => {
         it('gets signature help for namespaced constructor with args', () => {
             program.setFile('source/main.bs', `
                 function main()
-                    p = new people.coders.Person(arg1, arg2)
+                    p = new people.coders.Person(1, 2)
                 end function
                 namespace people.coders
                     class Person
@@ -2406,7 +2410,8 @@ describe('Program', () => {
                         end function
                     end class
                 end namespace
-                    `);
+            `);
+            program.validate();
             let signatureHelp = (program.getSignatureHelp(`${rootDir}/source/main.bs`, Position.create(2, 47)));
             expectZeroDiagnostics(program);
             expect(signatureHelp[0].signature.label).to.equal('people.coders.Person(arg1, arg2)');
@@ -2486,6 +2491,7 @@ describe('Program', () => {
                 function test(arg1, arg2, arg3)
                 end function
             `);
+            program.validate();
             for (let col = 21; col < 27; col++) {
                 let signatureHelp = (program.getSignatureHelp(`${rootDir}/source/main.bs`, Position.create(2, col)));
                 expect(signatureHelp, `failed on col ${col}`).to.have.lengthOf(1);
diff --git a/src/Program.ts b/src/Program.ts
index 887e43dd..fe356742 100644
--- a/src/Program.ts
+++ b/src/Program.ts
@@ -19,8 +19,8 @@ import { globalFile } from './globalCallables';
 import { parseManifest } from './preprocessor/Manifest';
 import { URI } from 'vscode-uri';
 import PluginInterface from './PluginInterface';
-import { isBrsFile, isXmlFile, isMethodStatement, isXmlScope } from './astUtils/reflection';
-import type { FunctionStatement } from './parser/Statement';
+import { isBrsFile, isXmlFile, isMethodStatement, isXmlScope, isNamespaceStatement } from './astUtils/reflection';
+import type { FunctionStatement, NamespaceStatement } from './parser/Statement';
 import { ParseMode } from './parser/Parser';
 import { TokenKind } from './lexer/TokenKind';
 import { BscPlugin } from './bscPlugin/BscPlugin';
@@ -825,7 +825,7 @@ export class Program {
                 filesSearched.add(file);
 
                 for (const statement of [...file.parser.references.functionStatements, ...file.parser.references.classStatements.flatMap((cs) => cs.methods)]) {
-                    let parentNamespaceName = statement.namespaceName?.getName(originFile.parseMode)?.toLowerCase();
+                    let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
                     if (statement.name.text.toLowerCase() === lowerName && (!parentNamespaceName || parentNamespaceName === lowerNamespaceName)) {
                         if (!results.has(statement)) {
                             results.set(statement, { item: statement, file: file });
diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts
index 36cd16aa..4923b6a2 100644
--- a/src/Scope.spec.ts
+++ b/src/Scope.spec.ts
@@ -65,8 +65,8 @@ describe('Scope', () => {
         program.validate();
         const scope = program.getScopesForFile('source/alpha.bs')[0];
         scope.linkSymbolTable();
-        const symbolTable = file.parser.references.namespaceStatements[1].symbolTable;
-        //the symbol table should contain the relative names for all items in this namespace across files
+        const symbolTable = file.parser.references.namespaceStatements[1].getSymbolTable();
+        //the symbol table should contain the relative names for all items in this namespace across the entire scope
         expect(
             symbolTable.hasSymbol('Beta')
         ).to.be.true;
@@ -277,7 +277,7 @@ describe('Scope', () => {
             ]);
         });
 
-        it('accepts namespace names in their transpiled form on .brs files', () => {
+        it('accepts namespace names in their transpiled form in .brs files', () => {
             program.setFile('source/ns.bs', `
                 namespace MyNamespace
                     sub foo()
@@ -1386,6 +1386,7 @@ describe('Scope', () => {
                 end namespace
             `);
             delete ((file.ast.statements[0] as NamespaceStatement).body.statements[0] as FunctionStatement).name;
+            program.validate();
             program['scopes']['source'].buildNamespaceLookup();
         });
     });
@@ -1418,7 +1419,7 @@ describe('Scope', () => {
                     bar3_2
                 end enum
             `);
-            // program.validate();
+            program.validate();
 
             expect(
                 [...sourceScope.getEnumMap().keys()]
diff --git a/src/Scope.ts b/src/Scope.ts
index b24f1544..2dd9074d 100644
--- a/src/Scope.ts
+++ b/src/Scope.ts
@@ -17,7 +17,7 @@ import { URI } from 'vscode-uri';
 import { LogLevel } from './Logger';
 import type { BrsFile } from './files/BrsFile';
 import type { DependencyGraph, DependencyChangedEvent } from './DependencyGraph';
-import { isBrsFile, isMethodStatement, isClassStatement, isConstStatement, isCustomType, isEnumStatement, isFunctionStatement, isFunctionType, isXmlFile } from './astUtils/reflection';
+import { isBrsFile, isMethodStatement, isClassStatement, isConstStatement, isCustomType, isEnumStatement, isFunctionStatement, isFunctionType, isXmlFile, isNamespaceStatement } from './astUtils/reflection';
 import { SymbolTable } from './SymbolTable';
 import type { Statement } from './parser/AstNode';
 
@@ -519,7 +519,7 @@ export class Scope {
         this.enumerateBrsFiles((file) => {
             for (let namespaceStatement of file.parser.references.namespaceStatements) {
                 //TODO should we handle non-brighterscript?
-                let name = namespaceStatement.nameExpression.getName(ParseMode.BrighterScript);
+                let name = namespaceStatement.getName(ParseMode.BrighterScript);
                 let nameParts = name.split('.');
 
                 let loopName = null;
@@ -559,7 +559,7 @@ export class Scope {
                 }
                 // Merges all the symbol tables of the namespace statements into the new symbol table created above.
                 // Set those symbol tables to have this new merged table as a parent
-                ns.symbolTable.mergeSymbolTable(namespaceStatement.symbolTable);
+                ns.symbolTable.mergeSymbolTable(namespaceStatement.getSymbolTable());
             }
 
             //associate child namespaces with their parents
@@ -709,9 +709,9 @@ export class Scope {
 
                 //link each NamespaceStatement's SymbolTable with the aggregate NamespaceLookup SymbolTable
                 for (const namespace of file.parser.references.namespaceStatements) {
-                    const namespaceNameLower = namespace.nameExpression.getName(ParseMode.BrighterScript).toLowerCase();
+                    const namespaceNameLower = namespace.getName(ParseMode.BrighterScript).toLowerCase();
                     const namespaceSymbolTable = this.namespaceLookup.get(namespaceNameLower).symbolTable;
-                    namespace.symbolTable.pushParent(namespaceSymbolTable);
+                    namespace.getSymbolTable().pushParent(namespaceSymbolTable);
                 }
             }
         }
@@ -723,7 +723,7 @@ export class Scope {
                 file.parser?.symbolTable.popParent();
 
                 for (const namespace of file.parser.references.namespaceStatements) {
-                    namespace.symbolTable.popParent();
+                    namespace.getSymbolTable().popParent();
                 }
             }
         }
@@ -812,7 +812,7 @@ export class Scope {
             if (isCustomType(func.returnType) && func.returnTypeToken) {
                 // check if this custom type is in our class map
                 const returnTypeName = func.returnType.name;
-                const currentNamespaceName = func.namespaceName?.getName(ParseMode.BrighterScript);
+                const currentNamespaceName = func.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
                 if (!this.hasClass(returnTypeName, currentNamespaceName) && !this.hasInterface(returnTypeName) && !this.hasEnum(returnTypeName)) {
                     this.diagnostics.push({
                         ...DiagnosticMessages.invalidFunctionReturnType(returnTypeName),
@@ -825,7 +825,7 @@ export class Scope {
             for (let param of func.parameters) {
                 if (isCustomType(param.type) && param.typeToken) {
                     const paramTypeName = param.type.name;
-                    const currentNamespaceName = func.namespaceName?.getName(ParseMode.BrighterScript);
+                    const currentNamespaceName = func.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
                     if (!this.hasClass(paramTypeName, currentNamespaceName) && !this.hasInterface(paramTypeName) && !this.hasEnum(paramTypeName)) {
                         this.diagnostics.push({
                             ...DiagnosticMessages.functionParameterTypeIsInvalid(param.name.text, paramTypeName),
diff --git a/src/astUtils/creators.ts b/src/astUtils/creators.ts
index e9fcb8bf..92ae752a 100644
--- a/src/astUtils/creators.ts
+++ b/src/astUtils/creators.ts
@@ -2,7 +2,6 @@ import type { Range } from 'vscode-languageserver';
 import type { Identifier, Token } from '../lexer/Token';
 import { TokenKind } from '../lexer/TokenKind';
 import type { Expression } from '../parser/AstNode';
-import type { NamespacedVariableNameExpression } from '../parser/Expression';
 import { LiteralExpression, CallExpression, DottedGetExpression, VariableExpression, FunctionExpression } from '../parser/Expression';
 import type { SGAttribute } from '../parser/SGTypes';
 import { Block, MethodStatement } from '../parser/Statement';
@@ -104,13 +103,13 @@ export function createIdentifier(name: string, range?: Range): Identifier {
     };
 }
 
-export function createVariableExpression(ident: string, range?: Range, namespaceName?: NamespacedVariableNameExpression): VariableExpression {
-    return new VariableExpression(createToken(TokenKind.Identifier, ident, range), namespaceName);
+export function createVariableExpression(ident: string, range?: Range): VariableExpression {
+    return new VariableExpression(createToken(TokenKind.Identifier, ident, range));
 }
 
-export function createDottedIdentifier(path: string[], range?: Range, namespaceName?: NamespacedVariableNameExpression): DottedGetExpression {
+export function createDottedIdentifier(path: string[], range?: Range): DottedGetExpression {
     const ident = path.pop();
-    const obj = path.length > 1 ? createDottedIdentifier(path, range, namespaceName) : createVariableExpression(path[0], range, namespaceName);
+    const obj = path.length > 1 ? createDottedIdentifier(path, range) : createVariableExpression(path[0], range);
     return new DottedGetExpression(obj, createToken(TokenKind.Identifier, ident, range), createToken(TokenKind.Dot, '.', range));
 }
 
@@ -165,13 +164,12 @@ export function createClassMethodStatement(name: string, kind: TokenKind.Sub | T
     return createMethodStatement(name, kind, [accessModifier]);
 }
 
-export function createCall(callee: Expression, args?: Expression[], namespaceName?: NamespacedVariableNameExpression) {
+export function createCall(callee: Expression, args?: Expression[]) {
     return new CallExpression(
         callee,
         createToken(TokenKind.LeftParen, '('),
         createToken(TokenKind.RightParen, ')'),
-        args || [],
-        namespaceName
+        args || []
     );
 }
 
diff --git a/src/astUtils/reflection.spec.ts b/src/astUtils/reflection.spec.ts
index 9b130213..c130b032 100644
--- a/src/astUtils/reflection.spec.ts
+++ b/src/astUtils/reflection.spec.ts
@@ -35,7 +35,7 @@ describe('reflection', () => {
         const comment = new CommentStatement([token]);
         const exitFor = new ExitForStatement({ exitFor: token });
         const exitWhile = new ExitWhileStatement({ exitWhile: token });
-        const funs = new FunctionStatement(ident, new FunctionExpression([], block, token, token, token, token), undefined);
+        const funs = new FunctionStatement(ident, new FunctionExpression([], block, token, token, token, token));
         const ifs = new IfStatement({ if: token }, expr, block);
         const increment = new IncrementStatement(expr, token);
         const print = new PrintStatement({ print: token }, []);
@@ -192,7 +192,7 @@ describe('reflection', () => {
         };
         const nsVar = new NamespacedVariableNameExpression(createVariableExpression('a', range));
         const binary = new BinaryExpression(expr, token, expr);
-        const call = new CallExpression(expr, token, token, [], undefined);
+        const call = new CallExpression(expr, token, token, []);
         const fun = new FunctionExpression([], block, token, token, token, token);
         const dottedGet = new DottedGetExpression(expr, ident, token);
         const xmlAttrGet = new XmlAttributeGetExpression(expr, ident, token);
@@ -203,7 +203,7 @@ describe('reflection', () => {
         const arrayLit = new ArrayLiteralExpression([], token, token);
         const aaLit = new AALiteralExpression([], token, token);
         const unary = new UnaryExpression(token, expr);
-        const variable = new VariableExpression(ident, undefined);
+        const variable = new VariableExpression(ident);
         const sourceLit = new SourceLiteralExpression(token);
         const newx = new NewExpression(token, call);
         const callfunc = new CallfuncExpression(expr, token, ident, token, [], token);
diff --git a/src/bscPlugin/hover/HoverProcessor.spec.ts b/src/bscPlugin/hover/HoverProcessor.spec.ts
index 66527da9..58c7b5cd 100644
--- a/src/bscPlugin/hover/HoverProcessor.spec.ts
+++ b/src/bscPlugin/hover/HoverProcessor.spec.ts
@@ -135,6 +135,7 @@ describe('HoverProcessor', () => {
 
                 end sub
             `);
+            program.validate();
 
             let hover = program.getHover(mainFile.pathAbsolute, util.createPosition(2, 25))[0];
             expect(hover?.range).to.eql(util.createRange(2, 20, 2, 29));
@@ -148,6 +149,7 @@ describe('HoverProcessor', () => {
                 end sub
                 const SOME_VALUE = true
             `);
+            program.validate();
             // print SOM|E_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(2, 29))[0];
             expect(hover?.range).to.eql(util.createRange(2, 26, 2, 36));
@@ -162,6 +164,7 @@ describe('HoverProcessor', () => {
                 end sub
                 const SOME_VALUE = "value"
             `);
+            program.validate();
             // value += SOME|_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(3, 33))[0];
             expect(hover?.range).to.eql(util.createRange(3, 29, 3, 39));
@@ -178,6 +181,7 @@ describe('HoverProcessor', () => {
                     const SOME_VALUE = "value"
                 end namespace
             `);
+            program.validate();
             // value += SOME|_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(3, 47))[0];
             expect(hover?.range).to.eql(util.createRange(3, 43, 3, 53));
@@ -193,6 +197,7 @@ describe('HoverProcessor', () => {
                     const SOME_VALUE = true
                 end namespace
             `);
+            program.validate();
             // print name.SOM|E_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(2, 34))[0];
             expect(hover?.range).to.eql(util.createRange(2, 31, 2, 41));
@@ -208,6 +213,7 @@ describe('HoverProcessor', () => {
                     const SOME_VALUE = true
                 end namespace
             `);
+            program.validate();
             // print name.sp.a.c.e.SOM|E_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(2, 43))[0];
             expect(hover?.range).to.eql(util.createRange(2, 40, 2, 50));
diff --git a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts
index 07f28d48..86df2b3b 100644
--- a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts
+++ b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts
@@ -1,11 +1,12 @@
 import type { Range } from 'vscode-languageserver-protocol';
 import { SemanticTokenModifiers } from 'vscode-languageserver-protocol';
 import { SemanticTokenTypes } from 'vscode-languageserver-protocol';
-import { isCallExpression, isCustomType, isNewExpression } from '../../astUtils/reflection';
+import { isCallExpression, isCustomType, isNamespaceStatement, isNewExpression } from '../../astUtils/reflection';
 import type { BrsFile } from '../../files/BrsFile';
 import type { OnGetSemanticTokensEvent } from '../../interfaces';
 import type { Locatable } from '../../lexer/Token';
 import { ParseMode } from '../../parser/Parser';
+import type { NamespaceStatement } from '../../parser/Statement';
 import util from '../../util';
 
 export class BrsFileSemanticTokensProcessor {
@@ -35,9 +36,10 @@ export class BrsFileSemanticTokensProcessor {
         for (const func of this.event.file.parser.references.functionExpressions) {
             for (const parm of func.parameters) {
                 if (isCustomType(parm.type)) {
+                    const namespace = parm.findAncestor<NamespaceStatement>(isNamespaceStatement);
                     classes.push({
                         className: parm.typeToken.text,
-                        namespaceName: parm.namespaceName?.getName(ParseMode.BrighterScript),
+                        namespaceName: namespace?.getName(ParseMode.BrighterScript),
                         range: parm.typeToken.range
                     });
                 }
diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts
index dba2b0d5..b9ead406 100644
--- a/src/bscPlugin/validation/BrsFileValidator.ts
+++ b/src/bscPlugin/validation/BrsFileValidator.ts
@@ -1,12 +1,14 @@
 import { isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isEnumStatement, isFunctionStatement, isImportStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespacedVariableNameExpression, isNamespaceStatement } from '../../astUtils/reflection';
-import { WalkMode } from '../../astUtils/visitors';
+import { createVisitor, WalkMode } from '../../astUtils/visitors';
 import { DiagnosticMessages } from '../../DiagnosticMessages';
 import type { BrsFile } from '../../files/BrsFile';
 import type { OnFileValidateEvent } from '../../interfaces';
 import { TokenKind } from '../../lexer/TokenKind';
 import type { AstNode } from '../../parser/AstNode';
 import type { LiteralExpression } from '../../parser/Expression';
+import { ParseMode } from '../../parser/Parser';
 import type { EnumMemberStatement, EnumStatement, ImportStatement, LibraryStatement } from '../../parser/Statement';
+import { DynamicType } from '../../types/DynamicType';
 import util from '../../util';
 
 export class BrsFileValidator {
@@ -29,8 +31,8 @@ export class BrsFileValidator {
      * Set the parent node on a given AstNode. This handles some edge cases where not every expression is iterated normally,
      * so it will also reach into nested objects to set their parent values as well
      */
-    private setParent(node1: AstNode, parent: AstNode) {
-        const pairs = [[node1, parent]];
+    private setParent(node: AstNode, parent: AstNode) {
+        const pairs = [[node, parent]];
         while (pairs.length > 0) {
             const [childNode, parentNode] = pairs.pop();
             //skip this entry if there's already a parent
@@ -58,6 +60,13 @@ export class BrsFileValidator {
                 pairs.push([childNode.expression, childNode]);
             }
             childNode.parent = parentNode;
+            //if the node has a symbol table, link it to its parent's symbol table
+            if (childNode.symbolTable) {
+                const parentSymbolTable = parentNode.getSymbolTable();
+                if (parentSymbolTable) {
+                    childNode.symbolTable.pushParent(parentSymbolTable);
+                }
+            }
         }
     }
 
@@ -65,14 +74,85 @@ export class BrsFileValidator {
      * Walk the full AST
      */
     private walk() {
+        const visitor = createVisitor({
+            MethodStatement: (node) => {
+                //add the `super` symbol to class methods
+                node.func.body.symbolTable.addSymbol('super', undefined, DynamicType.instance);
+            },
+            EnumStatement: (node) => {
+                this.validateEnumDeclaration(node);
+
+                //register this enum declaration
+                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
+            },
+            ClassStatement: (node) => {
+                //register this class
+                node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
+            },
+            AssignmentStatement: (node) => {
+                //register this variable
+                node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
+            },
+            FunctionParameterExpression: (node) => {
+                node.getSymbolTable()?.addSymbol(node.name.text, node.name.range, node.type);
+            },
+            ForEachStatement: (node) => {
+                //register the for loop variable
+                node.parent.getSymbolTable()?.addSymbol(node.item.text, node.item.range, DynamicType.instance);
+            },
+            NamespaceStatement: (node) => {
+                node.parent.getSymbolTable().addSymbol(
+                    node.name.split('.')[0],
+                    node.nameExpression.range,
+                    DynamicType.instance
+                );
+            },
+            FunctionStatement: (node) => {
+                if (node.name?.text) {
+                    node.parent.getSymbolTable().addSymbol(
+                        node.name.text,
+                        node.name.range,
+                        DynamicType.instance
+                    );
+                }
+
+                const namespace = node.findAncestor(isNamespaceStatement);
+                //this function is declared inside a namespace
+                if (namespace) {
+                    //add the transpiled name for namespaced functions to the root symbol table
+                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
+                    const funcType = node.func.getFunctionType();
+                    funcType.setName(transpiledNamespaceFunctionName);
+
+                    this.event.file.parser.ast.symbolTable.addSymbol(
+                        transpiledNamespaceFunctionName,
+                        node.name.range,
+                        funcType
+                    );
+                }
+            },
+            FunctionExpression: (node) => {
+                if (!node.body.symbolTable.hasSymbol('m')) {
+                    node.body.symbolTable.addSymbol('m', undefined, DynamicType.instance);
+                }
+            },
+            ConstStatement: (node) => {
+                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
+            },
+            CatchStatement: (node) => {
+                node.parent.getSymbolTable().addSymbol(node.exceptionVariable.text, node.exceptionVariable.range, DynamicType.instance);
+            },
+            DimStatement: (node) => {
+                if (node.identifier) {
+                    node.parent.getSymbolTable().addSymbol(node.identifier.text, node.identifier.range, DynamicType.instance);
+                }
+            }
+        });
+
         this.event.file.ast.walk((node, parent) => {
             // link every child with its parent
             this.setParent(node, parent);
-
-            //do some file-based validations
-            if (isEnumStatement(node)) {
-                this.validateEnumDeclaration(node);
-            }
+            visitor(node, parent);
         }, {
             walkMode: WalkMode.visitAllRecursive
         });
diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts
index 91d5f3be..33d9199f 100644
--- a/src/bscPlugin/validation/ScopeValidator.ts
+++ b/src/bscPlugin/validation/ScopeValidator.ts
@@ -1,5 +1,5 @@
 import { URI } from 'vscode-uri';
-import { isBrsFile, isCallExpression, isLiteralExpression, isNewExpression, isXmlScope } from '../../astUtils/reflection';
+import { isBrsFile, isLiteralExpression, isXmlScope } from '../../astUtils/reflection';
 import { Cache } from '../../Cache';
 import { DiagnosticMessages } from '../../DiagnosticMessages';
 import type { BrsFile } from '../../files/BrsFile';
@@ -10,9 +10,9 @@ import { nodes, components } from '../../roku-types';
 import type { BRSComponentData } from '../../roku-types';
 import type { Token } from '../../lexer/Token';
 import type { Scope } from '../../Scope';
-import type { SymbolTable } from '../../SymbolTable';
-import type { DiagnosticRelatedInformation, Position } from 'vscode-languageserver';
+import type { DiagnosticRelatedInformation } from 'vscode-languageserver';
 import type { Expression } from '../../parser/AstNode';
+import type { VariableExpression, DottedGetExpression } from '../../parser/Expression';
 
 /**
  * The lower-case names of all platform-included scenegraph nodes
@@ -27,181 +27,133 @@ const platformComponentNames = new Set(Object.values(components).map(x => x.name
  */
 export class ScopeValidator {
 
-    private events: OnScopeValidateEvent[] = [];
+    /**
+     * The event currently being processed. This will change multiple times throughout the lifetime of this validator
+     */
+    private event: OnScopeValidateEvent;
 
     public processEvent(event: OnScopeValidateEvent) {
-        this.events.push(event);
-        event.scope.linkSymbolTable();
-        this.detectDuplicateEnums(event);
-        this.validateCreateObjectCalls(event);
-        this.iterateExpressions(event);
-        event.scope.unlinkSymbolTable();
+        this.event = event;
+        this.walkFiles();
+        this.detectDuplicateEnums();
     }
 
     public reset() {
+        this.event = undefined;
         this.onceCache.clear();
         this.multiScopeCache.clear();
-        this.events = [];
     }
 
-    /**
-     * Adds a diagnostic to the first scope for this key. Prevents duplicate diagnostics
-     * for diagnostics where scope isn't important. (i.e. CreateObject validations)
-     */
-    private addDiagnosticOnce(event: OnScopeValidateEvent, diagnostic: BsDiagnostic) {
-        this.onceCache.getOrAdd(`${diagnostic.code}-${diagnostic.message}-${util.rangeToString(diagnostic.range)}`, () => {
-            event.scope.addDiagnostics([diagnostic]);
-            return true;
+    private walkFiles() {
+        this.event.scope.enumerateOwnFiles((file) => {
+            if (isBrsFile(file)) {
+                this.iterateFileExpressions(file);
+                this.validateCreateObjectCalls(file);
+            }
         });
     }
-    private onceCache = new Cache<string, boolean>();
 
-    private addDiagnostic(event: OnScopeValidateEvent, diagnostic: BsDiagnostic) {
-        event.scope.addDiagnostics([diagnostic]);
-    }
+    private expressionsByFile = new Cache<BrsFile, Readonly<ExpressionInfo>[]>();
+    private iterateFileExpressions(file: BrsFile) {
+        const { scope } = this.event;
+        //build an expression collection ONCE per file
+        const expressionInfos = this.expressionsByFile.getOrAdd(file, () => {
+            const result: DeepWriteable<ExpressionInfo[]> = [];
+            const expressions = [
+                ...file.parser.references.expressions,
+                //all class "extends <whatever>" expressions
+                ...file.parser.references.classStatements.map(x => x.parentClassName?.expression),
+                //all interface "extends <whatever>" expressions
+                ...file.parser.references.interfaceStatements.map(x => x.parentInterfaceName?.expression)
+            ];
+            for (let expression of expressions) {
+                if (!expression) {
+                    continue;
+                }
 
-    /**
-     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
-     */
-    private addMultiScopeDiagnostic(event: OnScopeValidateEvent, diagnostic: BsDiagnostic, message = 'Not defined in scope') {
-        diagnostic = this.multiScopeCache.getOrAdd(`${diagnostic.file?.srcPath}-${diagnostic.code}-${diagnostic.message}-${util.rangeToString(diagnostic.range)}`, () => {
-            if (!diagnostic.relatedInformation) {
-                diagnostic.relatedInformation = [];
+                //walk left-to-right on every expression, only keep the ones that start with VariableExpression, and then keep subsequent DottedGet parts
+                const parts = util.getDottedGetPath(expression);
+
+                if (parts.length > 0) {
+                    result.push({
+                        parts: parts,
+                        expression: expression
+                    });
+                }
             }
-            this.addDiagnostic(event, diagnostic);
-            return diagnostic;
+            return result as unknown as Readonly<ExpressionInfo>[];
         });
-        const info = {
-            message: `${message} '${event.scope.name}'`
-        } as DiagnosticRelatedInformation;
-        if (isXmlScope(event.scope) && event.scope.xmlFile?.srcPath) {
-            info.location = util.createLocation(
-                URI.file(event.scope.xmlFile.srcPath).toString(),
-                util.createRange(0, 0, 0, 10)
-            );
-        } else {
-            info.location = util.createLocation(
-                URI.file(diagnostic.file.srcPath).toString(),
-                diagnostic.range
-            );
-        }
-        diagnostic.relatedInformation.push(info);
-    }
 
-    /**
-     * Find the closest symbol table for the given position
-     */
-    private getSymbolTable(scope: Scope, file: BrsFile, position: Position) {
-        let symbolTable: SymbolTable;
-        symbolTable = file.getFunctionScopeAtPosition(position)?.func.symbolTable;
-        if (!symbolTable) {
-            symbolTable = file.getNamespaceStatementForPosition(position)?.symbolTable;
-        }
-        if (!symbolTable) {
-            symbolTable = scope.symbolTable;
-        }
-        return symbolTable;
-    }
-
-    private multiScopeCache = new Cache<string, BsDiagnostic>();
+        outer:
+        for (const info of expressionInfos) {
+            const symbolTable = info.expression.getSymbolTable();
+            const firstPart = info.parts[0];
+            //flag all unknown left-most variables
+            if (!symbolTable.hasSymbol(firstPart.name?.text)) {
+                this.addMultiScopeDiagnostic({
+                    file: file as BscFile,
+                    ...DiagnosticMessages.cannotFindName(firstPart.name?.text),
+                    range: firstPart.name.range
+                });
+                //skip to the next expression
+                continue;
+            }
 
-    private iterateExpressions(event: OnScopeValidateEvent) {
-        const { scope } = event;
-        event.scope.enumerateOwnFiles((file) => {
-            if (isBrsFile(file)) {
-                const expressions = [
-                    ...file.parser.references.expressions,
-                    //all class "extends <whatever>" expressions
-                    ...file.parser.references.classStatements.map(x => x.parentClassName?.expression),
-                    //all interface "extends <whatever>" expressions
-                    ...file.parser.references.interfaceStatements.map(x => x.parentInterfaceName?.expression)
-                ];
-                outer:
-                for (let referenceExpression of expressions) {
-                    if (!referenceExpression) {
-                        continue;
-                    }
-                    let expression: Expression;
-                    //lift the callee from call expressions to handle namespaced function calls
-                    if (isCallExpression(referenceExpression)) {
-                        expression = referenceExpression.callee;
-                    } else if (isNewExpression(referenceExpression)) {
-                        expression = referenceExpression.call.callee;
-                    } else {
-                        expression = referenceExpression;
-                    }
-                    const tokens = util.getAllDottedGetParts(expression);
-                    if (tokens?.length > 0) {
-                        const symbolTable = this.getSymbolTable(scope, file, tokens[0].range.start); //flag all unknown left-most variables
-                        if (!symbolTable.hasSymbol(tokens[0]?.text)) {
-                            this.addMultiScopeDiagnostic(event, {
-                                file: file as BscFile,
-                                ...DiagnosticMessages.cannotFindName(tokens[0].text),
-                                range: tokens[0].range
-                            });
-                            //skip to the next expression
-                            continue;
-                        }
-                        //at this point, we know the first item is a known symbol. find unknown namespace parts after the first part
-                        if (tokens.length > 1) {
-                            const firstNamespacePart = tokens.shift().text;
-                            const firstNamespacePartLower = firstNamespacePart?.toLowerCase();
-                            const namespaceContainer = scope.namespaceLookup.get(firstNamespacePartLower);
-                            const enumStatement = scope.getEnum(firstNamespacePartLower);
-                            //if this isn't a namespace, skip it
-                            if (!namespaceContainer && !enumStatement) {
-                                continue;
-                            }
-                            //catch unknown namespace items
-                            const processedNames: string[] = [firstNamespacePart];
-                            for (const token of tokens ?? []) {
-                                processedNames.push(token.text);
-                                const entityName = processedNames.join('.');
-                                const entityNameLower = entityName.toLowerCase();
+            const firstNamespacePart = info.parts[0].name.text;
+            const firstNamespacePartLower = firstNamespacePart?.toLowerCase();
+            const namespaceContainer = scope.namespaceLookup.get(firstNamespacePartLower);
+            const enumStatement = scope.getEnum(firstNamespacePartLower);
+            //if this isn't a namespace, skip it
+            if (!namespaceContainer && !enumStatement) {
+                continue;
+            }
+            //catch unknown namespace items
+            const processedNames: string[] = [firstNamespacePart];
+            for (let i = 1; i < info.parts.length; i++) {
+                const part = info.parts[i];
+                processedNames.push(part.name.text);
+                const entityName = processedNames.join('.');
+                const entityNameLower = entityName.toLowerCase();
 
-                                //if this is an enum member, stop validating here to prevent errors further down the chain
-                                if (scope.getEnumMemberMap().has(entityNameLower)) {
-                                    break;
-                                }
+                //if this is an enum member, stop validating here to prevent errors further down the chain
+                if (scope.getEnumMemberMap().has(entityNameLower)) {
+                    break;
+                }
 
-                                if (
-                                    !scope.getEnumMap().has(entityNameLower) &&
-                                    !scope.getClassMap().has(entityNameLower) &&
-                                    !scope.getConstMap().has(entityNameLower) &&
-                                    !scope.getCallableByName(entityNameLower) &&
-                                    !scope.namespaceLookup.has(entityNameLower)
-                                ) {
-                                    //if this looks like an enum, provide a nicer error message
-                                    const theEnum = this.getEnum(scope, entityNameLower)?.item;
-                                    if (theEnum) {
-                                        this.addMultiScopeDiagnostic(event, {
-                                            file: file,
-                                            ...DiagnosticMessages.unknownEnumValue(token.text?.split('.').pop(), theEnum.fullName),
-                                            range: tokens[tokens.length - 1].range,
-                                            relatedInformation: [{
-                                                message: 'Enum declared here',
-                                                location: util.createLocation(
-                                                    URI.file(file.srcPath).toString(),
-                                                    theEnum.tokens.name.range
-                                                )
-                                            }]
-                                        });
-                                    } else {
-                                        this.addMultiScopeDiagnostic(event, {
-                                            ...DiagnosticMessages.cannotFindName(token.text, entityName),
-                                            range: token.range,
-                                            file: file
-                                        });
-                                    }
-                                    //no need to add another diagnostic for future unknown items
-                                    continue outer;
-                                }
-                            }
-                        }
+                if (
+                    !scope.getEnumMap().has(entityNameLower) &&
+                    !scope.getClassMap().has(entityNameLower) &&
+                    !scope.getConstMap().has(entityNameLower) &&
+                    !scope.getCallableByName(entityNameLower) &&
+                    !scope.namespaceLookup.has(entityNameLower)
+                ) {
+                    //if this looks like an enum, provide a nicer error message
+                    const theEnum = this.getEnum(scope, entityNameLower)?.item;
+                    if (theEnum) {
+                        this.addMultiScopeDiagnostic({
+                            file: file,
+                            ...DiagnosticMessages.unknownEnumValue(part.name.text?.split('.').pop(), theEnum.fullName),
+                            range: part.name.range,
+                            relatedInformation: [{
+                                message: 'Enum declared here',
+                                location: util.createLocation(
+                                    URI.file(file.srcPath).toString(),
+                                    theEnum.tokens.name.range
+                                )
+                            }]
+                        });
+                    } else {
+                        this.addMultiScopeDiagnostic({
+                            ...DiagnosticMessages.cannotFindName(part.name.text, entityName),
+                            range: part.name.range,
+                            file: file
+                        });
                     }
+                    //no need to add another diagnostic for future unknown items
+                    continue outer;
                 }
             }
-        });
+        }
     }
 
     /**
@@ -224,10 +176,10 @@ export class ScopeValidator {
     /**
      * Flag duplicate enums
      */
-    private detectDuplicateEnums(event: OnScopeValidateEvent) {
+    private detectDuplicateEnums() {
         const diagnostics: BsDiagnostic[] = [];
         const enumLocationsByName = new Cache<string, Array<{ file: BrsFile; statement: EnumStatement }>>();
-        event.scope.enumerateBrsFiles((file) => {
+        this.event.scope.enumerateBrsFiles((file) => {
             for (const enumStatement of file.parser.references.enumStatements) {
                 const fullName = enumStatement.fullName;
                 const nameLower = fullName?.toLowerCase();
@@ -257,7 +209,7 @@ export class ScopeValidator {
             const fullName = primaryEnum.statement.fullName;
             for (const duplicateEnumInfo of enumLocations) {
                 diagnostics.push({
-                    ...DiagnosticMessages.duplicateEnumDeclaration(event.scope.name, fullName),
+                    ...DiagnosticMessages.duplicateEnumDeclaration(this.event.scope.name, fullName),
                     file: duplicateEnumInfo.file,
                     range: duplicateEnumInfo.statement.tokens.name.range,
                     relatedInformation: [{
@@ -270,7 +222,7 @@ export class ScopeValidator {
                 });
             }
         }
-        event.scope.addDiagnostics(diagnostics);
+        this.event.scope.addDiagnostics(diagnostics);
     }
 
     /**
@@ -279,76 +231,126 @@ export class ScopeValidator {
      * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
      * do this manually for now.
      */
-    protected validateCreateObjectCalls(event: OnScopeValidateEvent) {
+    protected validateCreateObjectCalls(file: BrsFile) {
         const diagnostics: BsDiagnostic[] = [];
 
-        event.scope.enumerateBrsFiles((file) => {
-            for (const call of file.functionCalls) {
-                //skip non CreateObject function calls
-                if (call.name?.toLowerCase() !== 'createobject' || !isLiteralExpression(call?.args[0]?.expression)) {
+        for (const call of file.functionCalls) {
+            //skip non CreateObject function calls
+            if (call.name?.toLowerCase() !== 'createobject' || !isLiteralExpression(call?.args[0]?.expression)) {
+                continue;
+            }
+            const firstParamToken = (call?.args[0]?.expression as any)?.token;
+            const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
+            //if this is a `createObject('roSGNode'` call, only support known sg node types
+            if (firstParamStringValue?.toLowerCase() === 'rosgnode' && isLiteralExpression(call?.args[1]?.expression)) {
+                const componentName: Token = (call?.args[1]?.expression as any)?.token;
+                //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
+                if (componentName?.text?.includes(':')) {
                     continue;
                 }
-                const firstParamToken = (call?.args[0]?.expression as any)?.token;
-                const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
-                //if this is a `createObject('roSGNode'` call, only support known sg node types
-                if (firstParamStringValue?.toLowerCase() === 'rosgnode' && isLiteralExpression(call?.args[1]?.expression)) {
-                    const componentName: Token = (call?.args[1]?.expression as any)?.token;
-                    //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
-                    if (componentName?.text?.includes(':')) {
-                        continue;
-                    }
-                    //add diagnostic for unknown components
-                    const unquotedComponentName = componentName?.text?.replace(/"/g, '');
-                    if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !event.program.getComponent(unquotedComponentName)) {
-                        this.addDiagnosticOnce(event, {
-                            file: file as BscFile,
-                            ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
-                            range: componentName.range
-                        });
-                    } else if (call?.args.length !== 2) {
-                        // roSgNode should only ever have 2 args in `createObject`
-                        this.addDiagnosticOnce(event, {
-                            file: file as BscFile,
-                            ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
-                            range: call.range
-                        });
-                    }
-                } else if (!platformComponentNames.has(firstParamStringValue.toLowerCase())) {
-                    this.addDiagnosticOnce(event, {
+                //add diagnostic for unknown components
+                const unquotedComponentName = componentName?.text?.replace(/"/g, '');
+                if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
+                    this.addDiagnosticOnce({
                         file: file as BscFile,
-                        ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
-                        range: firstParamToken.range
+                        ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
+                        range: componentName.range
                     });
-                } else {
-                    // This is valid brightscript component
-                    // Test for invalid arg counts
-                    const brightScriptComponent: BRSComponentData = components[firstParamStringValue.toLowerCase()];
-                    // Valid arg counts for createObject are 1+ number of args for constructor
-                    let validArgCounts = brightScriptComponent.constructors.map(cnstr => cnstr.params.length + 1);
-                    if (validArgCounts.length === 0) {
-                        // no constructors for this component, so createObject only takes 1 arg
-                        validArgCounts = [1];
-                    }
-                    if (!validArgCounts.includes(call?.args.length)) {
-                        // Incorrect number of arguments included in `createObject()`
-                        this.addDiagnosticOnce(event, {
-                            file: file as BscFile,
-                            ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
-                            range: call.range
-                        });
-                    }
+                } else if (call?.args.length !== 2) {
+                    // roSgNode should only ever have 2 args in `createObject`
+                    this.addDiagnosticOnce({
+                        file: file as BscFile,
+                        ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
+                        range: call.range
+                    });
+                }
+            } else if (!platformComponentNames.has(firstParamStringValue.toLowerCase())) {
+                this.addDiagnosticOnce({
+                    file: file as BscFile,
+                    ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
+                    range: firstParamToken.range
+                });
+            } else {
+                // This is valid brightscript component
+                // Test for invalid arg counts
+                const brightScriptComponent: BRSComponentData = components[firstParamStringValue.toLowerCase()];
+                // Valid arg counts for createObject are 1+ number of args for constructor
+                let validArgCounts = brightScriptComponent.constructors.map(cnstr => cnstr.params.length + 1);
+                if (validArgCounts.length === 0) {
+                    // no constructors for this component, so createObject only takes 1 arg
+                    validArgCounts = [1];
+                }
+                if (!validArgCounts.includes(call?.args.length)) {
+                    // Incorrect number of arguments included in `createObject()`
+                    this.addDiagnosticOnce({
+                        file: file as BscFile,
+                        ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
+                        range: call.range
+                    });
+                }
 
-                    // Test for deprecation
-                    if (brightScriptComponent.isDeprecated) {
-                        this.addDiagnosticOnce(event, {
-                            file: file as BscFile,
-                            ...DiagnosticMessages.deprecatedBrightScriptComponent(firstParamStringValue, brightScriptComponent.deprecatedDescription),
-                            range: call.range
-                        });
-                    }
+                // Test for deprecation
+                if (brightScriptComponent.isDeprecated) {
+                    this.addDiagnosticOnce({
+                        file: file as BscFile,
+                        ...DiagnosticMessages.deprecatedBrightScriptComponent(firstParamStringValue, brightScriptComponent.deprecatedDescription),
+                        range: call.range
+                    });
                 }
             }
+        }
+        this.event.scope.addDiagnostics(diagnostics);
+    }
+
+    /**
+     * Adds a diagnostic to the first scope for this key. Prevents duplicate diagnostics
+     * for diagnostics where scope isn't important. (i.e. CreateObject validations)
+     */
+    private addDiagnosticOnce(diagnostic: BsDiagnostic) {
+        this.onceCache.getOrAdd(`${diagnostic.code}-${diagnostic.message}-${util.rangeToString(diagnostic.range)}`, () => {
+            this.event.scope.addDiagnostics([diagnostic]);
+            return true;
+        });
+    }
+    private onceCache = new Cache<string, boolean>();
+
+    private addDiagnostic(diagnostic: BsDiagnostic) {
+        this.event.scope.addDiagnostics([diagnostic]);
+    }
+
+    /**
+     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
+     */
+    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, message = 'Not defined in scope') {
+        diagnostic = this.multiScopeCache.getOrAdd(`${diagnostic.file?.srcPath}-${diagnostic.code}-${diagnostic.message}-${util.rangeToString(diagnostic.range)}`, () => {
+            if (!diagnostic.relatedInformation) {
+                diagnostic.relatedInformation = [];
+            }
+            this.addDiagnostic(diagnostic);
+            return diagnostic;
         });
-        event.scope.addDiagnostics(diagnostics);
+        const info = {
+            message: `${message} '${this.event.scope.name}'`
+        } as DiagnosticRelatedInformation;
+        if (isXmlScope(this.event.scope) && this.event.scope.xmlFile?.srcPath) {
+            info.location = util.createLocation(
+                URI.file(this.event.scope.xmlFile.srcPath).toString(),
+                util.createRange(0, 0, 0, 10)
+            );
+        } else {
+            info.location = util.createLocation(
+                URI.file(diagnostic.file.srcPath).toString(),
+                diagnostic.range
+            );
+        }
+        diagnostic.relatedInformation.push(info);
     }
+
+    private multiScopeCache = new Cache<string, BsDiagnostic>();
+}
+
+interface ExpressionInfo {
+    parts: Readonly<[VariableExpression, ...DottedGetExpression[]]>;
+    expression: Readonly<Expression>;
 }
+type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
diff --git a/src/files/BrsFile.Class.spec.ts b/src/files/BrsFile.Class.spec.ts
index dc4b2f20..1278829e 100644
--- a/src/files/BrsFile.Class.spec.ts
+++ b/src/files/BrsFile.Class.spec.ts
@@ -317,7 +317,6 @@ describe('BrsFile BrighterScript classes', () => {
             `, undefined, 'source/main.bs');
         });
 
-
         it('works with namespaces', () => {
             testTranspile(`
                 namespace Birds.WaterFowl
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index 8603756e..5e6c1207 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -2787,12 +2787,14 @@ describe('BrsFile', () => {
             it('sets invalid on empty callfunc', () => {
                 testTranspile(`
                     sub main()
+                        node = invalid
                         node@.doSomething()
                         m.top.node@.doSomething()
                         m.top.node@.doSomething(1)
                     end sub
                 `, `
                     sub main()
+                        node = invalid
                         node.callfunc("doSomething", invalid)
                         m.top.node.callfunc("doSomething", invalid)
                         m.top.node.callfunc("doSomething", 1)
@@ -2803,10 +2805,12 @@ describe('BrsFile', () => {
             it('includes original arguments', () => {
                 testTranspile(`
                     sub main()
+                        node = invalid
                         node@.doSomething(1, true, m.top.someVal)
                     end sub
                 `, `
                     sub main()
+                        node = invalid
                         node.callfunc("doSomething", 1, true, m.top.someVal)
                     end sub
                 `);
@@ -2957,6 +2961,7 @@ describe('BrsFile', () => {
     describe('getTypedef', () => {
         function testTypedef(original: string, expected: string) {
             let file = program.setFile<BrsFile>('source/main.brs', original);
+            program.validate();
             expect(file.getTypedef().trimEnd()).to.eql(expected);
         }
 
@@ -3339,6 +3344,7 @@ describe('BrsFile', () => {
                     const CHARLIE = true
                 end namespace
             `);
+            program.validate();
             //print alpha.beta.char|lie
             expect(program.getDefinition(file.srcPath, Position.create(2, 41))).to.eql([{
                 uri: URI.file(file.srcPath).toString(),
diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts
index 048d4690..204900cf 100644
--- a/src/files/BrsFile.ts
+++ b/src/files/BrsFile.ts
@@ -605,7 +605,7 @@ export class BrsFile {
                 range: statement.func.range,
                 type: functionType,
                 getName: statement.getName.bind(statement),
-                hasNamespace: !!statement.namespaceName,
+                hasNamespace: !!statement.findAncestor<NamespaceStatement>(isNamespaceStatement),
                 functionStatement: statement
             });
         }
diff --git a/src/parser/AstNode.ts b/src/parser/AstNode.ts
index ece4422d..31f274c6 100644
--- a/src/parser/AstNode.ts
+++ b/src/parser/AstNode.ts
@@ -33,16 +33,32 @@ export abstract class AstNode {
      * Certain expressions or statements can have a symbol table (such as blocks, functions, namespace bodies, etc).
      * If you're interested in getting the closest SymbolTable, use `getSymbolTable` instead.
      */
-    protected symbolTable?: SymbolTable;
+    public symbolTable?: SymbolTable;
 
     /**
      * Get the closest symbol table for this node. Should be overridden in children that directly contain a symbol table
      */
     public getSymbolTable(): SymbolTable {
-        return this.parent?.getSymbolTable();
+        if (this.symbolTable) {
+            return this.symbolTable;
+        } else {
+            return this.parent?.getSymbolTable();
+        }
     }
-}
 
+    /**
+     * Walk upward and return the first node that results in `true` from the matcher
+     */
+    public findAncestor<TNode extends AstNode = AstNode>(matcher: (node: AstNode) => boolean | undefined) {
+        let node = this.parent;
+        while (node) {
+            if (matcher(node)) {
+                return node as TNode;
+            }
+            node = node.parent;
+        }
+    }
+}
 
 export abstract class Statement extends AstNode {
     /**
diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts
index 3e752562..07564f03 100644
--- a/src/parser/Expression.ts
+++ b/src/parser/Expression.ts
@@ -1,7 +1,7 @@
 /* eslint-disable no-bitwise */
 import type { Token, Identifier } from '../lexer/Token';
 import { TokenKind } from '../lexer/TokenKind';
-import type { Block, CommentStatement, FunctionStatement } from './Statement';
+import type { Block, CommentStatement, FunctionStatement, NamespaceStatement } from './Statement';
 import type { Range } from 'vscode-languageserver';
 import util from '../util';
 import type { BrsTranspileState } from './BrsTranspileState';
@@ -9,13 +9,12 @@ import { ParseMode } from './Parser';
 import * as fileUrl from 'file-url';
 import type { WalkOptions, WalkVisitor } from '../astUtils/visitors';
 import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors';
-import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isStringType, isUnaryExpression, isVariableExpression } from '../astUtils/reflection';
+import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isNamespaceStatement, isStringType, isUnaryExpression, isVariableExpression } from '../astUtils/reflection';
 import type { TranspileResult, TypedefProvider } from '../interfaces';
 import { VoidType } from '../types/VoidType';
 import { DynamicType } from '../types/DynamicType';
 import type { BscType } from '../types/BscType';
 import { FunctionType } from '../types/FunctionType';
-import { SymbolTable } from '../SymbolTable';
 import { Expression } from './AstNode';
 
 export type ExpressionVisitor = (expression: Expression, parent: Expression) => void;
@@ -60,11 +59,7 @@ export class CallExpression extends Expression {
          */
         readonly openingParen: Token,
         readonly closingParen: Token,
-        readonly args: Expression[],
-        /**
-         * The namespace that currently wraps this call expression. This is NOT the namespace of the callee...that will be represented in the callee expression itself.
-         */
-        readonly namespaceName: NamespacedVariableNameExpression
+        readonly args: Expression[]
     ) {
         super();
         this.range = util.createRangeFromPositions(this.callee.range.start, this.closingParen.range.end);
@@ -120,9 +115,7 @@ export class FunctionExpression extends Expression implements TypedefProvider {
         /**
          * If this function is enclosed within another function, this will reference that parent function
          */
-        readonly parentFunction?: FunctionExpression,
-        readonly namespaceName?: NamespacedVariableNameExpression,
-        readonly parentSymbolTable?: SymbolTable
+        readonly parentFunction?: FunctionExpression
     ) {
         super();
         if (this.returnTypeToken) {
@@ -132,16 +125,6 @@ export class FunctionExpression extends Expression implements TypedefProvider {
         } else {
             this.returnType = DynamicType.instance;
         }
-        this.symbolTable = new SymbolTable(parentSymbolTable);
-        for (let param of parameters) {
-            this.symbolTable.addSymbol(param.name.text, param.name.range, DynamicType.instance);
-        }
-    }
-
-    public symbolTable: SymbolTable;
-
-    public getSymbolTable() {
-        return this.symbolTable;
     }
 
     /**
@@ -263,8 +246,7 @@ export class FunctionParameterExpression extends Expression {
         public name: Identifier,
         public typeToken?: Token,
         public defaultValue?: Expression,
-        public asToken?: Token,
-        readonly namespaceName?: NamespacedVariableNameExpression
+        public asToken?: Token
     ) {
         super();
         if (typeToken) {
@@ -763,8 +745,7 @@ export class UnaryExpression extends Expression {
 
 export class VariableExpression extends Expression {
     constructor(
-        readonly name: Identifier,
-        readonly namespaceName: NamespacedVariableNameExpression
+        readonly name: Identifier
     ) {
         super();
         this.range = this.name.range;
@@ -778,11 +759,12 @@ export class VariableExpression extends Expression {
 
     transpile(state: BrsTranspileState) {
         let result = [];
+        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
         //if the callee is the name of a known namespace function
-        if (state.file.calleeIsKnownNamespaceFunction(this, this.namespaceName?.getName(ParseMode.BrighterScript))) {
+        if (state.file.calleeIsKnownNamespaceFunction(this, namespace?.getName(ParseMode.BrighterScript))) {
             result.push(
                 state.sourceNode(this, [
-                    this.namespaceName.getName(ParseMode.BrightScript),
+                    namespace.getName(ParseMode.BrightScript),
                     '_',
                     this.getName(ParseMode.BrightScript)
                 ])
@@ -901,16 +883,13 @@ export class NewExpression extends Expression {
         return this.call.callee as NamespacedVariableNameExpression;
     }
 
-    public get namespaceName() {
-        return this.call.namespaceName;
-    }
-
     public readonly range: Range;
 
     public transpile(state: BrsTranspileState) {
+        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
         const cls = state.file.getClassFileLink(
             this.className.getName(ParseMode.BrighterScript),
-            this.namespaceName?.getName(ParseMode.BrighterScript)
+            namespace?.getName(ParseMode.BrighterScript)
         )?.item;
         //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
         //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index af39b69c..c68586f2 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -92,8 +92,6 @@ import { isAAMemberExpression, isAnnotationExpression, isBinaryExpression, isCal
 import { createVisitor, WalkMode } from '../astUtils/visitors';
 import { createStringLiteral, createToken } from '../astUtils/creators';
 import { Cache } from '../Cache';
-import { SymbolTable } from '../SymbolTable';
-import { DynamicType } from '../types/DynamicType';
 import type { Expression, Statement } from './AstNode';
 
 export class Parser {
@@ -117,12 +115,10 @@ export class Parser {
     }
 
     /**
-     * The top-level symbol table for this file. Things like top-level namespaces, non-namespaced classes, enums, interfaces, and functions beling here.
+     * The top-level symbol table for the body of this file.
      */
-    public symbolTable = new SymbolTable();
-
-    private get currentSymbolTable() {
-        return this.currentFunctionExpression?.symbolTable ?? this.currentNamespace?.symbolTable ?? this.symbolTable;
+    public get symbolTable() {
+        return this.ast.symbolTable;
     }
 
     /**
@@ -181,16 +177,6 @@ export class Parser {
 
     private globalTerminators = [] as TokenKind[][];
 
-    /**
-     * When a namespace has been started, this gets set. When it's done, this gets unset.
-     * It is useful for passing the namespace into certain statements that need it
-     */
-    private currentNamespace: NamespaceStatement;
-
-    private get currentNamespaceName(): NamespacedVariableNameExpression {
-        return this.currentNamespace?.nameExpression;
-    }
-
     /**
      * When a FunctionExpression has been started, this gets set. When it's done, this gets unset.
      * It's useful for passing the function into statements and expressions that need to be located
@@ -258,7 +244,7 @@ export class Parser {
     private body() {
         const parentAnnotations = this.enterAnnotationBlock();
 
-        let body = new Body([], this.symbolTable);
+        let body = new Body([]);
         if (this.tokens.length > 0) {
             this.consumeStatementSeparators(true);
 
@@ -535,8 +521,7 @@ export class Parser {
             extendsToken,
             parentInterfaceName,
             body,
-            endInterfaceToken,
-            this.currentNamespaceName
+            endInterfaceToken
         );
         this._references.interfaceStatements.push(statement);
         this.exitAnnotationBlock(parentAnnotations);
@@ -544,7 +529,7 @@ export class Parser {
     }
 
     private enumDeclaration(): EnumStatement {
-        const result = new EnumStatement({} as any, [], this.currentNamespaceName);
+        const result = new EnumStatement({} as any, []);
         this.warnIfNotBrighterScriptMode('enum declarations');
 
         const parentAnnotations = this.enterAnnotationBlock();
@@ -599,10 +584,6 @@ export class Parser {
         //consume the final `end interface` token
         result.tokens.endEnum = this.consumeToken(TokenKind.EndEnum);
 
-        if (result.name) {
-            this.currentSymbolTable.addSymbol(result.tokens.name.text, result.tokens.name.range, DynamicType.instance);
-        }
-
         this._references.enumStatements.push(result);
         this.exitAnnotationBlock(parentAnnotations);
         return result;
@@ -681,9 +662,6 @@ export class Parser {
                     //refer to this statement as parent of the expression
                     functionStatement.func.functionStatement = decl as MethodStatement;
 
-                    //add the `super` symbol to class methods
-                    funcDeclaration.func.symbolTable.addSymbol('super', undefined, DynamicType.instance);
-
                     //fields
                 } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
 
@@ -729,14 +707,9 @@ export class Parser {
             body,
             endingKeyword,
             extendsKeyword,
-            parentClassName,
-            this.currentNamespaceName
+            parentClassName
         );
 
-        if (className) {
-            this.currentSymbolTable.addSymbol(className.text, className.range, DynamicType.instance);
-        }
-
         this._references.classStatements.push(result);
         this.exitAnnotationBlock(parentAnnotations);
         return result;
@@ -907,13 +880,8 @@ export class Parser {
                 rightParen,
                 asToken,
                 typeToken,
-                this.currentFunctionExpression,
-                this.currentNamespaceName,
-                this.currentSymbolTable
+                this.currentFunctionExpression
             );
-            if (!func.symbolTable.hasSymbol('m')) {
-                func.symbolTable.addSymbol('m', undefined, DynamicType.instance);
-            }
             //if there is a parent function, register this function with the parent
             if (this.currentFunctionExpression) {
                 this.currentFunctionExpression.childFunctionExpressions.push(func);
@@ -923,9 +891,6 @@ export class Parser {
             if (!onlyCallableAsMember && name) {
                 const funcType = func.getFunctionType();
                 funcType.setName(name.text);
-
-                // add the function as declared to the current symbol table
-                this.currentSymbolTable.addSymbol(name.text, name.range, funcType);
             }
 
             this._references.functionExpressions.push(func);
@@ -966,25 +931,10 @@ export class Parser {
             if (isAnonymous) {
                 return func;
             } else {
-                let result = new FunctionStatement(name, func, this.currentNamespaceName);
+                let result = new FunctionStatement(name, func);
                 func.functionStatement = result;
                 this._references.functionStatements.push(result);
 
-                // Add the transpiled name for namespace functions
-                // to consider an edge case when defining namespaces in .bs files
-                // and using them in .brs files.
-                if (func.namespaceName) {
-                    const transpiledNamespaceFunctionName = result.getName(ParseMode.BrightScript);
-                    const funcType = func.getFunctionType();
-                    funcType.setName(transpiledNamespaceFunctionName);
-
-                    this.symbolTable.addSymbol(
-                        transpiledNamespaceFunctionName,
-                        name.range,
-                        funcType
-                    );
-                }
-
                 return result;
             }
         } finally {
@@ -1034,8 +984,7 @@ export class Parser {
             name,
             typeToken,
             defaultValue,
-            asToken,
-            this.currentNamespaceName
+            asToken
         );
     }
 
@@ -1058,7 +1007,7 @@ export class Parser {
         if (operator.kind === TokenKind.Equal) {
             result = new AssignmentStatement(operator, name, value, this.currentFunctionExpression);
         } else {
-            const nameExpression = new VariableExpression(name, this.currentNamespaceName);
+            const nameExpression = new VariableExpression(name);
             result = new AssignmentStatement(
                 operator,
                 name,
@@ -1073,7 +1022,6 @@ export class Parser {
             this._references.expressions.add(result);
         }
 
-        this.currentSymbolTable.addSymbol(name.text, name.range, DynamicType.instance);
         this._references.assignmentStatements.push(result);
         return result;
     }
@@ -1309,8 +1257,6 @@ export class Parser {
             throw this.lastDiagnosticAsError();
         }
 
-        this.currentSymbolTable.addSymbol(name.text, name.range, DynamicType.instance);
-
         this.consumeStatementSeparators();
 
         let body = this.block(TokenKind.EndFor, TokenKind.Next);
@@ -1372,16 +1318,12 @@ export class Parser {
 
         let name = this.getNamespacedVariableNameExpression();
         //set the current namespace name
-        let result = new NamespaceStatement(keyword, name, null, null, this.currentSymbolTable);
-        this.currentNamespace = result;
+        let result = new NamespaceStatement(keyword, name, null, null);
 
         this.globalTerminators.push([TokenKind.EndNamespace]);
         let body = this.body();
         this.globalTerminators.pop();
 
-        //unset the current namespace name
-        this.currentNamespace = undefined;
-
         let endKeyword: Token;
         if (this.check(TokenKind.EndNamespace)) {
             endKeyword = this.advance();
@@ -1400,14 +1342,6 @@ export class Parser {
         //cache the range property so that plugins can't affect it
         result.cacheRange();
 
-        if (result.name) {
-            this.symbolTable.addSymbol(
-                result.name.split('.')[0],
-                result.nameExpression.range,
-                DynamicType.instance
-            );
-        }
-
         return result;
     }
 
@@ -1426,7 +1360,7 @@ export class Parser {
         if (firstIdentifier) {
             // force it into an identifier so the AST makes some sense
             firstIdentifier.kind = TokenKind.Identifier;
-            const varExpr = new VariableExpression(firstIdentifier, null);
+            const varExpr = new VariableExpression(firstIdentifier);
             expr = varExpr;
 
             //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
@@ -1493,10 +1427,7 @@ export class Parser {
             const: constToken,
             name: nameToken,
             equals: equalToken
-        }, expression, this.currentNamespaceName);
-        if (nameToken) {
-            this.currentSymbolTable.addSymbol(nameToken.text, nameToken.range, DynamicType.instance);
-        }
+        }, expression);
         this._references.constStatements.push(statement);
         return statement;
     }
@@ -1720,9 +1651,6 @@ export class Parser {
         } else {
             statement.tokens.endTry = this.advance();
         }
-        if (exceptionVarToken) {
-            this.currentSymbolTable.addSymbol(exceptionVarToken.text, exceptionVarToken.range, DynamicType.instance);
-        }
         return statement;
     }
 
@@ -1774,9 +1702,6 @@ export class Parser {
             });
         }
         let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
-        if (identifier) {
-            this.currentSymbolTable.addSymbol(identifier.text, identifier.range, DynamicType.instance);
-        }
         return new DimStatement(dim, identifier, leftSquareBracket, expressions, rightSquareBracket);
     }
 
@@ -2569,7 +2494,7 @@ export class Parser {
             TokenKind.RightParen
         );
 
-        let expression = new CallExpression(callee, openingParen, closingParen, args, this.currentNamespaceName);
+        let expression = new CallExpression(callee, openingParen, closingParen, args);
         if (addToCallExpressionList) {
             this.callExpressions.push(expression);
         }
@@ -2630,7 +2555,7 @@ export class Parser {
                 return this.templateString(true);
 
             case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
-                return new VariableExpression(this.previous() as Identifier, this.currentNamespaceName);
+                return new VariableExpression(this.previous() as Identifier);
 
             case this.match(TokenKind.LeftParen):
                 let left = this.previous();
@@ -2651,7 +2576,7 @@ export class Parser {
                 let token = Object.assign(this.previous(), {
                     kind: TokenKind.Identifier
                 }) as Identifier;
-                return new VariableExpression(token, this.currentNamespaceName);
+                return new VariableExpression(token);
 
             case this.checkAny(TokenKind.Function, TokenKind.Sub):
                 return this.anonymousFunction();
diff --git a/src/parser/Statement.spec.ts b/src/parser/Statement.spec.ts
index 594fc051..65cfa333 100644
--- a/src/parser/Statement.spec.ts
+++ b/src/parser/Statement.spec.ts
@@ -1,10 +1,9 @@
 import { expect } from 'chai';
 import type { NamespaceStatement } from './Statement';
-import { Body, ClassStatement, CommentStatement, EmptyStatement } from './Statement';
+import { Body, CommentStatement, EmptyStatement } from './Statement';
 import { ParseMode, Parser } from './Parser';
 import { WalkMode } from '../astUtils/visitors';
-import { CancellationTokenSource, Range } from 'vscode-languageserver';
-import { NamespacedVariableNameExpression, VariableExpression } from './Expression';
+import { CancellationTokenSource } from 'vscode-languageserver';
 import { Program } from '../Program';
 import * as path from 'path';
 import { trim } from '../testHelpers.spec';
@@ -64,26 +63,26 @@ describe('Statement', () => {
     });
 
     describe('ClassStatement', () => {
-        function create(name: string, namespaceName?: string) {
-            let stmt = new ClassStatement(
-                <any>{ range: Range.create(0, 0, 0, 0) },
-                <any>{ text: name },
-                null,
-                <any>{ range: Range.create(0, 0, 0, 0) },
-                null,
-                null,
-                namespaceName ? new NamespacedVariableNameExpression(new VariableExpression(<any>{ text: namespaceName }, null)) : null
-            );
-            return stmt;
-        }
         describe('getName', () => {
             it('handles null namespace name', () => {
-                let stmt = create('Animal');
+                const file = program.setFile<BrsFile>('source/lib.bs', `
+                    class Animal
+                    end class
+                `);
+                program.validate();
+                const stmt = file.parser.references.classStatements[0];
                 expect(stmt.getName(ParseMode.BrightScript)).to.equal('Animal');
                 expect(stmt.getName(ParseMode.BrighterScript)).to.equal('Animal');
             });
             it('handles namespaces', () => {
-                let stmt = create('Animal', 'NameA');
+                const file = program.setFile<BrsFile>('source/lib.bs', `
+                    namespace NameA
+                        class Animal
+                        end class
+                    end namespace
+                `);
+                program.validate();
+                const stmt = file.parser.references.classStatements[0];
                 expect(stmt.getName(ParseMode.BrightScript)).to.equal('NameA_Animal');
                 expect(stmt.getName(ParseMode.BrighterScript)).to.equal('NameA.Animal');
             });
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 79223b6d..4133ef2d 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -10,7 +10,7 @@ import type { BrsTranspileState } from './BrsTranspileState';
 import { ParseMode } from './Parser';
 import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
 import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors';
-import { isCallExpression, isCommentStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFieldStatement, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isTypedefProvider, isVoidType } from '../astUtils/reflection';
+import { isCallExpression, isCommentStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFieldStatement, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypedefProvider, isVoidType } from '../astUtils/reflection';
 import type { TranspileResult, TypedefProvider } from '../interfaces';
 import { createInvalidLiteral, createMethodStatement, createToken, interpolatedRange } from '../astUtils/creators';
 import { DynamicType } from '../types/DynamicType';
@@ -44,15 +44,12 @@ export class EmptyStatement extends Statement {
  */
 export class Body extends Statement implements TypedefProvider {
     constructor(
-        public statements: Statement[] = [],
-        public symbolTable = new SymbolTable()
+        public statements: Statement[] = []
     ) {
         super();
     }
 
-    public getSymbolTable() {
-        return this.symbolTable;
-    }
+    public symbolTable = new SymbolTable();
 
     public get range() {
         return util.createRangeFromPositions(
@@ -167,6 +164,8 @@ export class Block extends Statement {
 
     public readonly range: Range;
 
+    public symbolTable = new SymbolTable();
+
     transpile(state: BrsTranspileState) {
         state.blockDepth++;
         let results = [] as TranspileResult;
@@ -326,8 +325,7 @@ export class ExitWhileStatement extends Statement {
 export class FunctionStatement extends Statement implements TypedefProvider {
     constructor(
         public name: Identifier,
-        public func: FunctionExpression,
-        public namespaceName: NamespacedVariableNameExpression
+        public func: FunctionExpression
     ) {
         super();
         this.range = this.func.range;
@@ -339,16 +337,16 @@ export class FunctionStatement extends Statement implements TypedefProvider {
      * Get the name of this expression based on the parse mode
      */
     public getName(parseMode: ParseMode) {
-        if (this.namespaceName) {
+        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
+        if (namespace) {
             let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
-            let namespaceName = this.namespaceName.getName(parseMode);
-            return namespaceName + delimiter + this.name.text;
+            let namespaceName = namespace.getName(parseMode);
+            return namespaceName + delimiter + this.name?.text;
         } else {
             return this.name.text;
         }
     }
 
-
     transpile(state: BrsTranspileState) {
         //create a fake token using the full transpiled name
         let nameToken = {
@@ -1082,18 +1080,14 @@ export class NamespaceStatement extends Statement implements TypedefProvider {
         //this should technically only be a VariableExpression or DottedGetExpression, but that can be enforced elsewhere
         public nameExpression: NamespacedVariableNameExpression,
         public body: Body,
-        public endKeyword: Token,
-        readonly parentSymbolTable?: SymbolTable
+        public endKeyword: Token
     ) {
         super();
-        this.name = this.nameExpression.getName(ParseMode.BrighterScript);
-        this.symbolTable = new SymbolTable(parentSymbolTable);
+        this.name = this.getName(ParseMode.BrighterScript);
     }
 
-    public symbolTable: SymbolTable;
-
     public getSymbolTable() {
-        return this.symbolTable;
+        return this.body.symbolTable;
     }
 
     /**
@@ -1130,7 +1124,7 @@ export class NamespaceStatement extends Statement implements TypedefProvider {
     getTypedef(state: BrsTranspileState) {
         let result = [
             'namespace ',
-            ...this.nameExpression.getName(ParseMode.BrighterScript),
+            ...this.getName(ParseMode.BrighterScript),
             state.newline
         ];
         state.blockDepth++;
@@ -1216,8 +1210,7 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
         extendsToken: Token,
         public parentInterfaceName: NamespacedVariableNameExpression,
         public body: Statement[],
-        endInterfaceToken: Token,
-        public namespaceName: NamespacedVariableNameExpression
+        endInterfaceToken: Token
     ) {
         super();
         this.tokens.interface = interfaceToken;
@@ -1257,8 +1250,9 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
     public get fullName() {
         const name = this.tokens.name?.text;
         if (name) {
-            if (this.namespaceName) {
-                let namespaceName = this.namespaceName.getName(ParseMode.BrighterScript);
+            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
+            if (namespace) {
+                let namespaceName = namespace.getName(ParseMode.BrighterScript);
                 return `${namespaceName}.${name}`;
             } else {
                 return name;
@@ -1280,9 +1274,10 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
      * Get the name of this expression based on the parse mode
      */
     public getName(parseMode: ParseMode) {
-        if (this.namespaceName) {
+        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
+        if (namespace) {
             let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
-            let namespaceName = this.namespaceName.getName(parseMode);
+            let namespaceName = namespace.getName(parseMode);
             return namespaceName + delimiter + this.name;
         } else {
             return this.name;
@@ -1519,8 +1514,7 @@ export class ClassStatement extends Statement implements TypedefProvider {
         public body: Statement[],
         readonly end: Token,
         readonly extendsKeyword?: Token,
-        readonly parentClassName?: NamespacedVariableNameExpression,
-        readonly namespaceName?: NamespacedVariableNameExpression
+        readonly parentClassName?: NamespacedVariableNameExpression
     ) {
         super();
         this.body = this.body ?? [];
@@ -1541,8 +1535,9 @@ export class ClassStatement extends Statement implements TypedefProvider {
     public getName(parseMode: ParseMode) {
         const name = this.name?.text;
         if (name) {
-            if (this.namespaceName) {
-                let namespaceName = this.namespaceName.getName(parseMode);
+            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
+            if (namespace) {
+                let namespaceName = namespace.getName(parseMode);
                 let separator = parseMode === ParseMode.BrighterScript ? '.' : '_';
                 return namespaceName + separator + name;
             } else {
@@ -1587,9 +1582,10 @@ export class ClassStatement extends Statement implements TypedefProvider {
             this.name.text
         );
         if (this.extendsKeyword && this.parentClassName) {
+            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
             const fqName = util.getFullyQualifiedClassName(
                 this.parentClassName.getName(ParseMode.BrighterScript),
-                this.namespaceName?.getName(ParseMode.BrighterScript)
+                namespace?.getName(ParseMode.BrighterScript)
             );
             result.push(
                 ` extends ${fqName}`
@@ -1634,10 +1630,11 @@ export class ClassStatement extends Statement implements TypedefProvider {
         let stmt = this as ClassStatement;
         while (stmt) {
             if (stmt.parentClassName) {
+                const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
                 //find the parent class
                 stmt = state.file.getClassFileLink(
                     stmt.parentClassName.getName(ParseMode.BrighterScript),
-                    stmt.namespaceName?.getName(ParseMode.BrighterScript)
+                    namespace?.getName(ParseMode.BrighterScript)
                 )?.item;
                 myIndex++;
             } else {
@@ -1665,9 +1662,10 @@ export class ClassStatement extends Statement implements TypedefProvider {
         let stmt = this as ClassStatement;
         while (stmt) {
             if (stmt.parentClassName) {
+                const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
                 stmt = state.file.getClassFileLink(
                     stmt.parentClassName.getName(ParseMode.BrighterScript),
-                    this.namespaceName?.getName(ParseMode.BrighterScript)
+                    namespace?.getName(ParseMode.BrighterScript)
                 )?.item;
                 ancestors.push(stmt);
             } else {
@@ -1725,9 +1723,10 @@ export class ClassStatement extends Statement implements TypedefProvider {
 
         //construct parent class or empty object
         if (ancestors[0]) {
+            const ancestorNamespace = ancestors[0].findAncestor<NamespaceStatement>(isNamespaceStatement);
             let fullyQualifiedClassName = util.getFullyQualifiedClassName(
                 ancestors[0].getName(ParseMode.BrighterScript),
-                ancestors[0].namespaceName?.getName(ParseMode.BrighterScript)
+                ancestorNamespace?.getName(ParseMode.BrighterScript)
             );
             result.push(
                 'instance = ',
@@ -1883,7 +1882,7 @@ export class MethodStatement extends FunctionStatement {
         func: FunctionExpression,
         public override: Token
     ) {
-        super(name, func, undefined);
+        super(name, func);
         if (modifiers) {
             if (Array.isArray(modifiers)) {
                 this.modifiers.push(...modifiers);
@@ -1905,6 +1904,13 @@ export class MethodStatement extends FunctionStatement {
 
     public readonly range: Range;
 
+    /**
+     * Get the name of this method.
+     */
+    public getName(parseMode: ParseMode) {
+        return this.name.text;
+    }
+
     transpile(state: BrsTranspileState) {
         if (this.name.text.toLowerCase() === 'new') {
             this.ensureSuperConstructorCall(state);
@@ -1992,8 +1998,7 @@ export class MethodStatement extends FunctionStatement {
                         isReserved: false,
                         range: state.classStatement.name.range,
                         leadingWhitespace: ''
-                    },
-                    null
+                    }
                 ),
                 {
                     kind: TokenKind.LeftParen,
@@ -2009,8 +2014,7 @@ export class MethodStatement extends FunctionStatement {
                     range: state.classStatement.name.range,
                     leadingWhitespace: ''
                 },
-                [],
-                null
+                []
             )
         );
         state.editor.arrayUnshift(this.func.body.statements, superCall);
@@ -2261,8 +2265,7 @@ export class EnumStatement extends Statement implements TypedefProvider {
             name: Identifier;
             endEnum: Token;
         },
-        public body: Array<EnumMemberStatement | CommentStatement>,
-        public namespaceName?: NamespacedVariableNameExpression
+        public body: Array<EnumMemberStatement | CommentStatement>
     ) {
         super();
         this.body = this.body ?? [];
@@ -2334,8 +2337,10 @@ export class EnumStatement extends Statement implements TypedefProvider {
     public get fullName() {
         const name = this.tokens.name?.text;
         if (name) {
-            if (this.namespaceName) {
-                let namespaceName = this.namespaceName.getName(ParseMode.BrighterScript);
+            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
+
+            if (namespace) {
+                let namespaceName = namespace.getName(ParseMode.BrighterScript);
                 return `${namespaceName}.${name}`;
             } else {
                 return name;
@@ -2452,8 +2457,7 @@ export class ConstStatement extends Statement implements TypedefProvider {
             name: Identifier;
             equals: Token;
         },
-        public value: Expression,
-        readonly namespaceName?: NamespacedVariableNameExpression
+        public value: Expression
     ) {
         super();
         this.range = util.createBoundingRange(this.tokens.const, this.tokens.name, this.tokens.equals, this.value);
@@ -2471,8 +2475,9 @@ export class ConstStatement extends Statement implements TypedefProvider {
     public get fullName() {
         const name = this.tokens.name?.text;
         if (name) {
-            if (this.namespaceName) {
-                let namespaceName = this.namespaceName.getName(ParseMode.BrighterScript);
+            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
+            if (namespace) {
+                let namespaceName = namespace.getName(ParseMode.BrighterScript);
                 return `${namespaceName}.${name}`;
             } else {
                 return name;
diff --git a/src/util.ts b/src/util.ts
index 1215d763..02f63ba8 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -26,7 +26,7 @@ import type { DottedGetExpression, VariableExpression } from './parser/Expressio
 import { Logger, LogLevel } from './Logger';
 import type { Identifier, Locatable, Token } from './lexer/Token';
 import { TokenKind } from './lexer/TokenKind';
-import { isBrsFile, isCallExpression, isCallfuncExpression, isDottedGetExpression, isExpression, isIndexedGetExpression, isNamespacedVariableNameExpression, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
+import { isBrsFile, isCallExpression, isCallfuncExpression, isDottedGetExpression, isExpression, isIndexedGetExpression, isNamespacedVariableNameExpression, isNewExpression, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
 import { WalkMode } from './astUtils/visitors';
 import { CustomType } from './types/CustomType';
 import { SourceNode } from 'source-map';
@@ -1406,6 +1406,7 @@ export class Util {
         while (nextPart) {
             if (isDottedGetExpression(nextPart) || isIndexedGetExpression(nextPart) || isXmlAttributeGetExpression(nextPart)) {
                 nextPart = nextPart.obj;
+
             } else if (isCallExpression(nextPart) || isCallfuncExpression(nextPart)) {
                 nextPart = nextPart.callee;
 
@@ -1419,6 +1420,43 @@ export class Util {
         return parts;
     }
 
+    /**
+     * Break an expression into each part, and return any VariableExpression or DottedGet expresisons from left-to-right.
+     */
+    public getDottedGetPath(expression: Expression): [VariableExpression, ...DottedGetExpression[]] {
+        let parts: Expression[] = [];
+        let nextPart = expression;
+        while (nextPart) {
+            if (isDottedGetExpression(nextPart)) {
+                parts.unshift(nextPart);
+                nextPart = nextPart.obj;
+
+            } else if (isIndexedGetExpression(nextPart) || isXmlAttributeGetExpression(nextPart)) {
+                nextPart = nextPart.obj;
+                parts = [];
+
+            } else if (isCallExpression(nextPart) || isCallfuncExpression(nextPart)) {
+                nextPart = nextPart.callee;
+                parts = [];
+
+            } else if (isNewExpression(nextPart)) {
+                nextPart = nextPart.call.callee;
+                parts = [];
+
+            } else if (isNamespacedVariableNameExpression(nextPart)) {
+                nextPart = nextPart.expression;
+
+            } else if (isVariableExpression(nextPart)) {
+                parts.unshift(nextPart);
+                break;
+            } else {
+                parts = [];
+                break;
+            }
+        }
+        return parts as any;
+    }
+
     /**
      * Returns an integer if valid, or undefined. Eliminates checking for NaN
      */
diff --git a/src/validators/ClassValidator.ts b/src/validators/ClassValidator.ts
index 239cba02..aa5d11b1 100644
--- a/src/validators/ClassValidator.ts
+++ b/src/validators/ClassValidator.ts
@@ -2,11 +2,11 @@ import type { Scope } from '../Scope';
 import { DiagnosticMessages } from '../DiagnosticMessages';
 import type { CallExpression } from '../parser/Expression';
 import { ParseMode } from '../parser/Parser';
-import type { ClassStatement, MethodStatement } from '../parser/Statement';
+import type { ClassStatement, MethodStatement, NamespaceStatement } from '../parser/Statement';
 import { CancellationTokenSource } from 'vscode-languageserver';
 import { URI } from 'vscode-uri';
 import util from '../util';
-import { isCallExpression, isFieldStatement, isMethodStatement, isCustomType } from '../astUtils/reflection';
+import { isCallExpression, isFieldStatement, isMethodStatement, isCustomType, isNamespaceStatement } from '../astUtils/reflection';
 import type { BscFile, BsDiagnostic } from '../interfaces';
 import { createVisitor, WalkMode } from '../astUtils/visitors';
 import type { BrsFile } from '../files/BrsFile';
@@ -60,14 +60,15 @@ export class BsClassValidator {
             let newExpressions = file.parser.references.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,
-                    newExpression.namespaceName?.getName(ParseMode.BrighterScript)
+                    namespaceName
                 );
 
                 if (!newableClass) {
                     //try and find functions with this name.
-                    let fullName = util.getFullyQualifiedClassName(className, newExpression.namespaceName?.getName(ParseMode.BrighterScript));
+                    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) {
@@ -89,7 +90,8 @@ export class BsClassValidator {
         for (const [className, classStatement] of this.classes) {
             //catch namespace class collision with global class
             let nonNamespaceClass = this.classes.get(util.getTextAfterFinalDot(className).toLowerCase());
-            if (classStatement.namespaceName && nonNamespaceClass) {
+            const namespace = classStatement.findAncestor<NamespaceStatement>(isNamespaceStatement);
+            if (namespace && nonNamespaceClass) {
                 this.diagnostics.push({
                     ...DiagnosticMessages.namespacedClassCannotShareNamewithNonNamespacedClass(
                         nonNamespaceClass.name.text
@@ -303,7 +305,8 @@ export class BsClassValidator {
                         const fieldTypeName = fieldType.name;
                         const lowerFieldTypeName = fieldTypeName?.toLowerCase();
                         if (lowerFieldTypeName) {
-                            const currentNamespaceName = classStatement.namespaceName?.getName(ParseMode.BrighterScript);
+                            const namespace = classStatement.findAncestor<NamespaceStatement>(isNamespaceStatement);
+                            const currentNamespaceName = namespace?.getName(ParseMode.BrighterScript);
                             //check if this custom type is in our class map
                             if (!this.getClassByName(lowerFieldTypeName, currentNamespaceName) && !this.scope.hasInterface(lowerFieldTypeName) && !this.scope.hasEnum(lowerFieldTypeName)) {
                                 this.diagnostics.push({
@@ -402,8 +405,9 @@ export class BsClassValidator {
                     //compute the relative name of the parent class and prepend the current class's namespace
                     //to the beginning of the parent class's name
                 } else {
-                    if (classStatement.namespaceName) {
-                        absoluteName = `${classStatement.namespaceName.getName(ParseMode.BrighterScript)}.${parentClassName}`;
+                    const namespace = classStatement.findAncestor<NamespaceStatement>(isNamespaceStatement);
+                    if (namespace) {
+                        absoluteName = `${namespace.getName(ParseMode.BrighterScript)}.${parentClassName}`;
                     } else {
                         absoluteName = parentClassName;
                     }


Test patch:
diff --git a/src/files/tests/optionalChaning.spec.ts b/src/files/tests/optionalChaning.spec.ts
index b788d847..16f0343d 100644
--- a/src/files/tests/optionalChaning.spec.ts
+++ b/src/files/tests/optionalChaning.spec.ts
@@ -55,7 +55,8 @@ describe('optional chaining', () => {
     it(`transpiles '?@`, () => {
         testTranspile(`
             sub main()
-                print xmlThing?@someAttr
+                someXml = invalid
+                print someXml?@someAttr
             end sub
         `);
     });
diff --git a/src/parser/tests/statement/Enum.spec.ts b/src/parser/tests/statement/Enum.spec.ts
index cd623558..c94a0f2e 100644
--- a/src/parser/tests/statement/Enum.spec.ts
+++ b/src/parser/tests/statement/Enum.spec.ts
@@ -181,7 +181,7 @@ describe('EnumStatement', () => {
     });
 
     it('allows enum in namespace', () => {
-        const parser = Parser.parse(`
+        const file = program.setFile<BrsFile>('source/types.bs', `
             namespace entities
                 enum Person
                     name
@@ -191,10 +191,11 @@ describe('EnumStatement', () => {
             enum Direction
                 up
             end enum
-        `, { mode: ParseMode.BrighterScript });
+        `);
+        program.validate();
 
-        expectZeroDiagnostics(parser);
-        expect(parser.references.enumStatements.map(x => x.fullName)).to.eql([
+        expectZeroDiagnostics(program);
+        expect(file.parser.references.enumStatements.map(x => x.fullName)).to.eql([
             'entities.Person',
             'Direction'
         ]);
@@ -754,6 +755,7 @@ describe('EnumStatement', () => {
                     end enum
                 end namespace
             `);
+            program.validate();
             //      enums.direction.|down
             expectCompletionsIncludes(program.getCompletions('source/main.bs', util.createPosition(2, 36)), [{
                 label: 'up',
@@ -792,6 +794,7 @@ describe('EnumStatement', () => {
                     end enum
                 end namespace
             `);
+            program.validate();
             //should NOT find Direction because it's not directly available at the top level (you need to go through `enums.` to get at it)
             //      dire|ction.down
             expectCompletionsExcludes(program.getCompletions('source/main.bs', util.createPosition(2, 24)), [{