Repository: rokucommunity/brighterscript
Pull request #1021 (2024-01-17 21:08:03)

Remove `Parser.references`
Basically, removes all the hard-coded/static lists in `Parser.references` and instead uses cached AST walks, that are invalidated during BRS File Validation time... At that point in time, all changes from plugins should be done, and we can trust the walk for finding quick lookups for various types of expressions/statements.



markwpearce: Fixes #1006

Patch:
diff --git a/src/Program.spec.ts b/src/Program.spec.ts
index 3df3de6e..f01c3dee 100644
--- a/src/Program.spec.ts
+++ b/src/Program.spec.ts
@@ -252,9 +252,9 @@ describe('Program', () => {
                 end sub
             `);
             //disable the plugins
-            expect(file.parser.references.expressions).to.be.lengthOf(1);
+            expect(file['_cachedLookups'].expressions).to.be.lengthOf(1);
             program.validate();
-            expect(file.parser.references.expressions).to.be.lengthOf(1);
+            expect(file['_cachedLookups'].expressions).to.be.lengthOf(1);
         });
         it('catches duplicate XML component names', () => {
             //add 2 components which both reference the same errored file
diff --git a/src/Program.ts b/src/Program.ts
index 42de4a9d..85ec9eca 100644
--- a/src/Program.ts
+++ b/src/Program.ts
@@ -20,7 +20,7 @@ import { parseManifest, getBsConst } from './preprocessor/Manifest';
 import { URI } from 'vscode-uri';
 import PluginInterface from './PluginInterface';
 import { isBrsFile, isXmlFile, isXmlScope, isNamespaceStatement } from './astUtils/reflection';
-import type { FunctionStatement, NamespaceStatement } from './parser/Statement';
+import type { FunctionStatement, MethodStatement, NamespaceStatement } from './parser/Statement';
 import { BscPlugin } from './bscPlugin/BscPlugin';
 import { Editor } from './astUtils/Editor';
 import type { Statement } from './parser/AstNode';
@@ -50,6 +50,7 @@ import type { BscType } from './types';
 import { InterfaceType } from './types';
 import { BuiltInInterfaceAdder } from './types/BuiltInInterfaceAdder';
 import type { UnresolvedSymbol } from './AstValidationSegmenter';
+import { WalkMode, createVisitor } from './astUtils/visitors';
 
 const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`;
 const bslibAliasedRokuModulesPkgPath = s`source/roku_modules/bslib/bslib.brs`;
@@ -1099,6 +1100,16 @@ export class Program {
         const filesSearched = new Set<BrsFile>();
         let lowerNamespaceName = namespaceName?.toLowerCase();
         let lowerName = name?.toLowerCase();
+
+        function addToResults(statement: FunctionStatement | MethodStatement, file: BrsFile) {
+            let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
+            if (statement.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
+                if (!results.has(statement)) {
+                    results.set(statement, { item: statement, file: file as BrsFile });
+                }
+            }
+        }
+
         //look through all files in scope for matches
         for (const scope of this.getScopesForFile(originFile)) {
             for (const file of scope.getAllFiles()) {
@@ -1108,14 +1119,16 @@ 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.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
-                    if (statement.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
-                        if (!results.has(statement)) {
-                            results.set(statement, { item: statement, file: file });
-                        }
+                file.ast.walk(createVisitor({
+                    FunctionStatement: (statement: FunctionStatement) => {
+                        addToResults(statement, file);
+                    },
+                    MethodStatement: (statement: MethodStatement) => {
+                        addToResults(statement, file);
                     }
-                }
+                }), {
+                    walkMode: WalkMode.visitStatements
+                });
             }
         }
         return [...results.values()];
@@ -1145,13 +1158,17 @@ export class Program {
             }
             filesSearched.add(file);
 
-            for (const statement of file.parser.references.functionStatements) {
-                if (funcNames.has(statement.name.text)) {
-                    if (!results.has(statement)) {
-                        results.set(statement, { item: statement, file: file });
+            file.ast.walk(createVisitor({
+                FunctionStatement: (statement: FunctionStatement) => {
+                    if (funcNames.has(statement.name.text)) {
+                        if (!results.has(statement)) {
+                            results.set(statement, { item: statement, file: file });
+                        }
                     }
                 }
-            }
+            }), {
+                walkMode: WalkMode.visitStatements
+            });
         }
         return [...results.values()];
     }
@@ -1584,7 +1601,8 @@ export class Program {
             if (isBrsFile(file)) {
                 //TODO handle namespace-relative function calls
                 //if the file has a function with this name
-                if (file.parser.references.functionStatementLookup.get(lowerFunctionName) !== undefined) {
+                // eslint-disable-next-line @typescript-eslint/dot-notation
+                if (file['_cachedLookups'].functionStatementMap.get(lowerFunctionName)) {
                     files.push(file);
                 }
             }
@@ -1603,7 +1621,9 @@ export class Program {
             if (isBrsFile(file)) {
                 //TODO handle namespace-relative classes
                 //if the file has a function with this name
-                if (file.parser.references.classStatementLookup.get(lowerClassName) !== undefined) {
+
+                // eslint-disable-next-line @typescript-eslint/dot-notation
+                if (file['_cachedLookups'].classStatementMap.get(lowerClassName) !== undefined) {
                     files.push(file);
                 }
             }
@@ -1617,7 +1637,9 @@ export class Program {
         //find every file with this class defined
         for (const file of Object.values(this.files)) {
             if (isBrsFile(file)) {
-                if (file.parser.references.namespaceStatements.find((x) => {
+
+                // eslint-disable-next-line @typescript-eslint/dot-notation
+                if (file['_cachedLookups'].namespaceStatements.find((x) => {
                     const namespaceName = x.name.toLowerCase();
                     return (
                         //the namespace name matches exactly
@@ -1630,16 +1652,18 @@ export class Program {
                 }
             }
         }
+
         return files;
     }
 
     public findFilesForEnum(name: string) {
         const files = [] as BscFile[];
         const lowerName = name.toLowerCase();
-        //find every file with this class defined
+        //find every file with this enum defined
         for (const file of Object.values(this.files)) {
             if (isBrsFile(file)) {
-                if (file.parser.references.enumStatementLookup.get(lowerName)) {
+                // eslint-disable-next-line @typescript-eslint/dot-notation
+                if (file['_cachedLookups'].enumStatementMap.get(lowerName)) {
                     files.push(file);
                 }
             }
diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts
index 852b5a18..c9058192 100644
--- a/src/Scope.spec.ts
+++ b/src/Scope.spec.ts
@@ -87,7 +87,7 @@ describe('Scope', () => {
         program.validate();
         const scope = program.getScopesForFile('source/alpha.bs')[0];
         scope.linkSymbolTable();
-        const symbolTable = file.parser.references.namespaceStatements[1].body.getSymbolTable();
+        const symbolTable = file.ast.findChildren<NamespaceStatement>(isNamespaceStatement)[1].body.getSymbolTable();
         //the symbol table should contain the relative names for all items in this namespace across the entire scope
         expect(
             // eslint-disable-next-line no-bitwise
diff --git a/src/Scope.ts b/src/Scope.ts
index ec91326b..ecab8f7d 100644
--- a/src/Scope.ts
+++ b/src/Scope.ts
@@ -247,7 +247,6 @@ export class Scope {
         return result;
     }
 
-
     public getAllFileLinks(name: string, containingNamespace?: string): FileLink<Statement>[] {
         let links: FileLink<Statement>[] = [];
 
@@ -329,7 +328,7 @@ export class Scope {
             const map = new Map<string, FileLink<ClassStatement>>();
             this.enumerateBrsFiles((file) => {
                 if (isBrsFile(file)) {
-                    for (let cls of file.parser.references.classStatements) {
+                    for (let cls of file['_cachedLookups'].classStatements) {
                         const className = cls.getName(ParseMode.BrighterScript);
                         //only track classes with a defined name (i.e. exclude nameless malformed classes)
                         if (className) {
@@ -351,7 +350,7 @@ export class Scope {
             const map = new Map<string, FileLink<InterfaceStatement>>();
             this.enumerateBrsFiles((file) => {
                 if (isBrsFile(file)) {
-                    for (let iface of file.parser.references.interfaceStatements) {
+                    for (let iface of file['_cachedLookups'].interfaceStatements) {
                         const ifaceName = iface.getName(ParseMode.BrighterScript);
                         //only track classes with a defined name (i.e. exclude nameless malformed classes)
                         if (ifaceName) {
@@ -372,7 +371,7 @@ export class Scope {
         return this.cache.getOrAdd('enumMap', () => {
             const map = new Map<string, FileLink<EnumStatement>>();
             this.enumerateBrsFiles((file) => {
-                for (let enumStmt of file.parser.references.enumStatements) {
+                for (let enumStmt of file['_cachedLookups'].enumStatements) {
                     //only track enums with a defined name (i.e. exclude nameless malformed enums)
                     if (enumStmt.fullName) {
                         map.set(enumStmt.fullName.toLowerCase(), { item: enumStmt, file: file });
@@ -391,7 +390,7 @@ export class Scope {
         return this.cache.getOrAdd('constMap', () => {
             const map = new Map<string, FileLink<ConstStatement>>();
             this.enumerateBrsFiles((file) => {
-                for (let stmt of file.parser.references.constStatements) {
+                for (let stmt of file['_cachedLookups'].constStatements) {
                     //only track enums with a defined name (i.e. exclude nameless malformed enums)
                     if (stmt.fullName) {
                         map.set(stmt.fullName.toLowerCase(), { item: stmt, file: file });
@@ -426,7 +425,7 @@ export class Scope {
     public isKnownNamespace(namespaceName: string) {
         let namespaceNameLower = namespaceName.toLowerCase();
         this.enumerateBrsFiles((file) => {
-            for (let namespace of file.parser.references.namespaceStatements) {
+            for (let namespace of file['_cachedLookups'].namespaceStatements) {
                 let loopNamespaceNameLower = namespace.name.toLowerCase();
                 if (loopNamespaceNameLower === namespaceNameLower || loopNamespaceNameLower.startsWith(namespaceNameLower + '.')) {
                     return true;
@@ -680,7 +679,7 @@ export class Scope {
     public getAllNamespaceStatements() {
         let result = [] as NamespaceStatement[];
         this.enumerateBrsFiles((file) => {
-            result.push(...file.parser.references.namespaceStatements);
+            result.push(...file['_cachedLookups'].namespaceStatements);
         });
         return result;
     }
@@ -912,7 +911,7 @@ export class Scope {
 
     private detectVariableNamespaceCollisions(file: BrsFile) {
         //find all function parameters
-        for (let func of file.parser.references.functionExpressions) {
+        for (let func of file['_cachedLookups'].functionExpressions) {
             for (let param of func.parameters) {
                 let lowerParamName = param.name.text.toLowerCase();
                 let namespace = this.getNamespace(lowerParamName, param.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase());
@@ -935,7 +934,7 @@ export class Scope {
             }
         }
 
-        for (let assignment of file.parser.references.assignmentStatements) {
+        for (let assignment of file['_cachedLookups'].assignmentStatements) {
             let lowerAssignmentName = assignment.name.text.toLowerCase();
             let namespace = this.getNamespace(lowerAssignmentName, assignment.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase());
             //see if the param matches any starting namespace part
@@ -1080,7 +1079,7 @@ export class Scope {
     public getNewExpressions() {
         let result = [] as AugmentedNewExpression[];
         this.enumerateBrsFiles((file) => {
-            let expressions = file.parser.references.newExpressions as AugmentedNewExpression[];
+            let expressions = file['_cachedLookups'].newExpressions as AugmentedNewExpression[];
             for (let expression of expressions) {
                 expression.file = file;
                 result.push(expression);
diff --git a/src/astUtils/CachedLookups.ts b/src/astUtils/CachedLookups.ts
new file mode 100644
index 00000000..c1d68049
--- /dev/null
+++ b/src/astUtils/CachedLookups.ts
@@ -0,0 +1,354 @@
+import type { AALiteralExpression, CallExpression, CallfuncExpression, DottedGetExpression, FunctionExpression, NewExpression, VariableExpression } from '../parser/Expression';
+import type { AssignmentStatement, ClassStatement, ConstStatement, EnumStatement, FunctionStatement, ImportStatement, InterfaceStatement, LibraryStatement, NamespaceStatement } from '../parser/Statement';
+import { Cache } from '../Cache';
+import { WalkMode, createVisitor } from './visitors';
+import type { Expression } from '../parser/AstNode';
+import { isAAMemberExpression, isBinaryExpression, isCallExpression, isCommentStatement, isDottedGetExpression, isFunctionExpression, isIndexedGetExpression, isMethodStatement, isNamespaceStatement, isNewExpression, isVariableExpression } from './reflection';
+import type { Parser } from '../parser/Parser';
+import { ParseMode } from '../parser/Parser';
+import type { Token } from '../lexer/Token';
+import { isToken } from '../lexer/Token';
+import type { Program } from '../Program';
+import util from '../util';
+
+
+export interface BscFileLike {
+    program: Program;
+    _parser: Parser;
+}
+
+export class CachedLookups {
+
+    private cache = new Cache();
+
+    constructor(private file: BscFileLike) { }
+
+    get namespaceStatements(): NamespaceStatement[] {
+        return this.getFromCache<Array<NamespaceStatement>>('namespaceStatements');
+    }
+
+    get functionStatements(): FunctionStatement[] {
+        return this.getFromCache<Array<FunctionStatement>>('functionStatements');
+    }
+
+    get functionStatementMap() {
+        return this.cache.getOrAdd('functionStatementMap', () => {
+            const funcMap = new Map<string, FunctionStatement>();
+            for (const stmt of this.functionStatements) {
+                funcMap.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
+            }
+            return funcMap;
+        });
+    }
+
+    get functionExpressions(): FunctionExpression[] {
+        return this.getFromCache<Array<FunctionExpression>>('functionExpressions');
+    }
+
+    get libraryStatements(): LibraryStatement[] {
+        return this.getFromCache<Array<LibraryStatement>>('libraryStatements');
+    }
+
+    get importStatements(): ImportStatement[] {
+        return this.getFromCache<Array<ImportStatement>>('importStatements');
+    }
+
+    /**
+     * A collection of full expressions. This excludes intermediary expressions.
+     *
+     * Example 1:
+     * `a.b.c` is composed of `a` (variableExpression)  `.b` (DottedGetExpression) `.c` (DottedGetExpression)
+     * This will only contain the final `.c` DottedGetExpression because `.b` and `a` can both be derived by walking back from the `.c` DottedGetExpression.
+     *
+     * Example 2:
+     * `name.space.doSomething(a.b.c)` will result in 2 entries in this list. the `CallExpression` for `doSomething`, and the `.c` DottedGetExpression.
+     *
+     * Example 3:
+     * `value = SomeEnum.value > 2 or SomeEnum.otherValue < 10` will result in 4 entries. `SomeEnum.value`, `2`, `SomeEnum.otherValue`, `10`
+     */
+    get expressions(): Set<Expression> {
+        return this.getFromCache<Set<Expression>>('expressions');
+    }
+
+    get newExpressions(): NewExpression[] {
+        return this.getFromCache<Array<NewExpression>>('newExpressions');
+    }
+
+    get classStatements(): ClassStatement[] {
+        return this.getFromCache<Array<ClassStatement>>('classStatements');
+    }
+
+    get classStatementMap() {
+        return this.cache.getOrAdd('classStatementMap', () => {
+            const classMap = new Map<string, ClassStatement>();
+            for (const stmt of this.classStatements) {
+                classMap.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
+            }
+            return classMap;
+        });
+    }
+
+    get assignmentStatements(): AssignmentStatement[] {
+        return this.getFromCache<Array<AssignmentStatement>>('assignmentStatements');
+    }
+
+    get enumStatements(): EnumStatement[] {
+        return this.getFromCache<Array<EnumStatement>>('enumStatements');
+    }
+
+    get enumStatementMap() {
+        return this.cache.getOrAdd('enumStatementMap', () => {
+            const enumMap = new Map<string, EnumStatement>();
+            for (const stmt of this.enumStatements) {
+                enumMap.set(stmt.fullName.toLowerCase(), stmt);
+            }
+            return enumMap;
+        });
+    }
+
+    get constStatements(): ConstStatement[] {
+        return this.getFromCache<Array<ConstStatement>>('constStatements');
+    }
+
+    get constStatementMap() {
+        return this.cache.getOrAdd('constStatementMap', () => {
+            const constMap = new Map<string, ConstStatement>();
+            for (const stmt of this.constStatements) {
+                constMap.set(stmt.fullName.toLowerCase(), stmt);
+            }
+            return constMap;
+        });
+    }
+
+    get interfaceStatements(): InterfaceStatement[] {
+        return this.getFromCache<Array<InterfaceStatement>>('interfaceStatements');
+    }
+
+    get interfaceStatementMap() {
+        return this.cache.getOrAdd('interfaceStatementMap', () => {
+            const ifaceMap = new Map<string, InterfaceStatement>();
+            for (const stmt of this.interfaceStatements) {
+                ifaceMap.set(stmt.fullName.toLowerCase(), stmt);
+            }
+            return ifaceMap;
+        });
+    }
+
+    get propertyHints(): Record<string, string> {
+        return this.getFromCache<Record<string, string>>('propertyHints');
+    }
+
+
+    invalidate() {
+        this.cache.clear();
+    }
+
+
+    private getFromCache<T>(cacheKey: string): T {
+        if (!this.cache.has(cacheKey)) {
+            this.rehydrate();
+        }
+        if (!this.cache.has(cacheKey)) {
+            this.file.program.logger.info('Unable to get cached lookup', cacheKey);
+        }
+        return this.cache.get(cacheKey);
+    }
+
+    private rehydrate() {
+        const expressions = new Set<Expression>();
+        const classStatements: ClassStatement[] = [];
+        const namespaceStatements: NamespaceStatement[] = [];
+        const enumStatements: EnumStatement[] = [];
+        const constStatements: ConstStatement[] = [];
+        const interfaceStatements: InterfaceStatement[] = [];
+        const assignmentStatements: AssignmentStatement[] = [];
+        const libraryStatements: LibraryStatement[] = [];
+        const importStatements: ImportStatement[] = [];
+        const functionStatements: FunctionStatement[] = [];
+        const functionExpressions: FunctionExpression[] = [];
+        const newExpressions: NewExpression[] = [];
+
+        const propertyHints: Record<string, string> = {};
+        const addPropertyHints = (item: Token | AALiteralExpression) => {
+            if (isToken(item)) {
+                const name = item.text;
+                propertyHints[name.toLowerCase()] = name;
+            } else {
+                for (const member of item.elements) {
+                    if (!isCommentStatement(member)) {
+                        const name = member.keyToken.text;
+                        if (!name.startsWith('"')) {
+                            propertyHints[name.toLowerCase()] = name;
+                        }
+                    }
+                }
+            }
+        };
+
+        const excludedExpressions = new Set<Expression>();
+        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
+            for (const p of e.args) {
+                expressions.add(p);
+            }
+            //add calls that were not excluded (from loop below)
+            if (!excludedExpressions.has(e)) {
+                expressions.add(e);
+            }
+
+            //if this call is part of a longer expression that includes a call higher up, find that higher one and remove it
+            if (e.callee) {
+                let node: Expression = e.callee;
+                while (node) {
+                    //the primary goal for this loop. If we found a parent call expression, remove it from `references`
+                    if (isCallExpression(node)) {
+                        expressions.delete(node);
+                        excludedExpressions.add(node);
+                        //stop here. even if there are multiple calls in the chain, each child will find and remove its closest parent, so that reduces excess walking.
+                        break;
+
+                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
+                    } else if (isVariableExpression(node)) {
+                        break;
+                        //if
+
+                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
+                        node = node.obj;
+                    } else {
+                        //some expression we don't understand. log it and quit the loop
+                        this.file.program.logger.info('Encountered unknown expression while calculating function expression chain', node);
+                        break;
+                    }
+                }
+            }
+        };
+        const visitVariableNameExpression = (e: VariableExpression | DottedGetExpression) => {
+            if (!isDottedGetExpression(e.parent) && // not last expression in a dotted-get chain
+                !isFunctionExpression(e) && // don't include function expressions
+                !isNamespaceStatement(e.parent) && // don't include the name of namespace
+                !isNewExpression(e.parent) && // don't include the inside of a new expression
+                !(isCallExpression(e.parent) && e.parent?.callee === e) && // don't include the callee
+                !util.isInTypeExpression(e)) {
+                expressions.add(e);
+            }
+        };
+
+        // eslint-disable-next-line @typescript-eslint/dot-notation
+        this.file['_parser']?.ast.walk(createVisitor({
+            AssignmentStatement: s => {
+                assignmentStatements.push(s);
+                expressions.add(s.value);
+            },
+            ClassStatement: s => {
+                classStatements.push(s);
+            },
+            InterfaceStatement: s => {
+                interfaceStatements.push(s);
+            },
+            FieldStatement: s => {
+                if (s.initialValue) {
+                    expressions.add(s.initialValue);
+                }
+            },
+            NamespaceStatement: s => {
+                namespaceStatements.push(s);
+            },
+            FunctionStatement: s => {
+                functionStatements.push(s);
+            },
+            ImportStatement: s => {
+                importStatements.push(s);
+            },
+            LibraryStatement: s => {
+                libraryStatements.push(s);
+            },
+            FunctionExpression: (expression, parent) => {
+                if (!isMethodStatement(parent)) {
+                    functionExpressions.push(expression);
+                }
+            },
+            NewExpression: e => {
+                newExpressions.push(e);
+                for (const p of e.call.args) {
+                    expressions.add(p);
+                }
+            },
+            ExpressionStatement: s => {
+                expressions.add(s.expression);
+            },
+            CallfuncExpression: e => {
+                visitCallExpression(e);
+            },
+            CallExpression: e => {
+                visitCallExpression(e);
+            },
+            AALiteralExpression: e => {
+                addPropertyHints(e);
+                expressions.add(e);
+                for (const member of e.elements) {
+                    if (isAAMemberExpression(member)) {
+                        expressions.add(member.value);
+                    }
+                }
+            },
+            BinaryExpression: (e, parent) => {
+                //walk the chain of binary expressions and add each one to the list of expressions
+                const expressionsParts: Expression[] = [e];
+                let expression: Expression;
+                while ((expression = expressionsParts.pop())) {
+                    if (isBinaryExpression(expression)) {
+                        expressionsParts.push(expression.left, expression.right);
+                    } else {
+                        expressions.add(expression);
+                    }
+                }
+            },
+            ArrayLiteralExpression: e => {
+                for (const element of e.elements) {
+                    //keep everything except comments
+                    if (!isCommentStatement(element)) {
+                        expressions.add(element);
+                    }
+                }
+            },
+            DottedGetExpression: e => {
+                visitVariableNameExpression(e);
+                addPropertyHints(e.name);
+            },
+            DottedSetStatement: e => {
+                addPropertyHints(e.name);
+            },
+            EnumStatement: e => {
+                enumStatements.push(e);
+            },
+            ConstStatement: s => {
+                constStatements.push(s);
+            },
+            UnaryExpression: e => {
+                expressions.add(e);
+            },
+            IncrementStatement: e => {
+                expressions.add(e);
+            },
+            VariableExpression: e => {
+                visitVariableNameExpression(e);
+            }
+        }), {
+            walkMode: WalkMode.visitAllRecursive
+        });
+
+        this.cache.set('expressions', expressions);
+        this.cache.set('classStatements', classStatements);
+        this.cache.set('namespaceStatements', namespaceStatements);
+        this.cache.set('namespaceStatements', namespaceStatements);
+        this.cache.set('enumStatements', enumStatements);
+        this.cache.set('constStatements', constStatements);
+        this.cache.set('interfaceStatements', interfaceStatements);
+        this.cache.set('assignmentStatements', assignmentStatements);
+        this.cache.set('libraryStatements', libraryStatements);
+        this.cache.set('importStatements', importStatements);
+        this.cache.set('functionStatements', functionStatements);
+        this.cache.set('functionExpressions', functionExpressions);
+        this.cache.set('newExpressions', newExpressions);
+        this.cache.set('propertyHints', propertyHints);
+    }
+}
diff --git a/src/astUtils/visitors.spec.ts b/src/astUtils/visitors.spec.ts
index 06941c83..d58eb6c2 100644
--- a/src/astUtils/visitors.spec.ts
+++ b/src/astUtils/visitors.spec.ts
@@ -9,13 +9,14 @@ import type { FunctionStatement } from '../parser/Statement';
 import { PrintStatement, Block, ReturnStatement, ExpressionStatement } from '../parser/Statement';
 import { TokenKind } from '../lexer/TokenKind';
 import { ChildrenSkipper, createVisitor, WalkMode, walkStatements } from './visitors';
-import { isPrintStatement } from './reflection';
+import { isFunctionExpression, isPrintStatement } from './reflection';
 import { createCall, createToken, createVariableExpression } from './creators';
 import { createStackedVisitor } from './stackedVisitor';
 import { Editor } from './Editor';
 import { Parser } from '../parser/Parser';
 import type { Statement, Expression, AstNode } from '../parser/AstNode';
 import { expectZeroDiagnostics } from '../testHelpers.spec';
+import type { FunctionExpression } from '..';
 
 describe('astUtils visitors', () => {
     const rootDir = process.cwd();
@@ -85,7 +86,8 @@ describe('astUtils visitors', () => {
 
     function functionsWalker(visitor: (statement: Statement, parent: Statement) => void, cancel?: CancellationToken) {
         return (file: BrsFile) => {
-            file.parser.references.functionExpressions.some(functionExpression => {
+            const funcExpressions = file.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitExpressionsRecursive });
+            funcExpressions.some(functionExpression => {
                 visitor(functionExpression.body, undefined);
                 walkStatements(functionExpression.body, (statement, parent) => visitor(statement, parent), cancel);
                 return cancel?.isCancellationRequested;
diff --git a/src/bscPlugin/codeActions/CodeActionsProcessor.ts b/src/bscPlugin/codeActions/CodeActionsProcessor.ts
index fc4449ff..de19efb4 100644
--- a/src/bscPlugin/codeActions/CodeActionsProcessor.ts
+++ b/src/bscPlugin/codeActions/CodeActionsProcessor.ts
@@ -9,6 +9,7 @@ import type { XmlFile } from '../../files/XmlFile';
 import type { OnGetCodeActionsEvent } from '../../interfaces';
 import { ParseMode } from '../../parser/Parser';
 import { util } from '../../util';
+import { isBrsFile } from '../../astUtils/reflection';
 
 export class CodeActionsProcessor {
     public constructor(
@@ -36,12 +37,13 @@ export class CodeActionsProcessor {
      */
     private suggestImports(diagnostic: Diagnostic, key: string, files: BscFile[]) {
         //skip if we already have this suggestion
-        if (this.suggestedImports.has(key)) {
+        if (this.suggestedImports.has(key) || !isBrsFile(this.event.file)) {
             return;
         }
 
         this.suggestedImports.add(key);
-        const importStatements = (this.event.file as BrsFile).parser.references.importStatements;
+        // eslint-disable-next-line @typescript-eslint/dot-notation
+        const importStatements = this.event.file['_cachedLookups'].importStatements;
         //find the position of the first import statement, or the top of the file if there is none
         const insertPosition = importStatements[importStatements.length - 1]?.importToken.range?.start ?? util.createPosition(0, 0);
 
diff --git a/src/bscPlugin/completions/CompletionsProcessor.spec.ts b/src/bscPlugin/completions/CompletionsProcessor.spec.ts
index c590b2c1..75d46f40 100644
--- a/src/bscPlugin/completions/CompletionsProcessor.spec.ts
+++ b/src/bscPlugin/completions/CompletionsProcessor.spec.ts
@@ -400,7 +400,7 @@ describe('CompletionsProcessor', () => {
             expect(completions).to.be.lengthOf(2);
         });
 
-        it('get all functions and properties in scope when doing any dotted get on non m ', () => {
+        it('get all functions and properties in scope when doing any dotted get on non m', () => {
             program.setFile('source/main.bs', `
                 sub main()
                     thing.anonPropA = "foo"
@@ -442,7 +442,7 @@ describe('CompletionsProcessor', () => {
             //note - we let the vscode extension do the filtering, so we still return everything; otherwise it exhibits strange behaviour in the IDE
             expect(
                 (program.getCompletions(`${rootDir}/source/main.bs`, Position.create(4, 32))).map(x => x.label).sort()
-            ).to.eql(['anonPropA', 'anonPropB', 'person', 'personAMethodA', 'personAMethodB', 'personAMethodC', 'personAName', 'personBMethodA', 'personBMethodB', 'personBName', 'personName']);
+            ).to.eql(['NameB', 'NameC', 'anonPropA', 'anonPropB', 'person', 'personAMethodA', 'personAMethodB', 'personAMethodC', 'personAName', 'personBMethodA', 'personBMethodB', 'personBName', 'personName']);
         });
 
         it('get all functions and properties relevant for m ', () => {
diff --git a/src/bscPlugin/completions/CompletionsProcessor.ts b/src/bscPlugin/completions/CompletionsProcessor.ts
index e4d39f9f..d50e371c 100644
--- a/src/bscPlugin/completions/CompletionsProcessor.ts
+++ b/src/bscPlugin/completions/CompletionsProcessor.ts
@@ -291,16 +291,20 @@ export class CompletionsProcessor {
                     gotSymbolsFromGlobal = true;
                 }
             }
+
+            let ignoreAllPropertyNames = false;
+
             // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
             switch (tokenBefore.kind) {
                 case TokenKind.New:
                     //we are after a new keyword; so we can only be namespaces that have a class or classes at this point
                     currentSymbols = currentSymbols.filter(symbol => isClassType(symbol.type) || this.isNamespaceTypeWithMemberType(symbol.type, isClassType));
+                    ignoreAllPropertyNames = true;
                     break;
             }
 
             result.push(...this.getSymbolsCompletion(currentSymbols, shouldLookForMembers || shouldLookForCallFuncMembers));
-            if (shouldLookForMembers && currentSymbols.length === 0) {
+            if (shouldLookForMembers && currentSymbols.length === 0 && !ignoreAllPropertyNames) {
                 // could not find members of actual known types.. just try everything
                 result.push(...this.getPropertyNameCompletions(scope),
                     ...this.getAllClassMemberCompletions(scope).values());
@@ -499,7 +503,8 @@ export class CompletionsProcessor {
         let filesSearched = new Set<BscFile>();
         for (const file of scope.getAllFiles()) {
             if (isBrsFile(file) && !filesSearched.has(file)) {
-                for (let cs of file.parser.references.classStatements) {
+                // eslint-disable-next-line @typescript-eslint/dot-notation
+                for (let cs of file['_cachedLookups'].classStatements) {
                     for (let s of [...cs.methods, ...cs.fields]) {
                         if (!results.has(s.name.text) && s.name.text.toLowerCase() !== 'new') {
                             results.set(s.name.text, {
diff --git a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts
index fdba1ef1..8b7b8b09 100644
--- a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts
+++ b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts
@@ -185,13 +185,13 @@ describe('BrsFileSemanticTokensProcessor', () => {
             //`type` function call
             range: util.createRange(2, 29, 2, 33),
             tokenType: SemanticTokenTypes.function
-        }, {
+        }, { //Humanoids
             range: util.createRange(2, 34, 2, 43),
             tokenType: SemanticTokenTypes.namespace
-        }, {
+        }, { //Aliens
             range: util.createRange(2, 44, 2, 50),
             tokenType: SemanticTokenTypes.namespace
-        }, {
+        }, { //Invade
             range: util.createRange(2, 51, 2, 57),
             tokenType: SemanticTokenTypes.function
         }]);
diff --git a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts
index 151678a4..81031563 100644
--- a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts
+++ b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts
@@ -25,7 +25,8 @@ export class BrsFileSemanticTokensProcessor {
     }
 
     private handleConstDeclarations() {
-        for (const stmt of this.event.file.parser.references.constStatements) {
+        // eslint-disable-next-line @typescript-eslint/dot-notation
+        for (const stmt of this.event.file['_cachedLookups'].constStatements) {
             this.addToken(stmt.tokens.name, SemanticTokenTypes.variable, [SemanticTokenModifiers.readonly, SemanticTokenModifiers.static]);
         }
     }
@@ -35,7 +36,8 @@ export class BrsFileSemanticTokensProcessor {
         const classes = [] as Array<{ className: string; namespaceName: string; range: Range }>;
 
         //classes used in function param types
-        for (const func of this.event.file.parser.references.functionExpressions) {
+        // eslint-disable-next-line @typescript-eslint/dot-notation
+        for (const func of this.event.file['_cachedLookups'].functionExpressions) {
             for (const param of func.parameters) {
                 if (isClassType(param.getType({ flags: SymbolTypeFlag.typetime }))) {
                     const namespace = param.findAncestor<NamespaceStatement>(isNamespaceStatement);
@@ -79,6 +81,9 @@ export class BrsFileSemanticTokensProcessor {
     }
 
     private addToken(locatable: Locatable, type: SemanticTokenTypes, modifiers: SemanticTokenModifiers[] = []) {
+
+        console.log(' - ', locatable.text, util.rangeToString(locatable.range), type);
+
         this.event.semanticTokens.push({
             range: locatable.range,
             tokenType: type,
@@ -95,10 +100,13 @@ export class BrsFileSemanticTokensProcessor {
         }
         scope.linkSymbolTable();
         const nodes = [
-            ...this.event.file.parser.references.expressions,
+            // eslint-disable-next-line @typescript-eslint/dot-notation
+            ...this.event.file['_cachedLookups'].expressions,
             //make a new VariableExpression to wrap the name. This is a hack, we could probably do it better
-            ...this.event.file.parser.references.assignmentStatements,
-            ...this.event.file.parser.references.functionExpressions.map(x => x.parameters).flat()
+            // eslint-disable-next-line @typescript-eslint/dot-notation
+            ...this.event.file['_cachedLookups'].assignmentStatements,
+            // eslint-disable-next-line @typescript-eslint/dot-notation
+            ...this.event.file['_cachedLookups'].functionExpressions.map(x => x.parameters).flat()
         ];
 
         for (let node of nodes) {
@@ -108,6 +116,9 @@ export class BrsFileSemanticTokensProcessor {
             } else if (isNewExpression(node)) {
                 node = node.call.callee;
             }
+
+            console.log(node.kind, util.rangeToString(node.range));
+
             const containingNamespaceNameLower = node.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase();
             const tokens = util.getAllDottedGetParts(node);
             const processedNames: string[] = [];
diff --git a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts
index d93eb74e..ae70d8e3 100644
--- a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts
+++ b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts
@@ -41,7 +41,8 @@ export class BrsFilePreTranspileProcessor {
 
     private iterateExpressions() {
         const scope = this.event.program.getFirstScopeForFile(this.event.file);
-        for (let expression of this.event.file.parser.references.expressions) {
+        // eslint-disable-next-line @typescript-eslint/dot-notation
+        for (let expression of this.event.file['_cachedLookups'].expressions) {
             if (expression) {
                 if (isUnaryExpression(expression)) {
                     this.processExpression(expression.right, scope);
@@ -89,7 +90,6 @@ export class BrsFilePreTranspileProcessor {
         let containingNamespace = this.event.file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript);
 
         const parts = util.splitExpression(expression);
-
         const processedNames: string[] = [];
         for (let part of parts) {
             let entityName: string;
diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts
index ac3617ba..d2fc871a 100644
--- a/src/bscPlugin/validation/BrsFileValidator.ts
+++ b/src/bscPlugin/validation/BrsFileValidator.ts
@@ -24,12 +24,19 @@ export class BrsFileValidator {
         const unlinkGlobalSymbolTable = this.event.file.parser.symbolTable.pushParentProvider(() => this.event.program.globalScope.symbolTable);
 
         util.validateTooDeepFile(this.event.file);
+
+        // Invalidate cache on this file
+        // It could have potentially changed before this from plugins, after this, it will not change
+        // eslint-disable-next-line @typescript-eslint/dot-notation
+        this.event.file['_cachedLookups'].invalidate();
+
         this.walk();
         this.flagTopLevelStatements();
         //only validate the file if it was actually parsed (skip files containing typedefs)
         if (!this.event.file.hasTypedef) {
             this.validateImportStatements();
         }
+
         if (isBrsFile(this.event.file)) {
             this.event.file.processSymbolInformation();
         }
@@ -114,7 +121,7 @@ export class BrsFileValidator {
                 //this function is declared inside a namespace
                 if (namespace) {
                     namespace.getSymbolTable().addSymbol(
-                        node.name.text,
+                        node.name?.text,
                         { definingNode: node },
                         funcType,
                         SymbolTypeFlag.runtime
@@ -317,9 +324,9 @@ export class BrsFileValidator {
 
         let statements = [
             // eslint-disable-next-line @typescript-eslint/dot-notation
-            ...this.event.file['_parser'].references.libraryStatements,
+            ...this.event.file['_cachedLookups'].libraryStatements,
             // eslint-disable-next-line @typescript-eslint/dot-notation
-            ...this.event.file['_parser'].references.importStatements
+            ...this.event.file['_cachedLookups'].importStatements
         ];
         for (let result of statements) {
             //if this statement is not one of the top-of-file statements,
diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts
index 100ec7d1..2d5f7e01 100644
--- a/src/bscPlugin/validation/ScopeValidator.ts
+++ b/src/bscPlugin/validation/ScopeValidator.ts
@@ -156,7 +156,8 @@ export class ScopeValidator {
         const diagnostics: BsDiagnosticWithOrigin[] = [];
         const enumLocationsByName = new Cache<string, Array<{ file: BrsFile; statement: EnumStatement }>>();
         this.event.scope.enumerateBrsFiles((file) => {
-            for (const enumStatement of file.parser.references.enumStatements) {
+            // eslint-disable-next-line @typescript-eslint/dot-notation
+            for (const enumStatement of file['_cachedLookups'].enumStatements) {
                 const fullName = enumStatement.fullName;
                 const nameLower = fullName?.toLowerCase();
                 if (nameLower?.length > 0) {
diff --git a/src/examples/plugins/removePrint.ts b/src/examples/plugins/removePrint.ts
index b669409c..2411760b 100644
--- a/src/examples/plugins/removePrint.ts
+++ b/src/examples/plugins/removePrint.ts
@@ -8,15 +8,13 @@ export default function plugin() {
         beforePrepareFile: (event: BeforePrepareFileEvent) => {
             if (isBrsFile(event.file)) {
                 // visit functions bodies and replace `PrintStatement` nodes with `EmptyStatement`
-                for (const func of event.file.parser.references.functionExpressions) {
-                    func.body.walk(createVisitor({
-                        PrintStatement: (statement) => {
-                            event.editor.overrideTranspileResult(statement, '');
-                        }
-                    }), {
-                        walkMode: WalkMode.visitStatements
-                    });
-                }
+                event.file.ast.walk(createVisitor({
+                    PrintStatement: (statement) => {
+                        event.editor.overrideTranspileResult(statement, '');
+                    }
+                }), {
+                    walkMode: WalkMode.visitExpressionsRecursive
+                });
             }
         }
     } as CompilerPlugin;
diff --git a/src/files/BrsFile.Class.spec.ts b/src/files/BrsFile.Class.spec.ts
index e18b5b28..24d08393 100644
--- a/src/files/BrsFile.Class.spec.ts
+++ b/src/files/BrsFile.Class.spec.ts
@@ -11,8 +11,10 @@ import { standardizePath as s } from '../util';
 import * as fsExtra from 'fs-extra';
 import { BrsTranspileState } from '../parser/BrsTranspileState';
 import { doesNotThrow } from 'assert';
-import type { MethodStatement } from '../parser/Statement';
+import type { ClassStatement, MethodStatement } from '../parser/Statement';
 import { tempDir, rootDir, stagingDir } from '../testHelpers.spec';
+import { isClassStatement } from '../astUtils/reflection';
+import { WalkMode } from '../astUtils/visitors';
 
 let sinon = sinonImport.createSandbox();
 
@@ -45,7 +47,8 @@ describe('BrsFile BrighterScript classes', () => {
 
             end class
         `);
-        expect(file.parser.references.classStatements.map(x => x.getName(ParseMode.BrighterScript)).sort()).to.eql(['Animal', 'Duck']);
+        const classStatements = file.ast.findChildren<ClassStatement>(isClassStatement);
+        expect(classStatements.map(x => x.getName(ParseMode.BrighterScript)).sort()).to.eql(['Animal', 'Duck']);
     });
 
     it('does not cause errors with incomplete class statement', () => {
@@ -110,7 +113,7 @@ describe('BrsFile BrighterScript classes', () => {
         `);
         program.validate();
         expectZeroDiagnostics(program);
-        let duckClass = file.parser.references.classStatements.find(x => x.name.text.toLowerCase() === 'duck');
+        let duckClass = file.ast.findChildren<ClassStatement>(isClassStatement, { walkMode: WalkMode.visitStatements }).find(x => x.name.text.toLowerCase() === 'duck');
         expect(duckClass).to.exist;
         expect(duckClass!.memberMap['move']).to.exist;
     });
@@ -1832,7 +1835,8 @@ describe('BrsFile BrighterScript classes', () => {
             end class
         `);
         doesNotThrow(() => {
-            file.parser.references.classStatements[0]['getParentClassIndex'](new BrsTranspileState(file));
+            const classStatements = file.ast.findChildren<ClassStatement>(isClassStatement);
+            classStatements[0]['getParentClassIndex'](new BrsTranspileState(file));
         });
     });
 
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index dd920d6e..6b5e6bd0 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -218,7 +218,7 @@ describe('BrsFile', () => {
             new ImportStatement(createToken(TokenKind.Import), createToken(TokenKind.StringLiteral, 'pkg:/source/lib.brs'))
         );
         expect(file.ownScriptImports).to.be.empty;
-        file.parser.invalidateReferences();
+        file['_cachedLookups'].invalidate();
         expect(file.ownScriptImports.map(x => x.text)).to.eql(['pkg:/source/lib.brs']);
     });
 
@@ -1144,6 +1144,7 @@ describe('BrsFile', () => {
                     return value.subType()
                 end function
             `);
+
             expect(file.callables[0]).to.deep.include({
                 file: file,
                 nameRange: Range.create(1, 25, 1, 36)
@@ -3119,7 +3120,7 @@ describe('BrsFile', () => {
             const stub = sinon.stub(file, 'parse').callThrough();
 
             //`file.parser` is a getter, so that should force the parse to occur
-            expect(file.parser.references.functionStatements).to.be.lengthOf(1);
+            expect(file.parser.ast).to.exist;
             expect(stub.called).to.be.true;
             //parse should have been called
         });
@@ -4161,4 +4162,67 @@ describe('BrsFile', () => {
             });
         });
     });
+
+    describe('propertyHints', () => {
+
+        it('extracts property names for completion', () => {
+            const file = program.setFile<BrsFile>('source/main.brs', `
+                function main(arg as string)
+                    aa1 = {
+                        "sprop1": 0,
+                        prop1: 1
+                        prop2: {
+                            prop3: 2
+                        }
+                    }
+                    aa2 = {
+                        prop4: {
+                            prop5: 5,
+                            "sprop2": 0,
+                            prop6: 6
+                        },
+                        prop7: 7
+                    }
+                    calling({
+                        prop8: 8,
+                        prop9: 9
+                    })
+                    aa1.field1 = 1
+                    aa1.field2.field3 = 2
+                    calling(aa2.field4, 3 + aa2.field5.field6)
+                end function
+            `);
+
+            const expected = [
+                'field1', 'field2', 'field3', 'field4', 'field5', 'field6',
+                'prop1', 'prop2', 'prop3', 'prop4', 'prop5', 'prop6', 'prop7', 'prop8', 'prop9'
+            ];
+
+            const { propertyHints } = file['_cachedLookups'];
+            expect(Object.keys(propertyHints).sort()).to.deep.equal(expected, 'Initial hints');
+        });
+
+        it('extracts property names matching JavaScript reserved names', () => {
+            const file = program.setFile<BrsFile>('source/main.brs', `
+                function main(arg as string)
+                    aa1 = {
+                        "constructor": 0,
+                        constructor: 1
+                        valueOf: {
+                            toString: 2
+                        }
+                    }
+                    aa1.constructor = 1
+                    aa1.valueOf.toString = 2
+                end function
+            `);
+
+            const expected = [
+                'constructor', 'tostring', 'valueof'
+            ];
+
+            const { propertyHints } = file['_cachedLookups'];
+            expect(Object.keys(propertyHints).sort()).to.deep.equal(expected, 'Initial hints');
+        });
+    });
 });
diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts
index aba7619d..7d072201 100644
--- a/src/files/BrsFile.ts
+++ b/src/files/BrsFile.ts
@@ -8,7 +8,7 @@ import * as path from 'path';
 import { DiagnosticCodeMap, diagnosticCodes, DiagnosticMessages } from '../DiagnosticMessages';
 import { FunctionScope } from '../FunctionScope';
 import type { Callable, CallableArg, CallableParam, CommentFlag, FunctionCall, BsDiagnostic, FileReference, FileLink, SerializedCodeFile, NamespaceContainer } from '../interfaces';
-import type { Token } from '../lexer/Token';
+import { type Token } from '../lexer/Token';
 import { Lexer } from '../lexer/Lexer';
 import { TokenKind, AllowedLocalIdentifiers } from '../lexer/TokenKind';
 import { Parser, ParseMode } from '../parser/Parser';
@@ -34,6 +34,8 @@ import type { BscFile } from './BscFile';
 import { Editor } from '../astUtils/Editor';
 import type { UnresolvedSymbol } from '../AstValidationSegmenter';
 import { AstValidationSegmenter } from '../AstValidationSegmenter';
+import type { BscFileLike } from '../astUtils/CachedLookups';
+import { CachedLookups } from '../astUtils/CachedLookups';
 
 
 export type ProvidedSymbolMap = Map<SymbolTypeFlag, Map<string, BscSymbol>>;
@@ -68,6 +70,7 @@ export class BrsFile implements BscFile {
             this.srcPath = s`${options.srcPath}`;
             this.destPath = s`${options.destPath}`;
             this.program = options.program;
+            this._cachedLookups = new CachedLookups(this as unknown as BscFileLike);
 
             this.extension = util.getExtension(this.srcPath);
             if (options.pkgPath) {
@@ -105,6 +108,8 @@ export class BrsFile implements BscFile {
 
     public program: Program;
 
+    private _cachedLookups: CachedLookups;
+
     /**
      * An editor assigned during the build flow that manages edits that will be undone once the build process is complete.
      */
@@ -175,10 +180,6 @@ export class BrsFile implements BscFile {
 
     public commentFlags = [] as CommentFlag[];
 
-    public callables = [] as Callable[];
-
-    public functionCalls = [] as FunctionCall[];
-
     private _functionScopes: FunctionScope[];
 
     public get functionScopes(): FunctionScope[] {
@@ -190,7 +191,7 @@ export class BrsFile implements BscFile {
 
     private get cache() {
         // eslint-disable-next-line @typescript-eslint/dot-notation
-        return this._parser?.references['cache'];
+        return this._cachedLookups['cache'];
     }
 
     /**
@@ -199,7 +200,7 @@ export class BrsFile implements BscFile {
     public get ownScriptImports() {
         const result = this.cache?.getOrAdd('BrsFile_ownScriptImports', () => {
             const result = [] as FileReference[];
-            for (const statement of this.parser?.references?.importStatements ?? []) {
+            for (const statement of this._cachedLookups?.importStatements ?? []) {
                 //register import statements
                 if (isImportStatement(statement) && statement.filePathToken) {
                     result.push({
@@ -407,12 +408,6 @@ export class BrsFile implements BscFile {
                 ...this._parser.diagnostics as BsDiagnostic[]
             );
 
-            //extract all callables from this file
-            this.findCallables();
-
-            //find all places where a sub/function is being called
-            this.findFunctionCalls();
-
             //attach this file to every diagnostic
             for (let diagnostic of this.diagnostics) {
                 diagnostic.file = this;
@@ -450,7 +445,7 @@ export class BrsFile implements BscFile {
 
     public findPropertyNameCompletions(): CompletionItem[] {
         //Build completion items from all the "properties" found in the file
-        const { propertyHints } = this.parser.references;
+        const { propertyHints } = this._cachedLookups;
         const results = [] as CompletionItem[];
         for (const key of Object.keys(propertyHints)) {
             results.push({
@@ -494,7 +489,7 @@ export class BrsFile implements BscFile {
      */
     private createFunctionScopes() {
         //find every function
-        let functions = this.parser.references.functionExpressions;
+        let functions = this._cachedLookups.functionExpressions;
 
         //create a functionScope for every function
         this._functionScopes = [];
@@ -557,7 +552,7 @@ export class BrsFile implements BscFile {
         }
 
         //find every variable assignment in the whole file
-        let assignmentStatements = this.parser.references.assignmentStatements;
+        let assignmentStatements = this._cachedLookups.assignmentStatements;
 
         for (let statement of assignmentStatements) {
 
@@ -580,131 +575,150 @@ export class BrsFile implements BscFile {
         }
     }
 
-    private findCallables() {
-        for (let statement of this.parser.references.functionStatements ?? []) {
+    public staticCallables: Callable[];
 
-            //extract the parameters
-            let params = [] as CallableParam[];
-            for (let param of statement.func.parameters) {
-                const paramType = param.getType({ flags: SymbolTypeFlag.typetime });
+    get callables(): Callable[] {
 
-                let callableParam = {
-                    name: param.name.text,
-                    type: paramType,
-                    isOptional: !!param.defaultValue,
-                    isRestArgument: false
-                };
-                params.push(callableParam);
-            }
-            const funcType = statement.getType({ flags: SymbolTypeFlag.typetime });
-            this.callables.push({
-                isSub: statement.func.functionType.text.toLowerCase() === 'sub',
-                name: statement.name.text,
-                nameRange: statement.name.range,
-                file: this,
-                params: params,
-                range: statement.func.range,
-                type: funcType,
-                getName: statement.getName.bind(statement),
-                hasNamespace: !!statement.findAncestor<NamespaceStatement>(isNamespaceStatement),
-                functionStatement: statement
-            });
+        if (this.staticCallables) {
+            // globalFile can statically set the callables
+            return this.staticCallables;
         }
-    }
 
-    private findFunctionCalls() {
-        this.functionCalls = [];
-        //for every function in the file
-        for (let func of this._parser.references.functionExpressions) {
-            //for all function calls in this function
-            for (let expression of func.callExpressions) {
-                if (
-                    //filter out dotted function invocations (i.e. object.doSomething()) (not currently supported. TODO support it)
-                    (expression.callee as any).obj ||
-                    //filter out method calls on method calls for now (i.e. getSomething().getSomethingElse())
-                    (expression.callee as any).callee ||
-                    //filter out callees without a name (immediately-invoked function expressions)
-                    !(expression.callee as any).name
-                ) {
-                    continue;
+        return this.cache.getOrAdd(`BrsFile_callables`, () => {
+            const callables = [];
+
+            for (let statement of this._cachedLookups.functionStatements ?? []) {
+
+                //extract the parameters
+                let params = [] as CallableParam[];
+                for (let param of statement.func.parameters) {
+                    const paramType = param.getType({ flags: SymbolTypeFlag.typetime });
+
+                    let callableParam = {
+                        name: param.name?.text,
+                        type: paramType,
+                        isOptional: !!param.defaultValue,
+                        isRestArgument: false
+                    };
+                    params.push(callableParam);
                 }
-                let functionName = (expression.callee as any).name.text;
-
-                //callee is the name of the function being called
-                let callee = expression.callee as VariableExpression;
-
-                let columnIndexBegin = callee.range.start.character;
-                let columnIndexEnd = callee.range.end.character;
-
-                let args = [] as CallableArg[];
-                //TODO convert if stmts to use instanceof instead
-                for (let arg of expression.args as any) {
-
-                    //is a literal parameter value
-                    if (isLiteralExpression(arg)) {
-                        args.push({
-                            range: arg.range,
-                            type: arg.getType(),
-                            text: arg.token.text,
-                            expression: arg,
-                            typeToken: undefined
-                        });
-
-                        //is variable being passed into argument
-                    } else if (arg.name) {
-                        args.push({
-                            range: arg.range,
-                            //TODO - look up the data type of the actual variable
-                            type: new DynamicType(),
-                            text: arg.name.text,
-                            expression: arg,
-                            typeToken: undefined
-                        });
-
-                    } else if (arg.value) {
-                        let text = '';
-                        /* istanbul ignore next: TODO figure out why value is undefined sometimes */
-                        if (arg.value.value) {
-                            text = arg.value.value.toString();
-                        }
-                        let callableArg = {
-                            range: arg.range,
-                            //TODO not sure what to do here
-                            type: new DynamicType(), // util.valueKindToBrsType(arg.value.kind),
-                            text: text,
-                            expression: arg,
-                            typeToken: undefined
-                        };
-                        //wrap the value in quotes because that's how it appears in the code
-                        if (isStringType(callableArg.type)) {
-                            callableArg.text = '"' + callableArg.text + '"';
+                const funcType = statement.getType({ flags: SymbolTypeFlag.typetime });
+                callables.push({
+                    isSub: statement.func.functionType.text.toLowerCase() === 'sub',
+                    name: statement.name?.text,
+                    nameRange: statement.name?.range,
+                    file: this,
+                    params: params,
+                    range: statement.func.range,
+                    type: funcType,
+                    getName: statement.getName.bind(statement),
+                    hasNamespace: !!statement.findAncestor<NamespaceStatement>(isNamespaceStatement),
+                    functionStatement: statement
+                });
+            }
+
+            return callables;
+        });
+
+
+    }
+
+    get functionCalls(): FunctionCall[] {
+        return this.cache.getOrAdd(`BrsFile_functionCalls`, () => {
+            const functionCalls = [];
+            //for every function in the file
+            for (let func of this._cachedLookups.functionExpressions) {
+                //for all function calls in this function
+                for (let expression of func.callExpressions) {
+                    if (
+                        //filter out dotted function invocations (i.e. object.doSomething()) (not currently supported. TODO support it)
+                        (expression.callee as any).obj ||
+                        //filter out method calls on method calls for now (i.e. getSomething().getSomethingElse())
+                        (expression.callee as any).callee ||
+                        //filter out callees without a name (immediately-invoked function expressions)
+                        !(expression.callee as any).name
+                    ) {
+                        continue;
+                    }
+                    let functionName = (expression.callee as any).name.text;
+
+                    //callee is the name of the function being called
+                    let callee = expression.callee as VariableExpression;
+
+                    let columnIndexBegin = callee.range.start.character;
+                    let columnIndexEnd = callee.range.end.character;
+
+                    let args = [] as CallableArg[];
+                    //TODO convert if stmts to use instanceof instead
+                    for (let arg of expression.args as any) {
+
+                        //is a literal parameter value
+                        if (isLiteralExpression(arg)) {
+                            args.push({
+                                range: arg.range,
+                                type: arg.getType(),
+                                text: arg.token.text,
+                                expression: arg,
+                                typeToken: undefined
+                            });
+
+                            //is variable being passed into argument
+                        } else if (arg.name) {
+                            args.push({
+                                range: arg.range,
+                                //TODO - look up the data type of the actual variable
+                                type: new DynamicType(),
+                                text: arg.name.text,
+                                expression: arg,
+                                typeToken: undefined
+                            });
+
+                        } else if (arg.value) {
+                            let text = '';
+                            /* istanbul ignore next: TODO figure out why value is undefined sometimes */
+                            if (arg.value.value) {
+                                text = arg.value.value.toString();
+                            }
+                            let callableArg = {
+                                range: arg.range,
+                                //TODO not sure what to do here
+                                type: new DynamicType(), // util.valueKindToBrsType(arg.value.kind),
+                                text: text,
+                                expression: arg,
+                                typeToken: undefined
+                            };
+                            //wrap the value in quotes because that's how it appears in the code
+                            if (isStringType(callableArg.type)) {
+                                callableArg.text = '"' + callableArg.text + '"';
+                            }
+                            args.push(callableArg);
+
+                        } else {
+                            args.push({
+                                range: arg.range,
+                                type: new DynamicType(),
+                                //TODO get text from other types of args
+                                text: '',
+                                expression: arg,
+                                typeToken: undefined
+                            });
                         }
-                        args.push(callableArg);
-
-                    } else {
-                        args.push({
-                            range: arg.range,
-                            type: new DynamicType(),
-                            //TODO get text from other types of args
-                            text: '',
-                            expression: arg,
-                            typeToken: undefined
-                        });
                     }
+                    let functionCall: FunctionCall = {
+                        range: expression.range,
+                        functionScope: this.getFunctionScopeAtPosition(callee.range.start),
+                        file: this,
+                        name: functionName,
+                        nameRange: util.createRange(callee.range.start.line, columnIndexBegin, callee.range.start.line, columnIndexEnd),
+                        //TODO keep track of parameters
+                        args: args,
+                        expression: expression
+                    };
+                    functionCalls.push(functionCall);
                 }
-                let functionCall: FunctionCall = {
-                    range: expression.range,
-                    functionScope: this.getFunctionScopeAtPosition(callee.range.start),
-                    file: this,
-                    name: functionName,
-                    nameRange: util.createRange(callee.range.start.line, columnIndexBegin, callee.range.start.line, columnIndexEnd),
-                    //TODO keep track of parameters
-                    args: args,
-                    expression: expression
-                };
-                this.functionCalls.push(functionCall);
             }
-        }
+            return functionCalls;
+        });
     }
 
     /**
@@ -740,7 +754,7 @@ export class BrsFile implements BscFile {
     public getNamespaceStatementForPosition(position: Position): NamespaceStatement {
         if (position) {
             return this.cache.getOrAdd(`namespaceStatementForPosition-${position.line}:${position.character}`, () => {
-                for (const statement of this.parser.references.namespaceStatements) {
+                for (const statement of this._cachedLookups.namespaceStatements) {
                     if (util.rangeContains(statement.range, position)) {
                         return statement;
                     }
@@ -1188,7 +1202,7 @@ export class BrsFile implements BscFile {
 
         let classToken = this.getTokenBefore(token, TokenKind.Class);
         if (classToken) {
-            let cs = this.parser.references.classStatements.find((cs) => cs.classKeyword.range === classToken.range);
+            let cs = this._cachedLookups.classStatements.find((cs) => cs.classKeyword.range === classToken.range);
             if (cs?.parentClassName) {
                 const nameParts = cs.parentClassName.getNameParts();
                 let extendedClass = this.getClassFileLink(nameParts[nameParts.length - 1], nameParts.slice(0, -1).join('.'));
@@ -1460,7 +1474,7 @@ export class BrsFile implements BscFile {
             table: this.parser.symbolTable
         }];
 
-        for (const namespaceStatement of this.parser.references.namespaceStatements) {
+        for (const namespaceStatement of this._cachedLookups.namespaceStatements) {
             tablesToGetSymbolsFrom.push({
                 table: namespaceStatement.body.getSymbolTable(),
                 namePrefixLower: namespaceStatement.getName(ParseMode.BrighterScript).toLowerCase()
@@ -1553,7 +1567,7 @@ export class BrsFile implements BscFile {
 
     private buildNamespaceLookup() {
         const namespaceLookup = new Map<string, NamespaceContainer>();
-        for (let namespaceStatement of this.parser.references.namespaceStatements) {
+        for (let namespaceStatement of this._cachedLookups.namespaceStatements) {
             let nameParts = namespaceStatement.getNameParts();
 
             let loopName: string = null;
@@ -1632,8 +1646,6 @@ export class BrsFile implements BscFile {
         //deleting these properties result in lower memory usage (garbage collection is magic!)
         delete this.fileContents;
         delete this._parser;
-        delete this.callables;
-        delete this.functionCalls;
         delete this._functionScopes;
         delete this.scopesByFunc;
     }
diff --git a/src/globalCallables.ts b/src/globalCallables.ts
index 75b7b173..d41822b7 100644
--- a/src/globalCallables.ts
+++ b/src/globalCallables.ts
@@ -1022,7 +1022,7 @@ for (let callable of globalCallables) {
     };
 
 }
-globalFile.callables = globalCallables as Callable[];
+globalFile.staticCallables = globalCallables as Callable[];
 
 
 /**
diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts
index 35782aea..9a687fbe 100644
--- a/src/parser/Parser.spec.ts
+++ b/src/parser/Parser.spec.ts
@@ -8,12 +8,8 @@ import type { AssignmentStatement, ClassStatement, InterfaceStatement, ReturnSta
 import { PrintStatement, FunctionStatement, NamespaceStatement, ImportStatement } from './Statement';
 import { Range } from 'vscode-languageserver';
 import { DiagnosticMessages } from '../DiagnosticMessages';
-import { isAssignmentStatement, isBinaryExpression, isBlock, isCallExpression, isClassStatement, isCommentStatement, isDottedGetExpression, isExpressionStatement, isFunctionStatement, isGroupingExpression, isIfStatement, isIndexedGetExpression, isInterfaceStatement, isLiteralExpression, isNamespaceStatement, isPrintStatement, isTypeCastExpression, isUnaryExpression, isVariableExpression } from '../astUtils/reflection';
+import { isAssignmentStatement, isBinaryExpression, isBlock, isCallExpression, isClassStatement, isCommentStatement, isDottedGetExpression, isExpression, isExpressionStatement, isFunctionStatement, isGroupingExpression, isIfStatement, isIndexedGetExpression, isInterfaceStatement, isLiteralExpression, isNamespaceStatement, isPrintStatement, isTypeCastExpression, isUnaryExpression, isVariableExpression } from '../astUtils/reflection';
 import { expectDiagnosticsIncludes, expectTypeToBe, expectZeroDiagnostics } from '../testHelpers.spec';
-import { BrsTranspileState } from './BrsTranspileState';
-import { SourceNode } from 'source-map';
-import { BrsFile } from '../files/BrsFile';
-import { Program } from '../Program';
 import { createVisitor, WalkMode } from '../astUtils/visitors';
 import type { Expression, Statement } from './AstNode';
 import { SymbolTypeFlag } from '../SymbolTable';
@@ -29,167 +25,167 @@ describe('parser', () => {
             diagnostics: []
         });
     });
+    /*
+        describe.skip('findReferences', () => {
+            it('gets called if references are missing', () => {
+                const parser = Parser.parse(`
+                    sub main()
+                    end sub
 
-    describe('findReferences', () => {
-        it('gets called if references are missing', () => {
-            const parser = Parser.parse(`
-                sub main()
-                end sub
-
-                sub UnusedFunction()
-                end sub
-            `);
-            expect(parser.references.functionStatements.map(x => x.name.text)).to.eql([
-                'main',
-                'UnusedFunction'
-            ]);
-            //simulate a tree-shaking plugin by removing the `UnusedFunction`
-            parser.ast.statements.splice(1);
-            //tell the parser we modified the AST and need to regenerate references
-            parser.invalidateReferences();
-            expect(parser['_references']).not.to.exist;
-            //calling `references` automatically regenerates the references
-            expect(parser.references.functionStatements.map(x => x.name.text)).to.eql([
-                'main'
-            ]);
-        });
+                    sub UnusedFunction()
+                    end sub
+                `);
+                expect(parser.references.functionStatements.map(x => x.name.text)).to.eql([
+                    'main',
+                    'UnusedFunction'
+                ]);
+                //simulate a tree-shaking plugin by removing the `UnusedFunction`
+                parser.ast.statements.splice(1);
+                //tell the parser we modified the AST and need to regenerate references
+                parser.invalidateReferences();
+                expect(parser['_references']).not.to.exist;
+                //calling `references` automatically regenerates the references
+                expect(parser.references.functionStatements.map(x => x.name.text)).to.eql([
+                    'main'
+                ]);
+            });
 
-        function expressionsToStrings(expressions: Set<Expression>) {
-            return [...expressions.values()].map(x => {
-                const file = new BrsFile({
-                    srcPath: '',
-                    destPath: '',
-                    program: new Program({} as any)
+            function expressionsToStrings(expressions: Set<Expression>) {
+                return [...expressions.values()].map(x => {
+                    const file = new BrsFile({
+                        srcPath: '',
+                        destPath: '',
+                        program: new Program({} as any)
+                    });
+                    const state = new BrsTranspileState(file);
+                    return new SourceNode(null, null, null, x.transpile(state)).toString();
                 });
-                const state = new BrsTranspileState(file);
-                return new SourceNode(null, null, null, x.transpile(state)).toString();
-            });
-        }
+            }
 
-        // eslint-disable-next-line func-names, prefer-arrow-callback
-        it('works for references.expressions', function () {
-            this.timeout(5000); // this test takes a long time on github
-            const parser = Parser.parse(`
-                b += "plus-equal"
-                a += 1 + 2
-                b += getValue1() + getValue2()
-                increment++
-                decrement--
-                some.node@.doCallfunc()
-                bravo(3 + 4).jump(callMe())
-                obj = {
-                    val1: someValue
-                }
-                arr = [
-                    one
-                ]
-                thing = alpha.bravo
-                alpha.charlie()
-                delta(alpha.delta)
-                call1().a.b.call2()
-                class Person
-                    name as string = "bob"
-                end class
-                function thing(p1 = name.space.getSomething())
+            // eslint-disable-next-line func-names, prefer-arrow-callback
+            it('works for references.expressions', function () {
+                this.timeout(5000); // this test takes a long time on github
+                const parser = Parser.parse(`
+                    b += "plus-equal"
+                    a += 1 + 2
+                    b += getValue1() + getValue2()
+                    increment++
+                    decrement--
+                    some.node@.doCallfunc()
+                    bravo(3 + 4).jump(callMe())
+                    obj = {
+                        val1: someValue
+                    }
+                    arr = [
+                        one
+                    ]
+                    thing = alpha.bravo
+                    alpha.charlie()
+                    delta(alpha.delta)
+                    call1().a.b.call2()
+                    class Person
+                        name as string = "bob"
+                    end class
+                    function thing(p1 = name.space.getSomething())
 
-                end function
-            `);
-            const expected = [
-                '"plus-equal"',
-                'b',
-                'b += "plus-equal"',
-                '1',
-                '2',
-                'a',
-                'a += 1 + 2',
-                'getValue1()',
-                'getValue2()',
-                'b',
-                'b += getValue1() + getValue2()',
-                'increment++',
-                'decrement--',
-                //currently the "toString" does a transpile, so that's why this is different.
-                'some.node.callfunc("doCallfunc", invalid)',
-                '3',
-                '4',
-                '3 + 4',
-                'callMe()',
-                'bravo(3 + 4).jump(callMe())',
-                'someValue',
-                '{\n    val1: someValue\n}',
-                'one',
-                '[\n    one\n]',
-                'alpha.bravo',
-                'alpha.charlie()',
-                'alpha.delta',
-                'delta(alpha.delta)',
-                'call1().a.b.call2()',
-                '"bob"',
-                'name.space.getSomething()'
-            ];
+                    end function
+                `);
+                const expected = [
+                    '"plus-equal"',
+                    'b',
+                    'b += "plus-equal"',
+                    '1',
+                    '2',
+                    'a',
+                    'a += 1 + 2',
+                    'getValue1()',
+                    'getValue2()',
+                    'b',
+                    'b += getValue1() + getValue2()',
+                    'increment++',
+                    'decrement--',
+                    //currently the "toString" does a transpile, so that's why this is different.
+                    'some.node.callfunc("doCallfunc", invalid)',
+                    '3',
+                    '4',
+                    '3 + 4',
+                    'callMe()',
+                    'bravo(3 + 4).jump(callMe())',
+                    'someValue',
+                    '{\n    val1: someValue\n}',
+                    'one',
+                    '[\n    one\n]',
+                    'alpha.bravo',
+                    'alpha.charlie()',
+                    'alpha.delta',
+                    'delta(alpha.delta)',
+                    'call1().a.b.call2()',
+                    '"bob"',
+                    'name.space.getSomething()'
+                ];
 
-            expect(
-                expressionsToStrings(parser.references.expressions)
-            ).to.eql(expected);
+                expect(
+                    expressionsToStrings(parser.references.expressions)
+                ).to.eql(expected);
 
-            //tell the parser we modified the AST and need to regenerate references
-            parser.invalidateReferences();
+                //tell the parser we modified the AST and need to regenerate references
+                parser.invalidateReferences();
 
-            expect(
-                expressionsToStrings(parser.references.expressions).sort()
-            ).to.eql(expected.sort());
-        });
+                expect(
+                    expressionsToStrings(parser.references.expressions).sort()
+                ).to.eql(expected.sort());
+            });
 
-        it('works for references.expressions', () => {
-            const parser = Parser.parse(`
-                value = true or type(true) = "something" or Enums.A.Value = "value" and Enum1.Value = Name.Space.Enum2.Value
-            `);
-            const expected = [
-                'true',
-                'type(true)',
-                '"something"',
-                'true',
-                'Enums.A.Value',
-                '"value"',
-                'Enum1.Value',
-                'Name.Space.Enum2.Value',
-                'true or type(true) = "something" or Enums.A.Value = "value" and Enum1.Value = Name.Space.Enum2.Value'
-            ];
+            it('works for references.expressions', () => {
+                const parser = Parser.parse(`
+                    value = true or type(true) = "something" or Enums.A.Value = "value" and Enum1.Value = Name.Space.Enum2.Value
+                `);
+                const expected = [
+                    'true',
+                    'type(true)',
+                    '"something"',
+                    'true',
+                    'Enums.A.Value',
+                    '"value"',
+                    'Enum1.Value',
+                    'Name.Space.Enum2.Value',
+                    'true or type(true) = "something" or Enums.A.Value = "value" and Enum1.Value = Name.Space.Enum2.Value'
+                ];
 
-            expect(
-                expressionsToStrings(parser.references.expressions)
-            ).to.eql(expected);
+                expect(
+                    expressionsToStrings(parser.references.expressions)
+                ).to.eql(expected);
 
-            //tell the parser we modified the AST and need to regenerate references
-            parser.invalidateReferences();
+                //tell the parser we modified the AST and need to regenerate references
+                parser.invalidateReferences();
 
-            expect(
-                expressionsToStrings(parser.references.expressions).sort()
-            ).to.eql(expected.sort());
-        });
+                expect(
+                    expressionsToStrings(parser.references.expressions).sort()
+                ).to.eql(expected.sort());
+            });
 
-        it('works for logical expression', () => {
-            const parser = Parser.parse(`
-                value = Enums.A.Value = "value"
-            `);
-            const expected = [
-                'Enums.A.Value',
-                '"value"',
-                'Enums.A.Value = "value"'
-            ];
+            it('works for logical expression', () => {
+                const parser = Parser.parse(`
+                    value = Enums.A.Value = "value"
+                `);
+                const expected = [
+                    'Enums.A.Value',
+                    '"value"',
+                    'Enums.A.Value = "value"'
+                ];
 
-            expect(
-                expressionsToStrings(parser.references.expressions)
-            ).to.eql(expected);
+                expect(
+                    expressionsToStrings(parser.references.expressions)
+                ).to.eql(expected);
 
-            //tell the parser we modified the AST and need to regenerate references
-            parser.invalidateReferences();
+                //tell the parser we modified the AST and need to regenerate references
+                parser.invalidateReferences();
 
-            expect(
-                expressionsToStrings(parser.references.expressions).sort()
-            ).to.eql(expected.sort());
-        });
-    });
+                expect(
+                    expressionsToStrings(parser.references.expressions).sort()
+                ).to.eql(expected.sort());
+            });
+        }); */
 
     describe('callfunc operator', () => {
         it('is not allowed in brightscript mode', () => {
@@ -220,7 +216,7 @@ describe('parser', () => {
         function getExpression<T>(text: string, options?: { matcher?: any; parseMode?: ParseMode }) {
             const parser = parse(text, options?.parseMode);
             expectZeroDiagnostics(parser);
-            const expressions = [...parser.references.expressions];
+            const expressions = parser.ast.findChildren<Expression>(isExpression);
             if (options?.matcher) {
                 return expressions.find(options.matcher) as unknown as T;
             } else {
@@ -289,8 +285,9 @@ describe('parser', () => {
                     key = isTrue ? ["name"] : ["age"]
                 end sub
             `, ParseMode.BrighterScript);
-            expect(parser.references.assignmentStatements[0].value).is.instanceof(IndexedGetExpression);
-            expect(parser.references.assignmentStatements[2].value).is.instanceof(TernaryExpression);
+            const assignmentStatements = parser.ast.findChildren<AssignmentStatement>(isAssignmentStatement);
+            expect(assignmentStatements[0].value).is.instanceof(IndexedGetExpression);
+            expect(assignmentStatements[2].value).is.instanceof(TernaryExpression);
         });
 
         it('distinguishes between optional chaining and ternary expression', () => {
@@ -302,8 +299,9 @@ describe('parser', () => {
                     key = isTrue ? ["name"] : getDefault()
                 end sub
             `, ParseMode.BrighterScript);
-            expect(parser.references.assignmentStatements[0].value).is.instanceof(IndexedGetExpression);
-            expect(parser.references.assignmentStatements[1].value).is.instanceof(TernaryExpression);
+            const assignmentStatements = parser.ast.findChildren<AssignmentStatement>(isAssignmentStatement);
+            expect(assignmentStatements[0].value).is.instanceof(IndexedGetExpression);
+            expect(assignmentStatements[1].value).is.instanceof(TernaryExpression);
         });
     });
 
@@ -401,7 +399,8 @@ describe('parser', () => {
                 `, ParseMode.BrighterScript);
                 expectZeroDiagnostics(parser);
                 // We expect these names to be "as given" in this context, because we aren't evaluating a full program.
-                expect(parser.references.namespaceStatements.map(statement => statement.getName(ParseMode.BrighterScript))).to.deep.equal([
+                const namespaceStatements = parser.ast.findChildren<NamespaceStatement>(isNamespaceStatement);
+                expect(namespaceStatements.map(statement => statement.getName(ParseMode.BrighterScript))).to.have.deep.members([
                     'Level1.Level2.Level3',
                     'Level1'
                 ]);
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index ae15dc02..919d1ed3 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -92,10 +92,8 @@ import {
 } from './Expression';
 import type { Diagnostic, Range } from 'vscode-languageserver';
 import { Logger } from '../Logger';
-import { isAAMemberExpression, isAnnotationExpression, isBinaryExpression, isCallExpression, isCallfuncExpression, isMethodStatement, isCommentStatement, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression } from '../astUtils/reflection';
-import { createVisitor, WalkMode } from '../astUtils/visitors';
+import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDottedGetExpression, isIfStatement, isIndexedGetExpression } from '../astUtils/reflection';
 import { createStringLiteral } from '../astUtils/creators';
-import { Cache } from '../Cache';
 import type { Expression, Statement } from './AstNode';
 import { SymbolTable } from '../SymbolTable';
 
@@ -126,45 +124,6 @@ export class Parser {
         return this.ast.symbolTable;
     }
 
-    /**
-     * References for significant statements/expressions in the parser.
-     * These are initially extracted during parse-time to improve performance, but will also be dynamically regenerated if need be.
-     *
-     * If a plugin modifies the AST, then the plugin should call Parser#invalidateReferences() to force this object to refresh
-     */
-    public get references() {
-        //build the references object if it's missing.
-        if (!this._references) {
-            this.findReferences();
-        }
-        return this._references;
-    }
-
-    private _references = new References();
-
-    /**
-     * Invalidates (clears) the references collection. This should be called anytime the AST has been manipulated.
-     */
-    invalidateReferences() {
-        this._references = undefined;
-    }
-
-    private addPropertyHints(item: Token | AALiteralExpression) {
-        if (isToken(item)) {
-            const name = item.text;
-            this._references.propertyHints[name.toLowerCase()] = name;
-        } else {
-            for (const member of item.elements) {
-                if (!isCommentStatement(member)) {
-                    const name = member.keyToken.text;
-                    if (!name.startsWith('"')) {
-                        this._references.propertyHints[name.toLowerCase()] = name;
-                    }
-                }
-            }
-        }
-    }
-
     /**
      * The list of diagnostics found during the parse process
      */
@@ -555,7 +514,6 @@ export class Parser {
             body,
             endInterfaceToken
         );
-        this._references.interfaceStatements.push(statement);
         this.exitAnnotationBlock(parentAnnotations);
         return statement;
     }
@@ -616,7 +574,6 @@ export class Parser {
         //consume the final `end interface` token
         result.tokens.endEnum = this.consumeToken(TokenKind.EndEnum);
 
-        this._references.enumStatements.push(result);
         this.exitAnnotationBlock(parentAnnotations);
         return result;
     }
@@ -679,9 +636,6 @@ export class Parser {
                 if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
                     const funcDeclaration = this.functionDeclaration(false, false);
 
-                    //remove this function from the lists because it's not a callable
-                    const functionStatement = this._references.functionStatements.pop();
-
                     //if we have an overrides keyword AND this method is called 'new', that's not allowed
                     if (overrideKeyword && funcDeclaration.name.text.toLowerCase() === 'new') {
                         this.diagnostics.push({
@@ -698,7 +652,7 @@ export class Parser {
                     );
 
                     //refer to this statement as parent of the expression
-                    functionStatement.func.functionStatement = decl as MethodStatement;
+                    funcDeclaration.func.functionStatement = decl as MethodStatement;
 
                     //fields
                 } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
@@ -748,7 +702,6 @@ export class Parser {
             parentClassName
         );
 
-        this._references.classStatements.push(result);
         this.exitAnnotationBlock(parentAnnotations);
         return result;
     }
@@ -931,8 +884,6 @@ export class Parser {
                 typeExpression
             );
 
-            this._references.functionExpressions.push(func);
-
             //support ending the function with `end sub` OR `end function`
             func.body = this.block();
             //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
@@ -969,7 +920,6 @@ export class Parser {
                 let result = new FunctionStatement(name, func);
                 func.symbolTable.name += `: '${name?.text}'`;
                 func.functionStatement = result;
-                this._references.functionStatements.push(result);
 
                 return result;
             }
@@ -1041,15 +991,8 @@ export class Parser {
                 name,
                 new BinaryExpression(nameExpression, operator, value)
             );
-            this.addExpressionsToReferences(nameExpression);
-            if (isBinaryExpression(value)) {
-                //remove the right-hand-side expression from this assignment operator, and replace with the full assignment expression
-                this._references.expressions.delete(value);
-            }
-            this._references.expressions.add(result);
         }
 
-        this._references.assignmentStatements.push(result);
         return result;
     }
 
@@ -1365,7 +1308,6 @@ export class Parser {
 
         result.body = body;
         result.endKeyword = endKeyword;
-        this._references.namespaceStatements.push(result);
         //cache the range property so that plugins can't affect it
         result.cacheRange();
         result.body.symbolTable.name += `: namespace '${result.name}'`;
@@ -1455,7 +1397,6 @@ export class Parser {
             name: nameToken,
             equals: equalToken
         }, expression);
-        this._references.constStatements.push(statement);
         return statement;
     }
 
@@ -1469,7 +1410,6 @@ export class Parser {
             )
         });
 
-        this._references.libraryStatements.push(libStatement);
         return libStatement;
     }
 
@@ -1484,7 +1424,6 @@ export class Parser {
             )
         );
 
-        this._references.importStatements.push(importStatement);
         return importStatement;
     }
 
@@ -2002,7 +1941,6 @@ export class Parser {
             }
 
             const result = new IncrementStatement(expr, operator);
-            this._references.expressions.add(result);
             return result;
         }
 
@@ -2287,7 +2225,6 @@ export class Parser {
 
             } while (asToken && typeExpression);
         }
-        this._references.expressions.add(expression);
         return expression;
     }
 
@@ -2319,7 +2256,6 @@ export class Parser {
         while (this.matchAny(TokenKind.And, TokenKind.Or)) {
             let operator = this.previous();
             let right = this.relational();
-            this.addExpressionsToReferences(expr, right);
             expr = new BinaryExpression(expr, operator, right);
         }
 
@@ -2341,21 +2277,12 @@ export class Parser {
         ) {
             let operator = this.previous();
             let right = this.additive();
-            this.addExpressionsToReferences(expr, right);
             expr = new BinaryExpression(expr, operator, right);
         }
 
         return expr;
     }
 
-    private addExpressionsToReferences(...expressions: Expression[]) {
-        for (const expression of expressions) {
-            if (!isBinaryExpression(expression)) {
-                this.references.expressions.add(expression);
-            }
-        }
-    }
-
     // TODO: bitshift
 
     private additive(): Expression {
@@ -2364,7 +2291,6 @@ export class Parser {
         while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
             let operator = this.previous();
             let right = this.multiplicative();
-            this.addExpressionsToReferences(expr, right);
             expr = new BinaryExpression(expr, operator, right);
         }
 
@@ -2384,7 +2310,6 @@ export class Parser {
         )) {
             let operator = this.previous();
             let right = this.exponential();
-            this.addExpressionsToReferences(expr, right);
             expr = new BinaryExpression(expr, operator, right);
         }
 
@@ -2397,7 +2322,6 @@ export class Parser {
         while (this.match(TokenKind.Caret)) {
             let operator = this.previous();
             let right = this.prefixUnary();
-            this.addExpressionsToReferences(expr, right);
             expr = new BinaryExpression(expr, operator, right);
         }
 
@@ -2466,7 +2390,6 @@ export class Parser {
         //pop the call from the  callExpressions list because this is technically something else
         this.callExpressions.pop();
         let result = new NewExpression(newToken, call);
-        this._references.newExpressions.push(result);
         return result;
     }
 
@@ -2490,22 +2413,14 @@ export class Parser {
             return this.newExpression();
         }
         let expr = this.primary();
-        //an expression to keep for _references
-        let referenceCallExpression: Expression;
+
         while (true) {
             if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
                 expr = this.finishCall(this.previous(), expr);
-                //store this call expression in references
-                referenceCallExpression = expr;
-
             } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
                 expr = this.indexedGet(expr);
-
             } else if (this.match(TokenKind.Callfunc)) {
                 expr = this.callfunc(expr);
-                //store this callfunc expression in references
-                referenceCallExpression = expr;
-
             } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
                 if (this.match(TokenKind.LeftSquareBracket)) {
                     expr = this.indexedGet(expr);
@@ -2523,8 +2438,6 @@ export class Parser {
                     // force it into an identifier so the AST makes some sense
                     name.kind = TokenKind.Identifier;
                     expr = new DottedGetExpression(expr, name as Identifier, dot);
-
-                    this.addPropertyHints(name);
                 }
 
             } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
@@ -2548,10 +2461,7 @@ export class Parser {
                 break;
             }
         }
-        //if we found a callExpression, add it to `expressions` in references
-        if (referenceCallExpression) {
-            this._references.expressions.add(referenceCallExpression);
-        }
+
         return expr;
     }
 
@@ -2645,9 +2555,6 @@ export class Parser {
                 nextToken.kind = TokenKind.Identifier;
             }
             expr = this.identifyingExpression(AllowedTypeIdentifiers);
-            if (expr) {
-                this._references.expressions.add(expr);
-            }
         }
 
         //Check if it has square brackets, thus making it an array
@@ -2666,7 +2573,6 @@ export class Parser {
                 if (this.check(TokenKind.RightSquareBracket)) {
                     const rightBracket = this.advance();
                     expr = new TypedArrayExpression(expr, leftBracket, rightBracket);
-                    this._references.expressions.add(expr);
                 }
             }
         }
@@ -2893,7 +2799,6 @@ export class Parser {
         }
 
         const aaExpr = new AALiteralExpression(members, openingBrace, closingBrace);
-        this.addPropertyHints(aaExpr);
         return aaExpr;
     }
 
@@ -3133,147 +3038,6 @@ export class Parser {
         }
     }
 
-    /**
-     * References are found during the initial parse.
-     * However, sometimes plugins can modify the AST, requiring a full walk to re-compute all references.
-     * This does that walk.
-     */
-    private findReferences() {
-        this._references = new References();
-        const excludedExpressions = new Set<Expression>();
-
-        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
-            for (const p of e.args) {
-                this._references.expressions.add(p);
-            }
-            //add calls that were not excluded (from loop below)
-            if (!excludedExpressions.has(e)) {
-                this._references.expressions.add(e);
-            }
-
-            //if this call is part of a longer expression that includes a call higher up, find that higher one and remove it
-            if (e.callee) {
-                let node: Expression = e.callee;
-                while (node) {
-                    //the primary goal for this loop. If we found a parent call expression, remove it from `references`
-                    if (isCallExpression(node)) {
-                        this.references.expressions.delete(node);
-                        excludedExpressions.add(node);
-                        //stop here. even if there are multiple calls in the chain, each child will find and remove its closest parent, so that reduces excess walking.
-                        break;
-
-                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
-                    } else if (isVariableExpression(node)) {
-                        break;
-                        //if
-
-                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
-                        node = node.obj;
-                    } else {
-                        //some expression we don't understand. log it and quit the loop
-                        this.logger.info('Encountered unknown expression while calculating function expression chain', node);
-                        break;
-                    }
-                }
-            }
-        };
-
-        this.ast.walk(createVisitor({
-            AssignmentStatement: s => {
-                this._references.assignmentStatements.push(s);
-                this.references.expressions.add(s.value);
-            },
-            ClassStatement: s => {
-                this._references.classStatements.push(s);
-            },
-            FieldStatement: s => {
-                if (s.initialValue) {
-                    this._references.expressions.add(s.initialValue);
-                }
-            },
-            NamespaceStatement: s => {
-                this._references.namespaceStatements.push(s);
-            },
-            FunctionStatement: s => {
-                this._references.functionStatements.push(s);
-            },
-            ImportStatement: s => {
-                this._references.importStatements.push(s);
-            },
-            LibraryStatement: s => {
-                this._references.libraryStatements.push(s);
-            },
-            FunctionExpression: (expression, parent) => {
-                if (!isMethodStatement(parent)) {
-                    this._references.functionExpressions.push(expression);
-                }
-            },
-            NewExpression: e => {
-                this._references.newExpressions.push(e);
-                for (const p of e.call.args) {
-                    this._references.expressions.add(p);
-                }
-            },
-            ExpressionStatement: s => {
-                this._references.expressions.add(s.expression);
-            },
-            CallfuncExpression: e => {
-                visitCallExpression(e);
-            },
-            CallExpression: e => {
-                visitCallExpression(e);
-            },
-            AALiteralExpression: e => {
-                this.addPropertyHints(e);
-                this._references.expressions.add(e);
-                for (const member of e.elements) {
-                    if (isAAMemberExpression(member)) {
-                        this._references.expressions.add(member.value);
-                    }
-                }
-            },
-            BinaryExpression: (e, parent) => {
-                //walk the chain of binary expressions and add each one to the list of expressions
-                const expressions: Expression[] = [e];
-                let expression: Expression;
-                while ((expression = expressions.pop())) {
-                    if (isBinaryExpression(expression)) {
-                        expressions.push(expression.left, expression.right);
-                    } else {
-                        this._references.expressions.add(expression);
-                    }
-                }
-            },
-            ArrayLiteralExpression: e => {
-                for (const element of e.elements) {
-                    //keep everything except comments
-                    if (!isCommentStatement(element)) {
-                        this._references.expressions.add(element);
-                    }
-                }
-            },
-            DottedGetExpression: e => {
-                this.addPropertyHints(e.name);
-            },
-            DottedSetStatement: e => {
-                this.addPropertyHints(e.name);
-            },
-            EnumStatement: e => {
-                this._references.enumStatements.push(e);
-            },
-            ConstStatement: s => {
-                this._references.constStatements.push(s);
-            },
-            UnaryExpression: e => {
-                this._references.expressions.add(e);
-            },
-            IncrementStatement: e => {
-                this._references.expressions.add(e);
-            }
-        }), {
-            walkMode: WalkMode.visitAllRecursive
-        });
-    }
 
     public dispose() {
     }
@@ -3295,95 +3059,6 @@ export interface ParseOptions {
     logger?: Logger;
 }
 
-export class References {
-    private cache = new Cache();
-    public assignmentStatements = [] as AssignmentStatement[];
-    public classStatements = [] as ClassStatement[];
-
-    public get classStatementLookup() {
-        if (!this._classStatementLookup) {
-            this._classStatementLookup = new Map();
-            for (const stmt of this.classStatements) {
-                this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
-            }
-        }
-        return this._classStatementLookup;
-    }
-    private _classStatementLookup: Map<string, ClassStatement>;
-
-    public functionExpressions = [] as FunctionExpression[];
-    public functionStatements = [] as FunctionStatement[];
-    /**
-     * A map of function statements, indexed by fully-namespaced lower function name.
-     */
-    public get functionStatementLookup() {
-        if (!this._functionStatementLookup) {
-            this._functionStatementLookup = new Map();
-            for (const stmt of this.functionStatements) {
-                this._functionStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
-            }
-        }
-        return this._functionStatementLookup;
-    }
-    private _functionStatementLookup: Map<string, FunctionStatement>;
-
-    public interfaceStatements = [] as InterfaceStatement[];
-
-    public get interfaceStatementLookup() {
-        if (!this._interfaceStatementLookup) {
-            this._interfaceStatementLookup = new Map();
-            for (const stmt of this.interfaceStatements) {
-                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
-            }
-        }
-        return this._interfaceStatementLookup;
-    }
-    private _interfaceStatementLookup: Map<string, InterfaceStatement>;
-
-    public enumStatements = [] as EnumStatement[];
-
-    public get enumStatementLookup() {
-        return this.cache.getOrAdd('enums', () => {
-            const result = new Map<string, EnumStatement>();
-            for (const stmt of this.enumStatements) {
-                result.set(stmt.fullName.toLowerCase(), stmt);
-            }
-            return result;
-        });
-    }
-
-    public constStatements = [] as ConstStatement[];
-
-    public get constStatementLookup() {
-        return this.cache.getOrAdd('consts', () => {
-            const result = new Map<string, ConstStatement>();
-            for (const stmt of this.constStatements) {
-                result.set(stmt.fullName.toLowerCase(), stmt);
-            }
-            return result;
-        });
-    }
-
-    /**
-     * A collection of full expressions. This excludes intermediary expressions.
-     *
-     * Example 1:
-     * `a.b.c` is composed of `a` (variableExpression)  `.b` (DottedGetExpression) `.c` (DottedGetExpression)
-     * This will only contain the final `.c` DottedGetExpression because `.b` and `a` can both be derived by walking back from the `.c` DottedGetExpression.
-     *
-     * Example 2:
-     * `name.space.doSomething(a.b.c)` will result in 2 entries in this list. the `CallExpression` for `doSomething`, and the `.c` DottedGetExpression.
-     *
-     * Example 3:
-     * `value = SomeEnum.value > 2 or SomeEnum.otherValue < 10` will result in 4 entries. `SomeEnum.value`, `2`, `SomeEnum.otherValue`, `10`
-     */
-    public expressions = new Set<Expression>();
-    public importStatements = [] as ImportStatement[];
-    public libraryStatements = [] as LibraryStatement[];
-    public namespaceStatements = [] as NamespaceStatement[];
-    public newExpressions = [] as NewExpression[];
-    public propertyHints = {} as Record<string, string>;
-}
 
 class CancelStatementError extends Error {
     constructor() {
diff --git a/src/parser/Statement.spec.ts b/src/parser/Statement.spec.ts
index a9812004..6b0948e3 100644
--- a/src/parser/Statement.spec.ts
+++ b/src/parser/Statement.spec.ts
@@ -1,9 +1,9 @@
 import { expect } from '../chai-config.spec';
-import type { NamespaceStatement } from './Statement';
+import type { NamespaceStatement, ClassStatement } from './Statement';
 import { Body, CommentStatement, EmptyStatement } from './Statement';
 import { ParseMode, Parser } from './Parser';
 import { WalkMode } from '../astUtils/visitors';
-import { isNamespaceStatement } from '../astUtils/reflection';
+import { isClassStatement, isNamespaceStatement } from '../astUtils/reflection';
 import { CancellationTokenSource } from 'vscode-languageserver';
 import { Program } from '../Program';
 import { trim } from '../testHelpers.spec';
@@ -88,7 +88,7 @@ describe('Statement', () => {
                     end class
                 `);
                 program.validate();
-                const stmt = file.parser.references.classStatements[0];
+                const stmt = file.ast.findChildren<ClassStatement>(isClassStatement)[0];
                 expect(stmt.getName(ParseMode.BrightScript)).to.equal('Animal');
                 expect(stmt.getName(ParseMode.BrighterScript)).to.equal('Animal');
             });
@@ -100,7 +100,7 @@ describe('Statement', () => {
                     end namespace
                 `);
                 program.validate();
-                const stmt = file.parser.references.classStatements[0];
+                const stmt = file.ast.findChildren<ClassStatement>(isClassStatement)[0];
                 expect(stmt.getName(ParseMode.BrightScript)).to.equal('NameA_Animal');
                 expect(stmt.getName(ParseMode.BrighterScript)).to.equal('NameA.Animal');
             });
diff --git a/src/validators/ClassValidator.ts b/src/validators/ClassValidator.ts
index ed8f0d74..8694ea4f 100644
--- a/src/validators/ClassValidator.ts
+++ b/src/validators/ClassValidator.ts
@@ -59,7 +59,8 @@ export class BsClassValidator {
      */
     private verifyNewExpressions() {
         this.scope.enumerateBrsFiles((file) => {
-            let newExpressions = file.parser.references.newExpressions;
+            // eslint-disable-next-line @typescript-eslint/dot-notation
+            let newExpressions = file['_cachedLookups'].newExpressions;
             for (let newExpression of newExpressions) {
                 let className = newExpression.className.getName(ParseMode.BrighterScript);
                 const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
@@ -310,7 +311,9 @@ export class BsClassValidator {
     private findClasses() {
         this.classes = new Map();
         this.scope.enumerateBrsFiles((file) => {
-            for (let x of file.parser.references.classStatements ?? []) {
+
+            // eslint-disable-next-line @typescript-eslint/dot-notation
+            for (let x of file['_cachedLookups'].classStatements ?? []) {
                 let classStatement = x as AugmentedClassStatement;
                 let name = classStatement.getName(ParseMode.BrighterScript);
                 //skip this class if it doesn't have a name


Test patch:
diff --git a/src/parser/tests/controlFlow/For.spec.ts b/src/parser/tests/controlFlow/For.spec.ts
index 1829bf73..92bd3c44 100644
--- a/src/parser/tests/controlFlow/For.spec.ts
+++ b/src/parser/tests/controlFlow/For.spec.ts
@@ -3,8 +3,10 @@ import { Parser } from '../../Parser';
 import { TokenKind } from '../../../lexer/TokenKind';
 import { EOF, identifier, token } from '../Parser.spec';
 import { Range } from 'vscode-languageserver';
-import type { ForStatement } from '../../Statement';
+import type { FunctionStatement } from '../../Statement';
+import { type ForStatement } from '../../Statement';
 import { LiteralExpression } from '../../Expression';
+import { isFunctionStatement } from '../../../astUtils/reflection';
 
 describe('parser for loops', () => {
     it('accepts a \'step\' clause', () => {
@@ -58,7 +60,8 @@ describe('parser for loops', () => {
         `);
         expect(parser.diagnostics).to.be.lengthOf(1);
         expect(parser.statements).to.be.lengthOf(1);
-        expect(parser.references.functionStatements[0].func.body.statements).to.be.lengthOf(1);
+        const functionStatements = parser.ast.findChildren<FunctionStatement>(isFunctionStatement);
+        expect(functionStatements[0].func.body.statements).to.be.lengthOf(1);
     });
 
     it('allows \'next\' to terminate loop', () => {
diff --git a/src/parser/tests/controlFlow/If.spec.ts b/src/parser/tests/controlFlow/If.spec.ts
index 30eea60e..a26ae1bd 100644
--- a/src/parser/tests/controlFlow/If.spec.ts
+++ b/src/parser/tests/controlFlow/If.spec.ts
@@ -4,8 +4,8 @@ import { Parser } from '../../Parser';
 import { Lexer } from '../../../lexer/Lexer';
 import { TokenKind } from '../../../lexer/TokenKind';
 import { EOF, identifier, rangeMatch, token } from '../Parser.spec';
-import { isBlock, isCommentStatement, isIfStatement } from '../../../astUtils/reflection';
-import type { Block, IfStatement } from '../../Statement';
+import { isBlock, isCommentStatement, isFunctionStatement, isIfStatement } from '../../../astUtils/reflection';
+import type { Block, FunctionStatement, IfStatement } from '../../Statement';
 
 describe('parser if statements', () => {
     it('allows empty if blocks', () => {
@@ -484,10 +484,12 @@ describe('parser if statements', () => {
             function test2()
             end function
         `);
-        let { statements, diagnostics, references } = Parser.parse(tokens);
+        let parser = Parser.parse(tokens);
+        let { statements, diagnostics } = parser;
         expect(diagnostics).to.be.length.greaterThan(0);
         expect(statements).to.be.lengthOf(2);
-        expect(references.functionStatements).to.be.lengthOf(2);
+        const functionStatements = parser.ast.findChildren<FunctionStatement>(isFunctionStatement);
+        expect(functionStatements).to.be.lengthOf(2);
     });
 
     it('catches extraneous colon at the end of one-line if', () => {
diff --git a/src/parser/tests/controlFlow/While.spec.ts b/src/parser/tests/controlFlow/While.spec.ts
index c9d00247..9ce08c0d 100644
--- a/src/parser/tests/controlFlow/While.spec.ts
+++ b/src/parser/tests/controlFlow/While.spec.ts
@@ -4,6 +4,8 @@ import { Parser } from '../../Parser';
 import { TokenKind } from '../../../lexer/TokenKind';
 import { EOF, identifier, token } from '../Parser.spec';
 import { Range } from 'vscode-languageserver';
+import type { FunctionStatement } from '../../Statement';
+import { isFunctionStatement } from '../../../astUtils/reflection';
 
 describe('parser while statements', () => {
 
@@ -59,7 +61,8 @@ describe('parser while statements', () => {
         `);
         expect(parser.diagnostics).to.be.lengthOf(1);
         expect(parser.statements).to.be.lengthOf(1);
-        expect(parser.references.functionStatements[0].func.body.statements).to.be.lengthOf(1);
+        const functionStatements = parser.ast.findChildren<FunctionStatement>(isFunctionStatement);
+        expect(functionStatements[0].func.body.statements).to.be.lengthOf(1);
     });
 
     it('location tracking', () => {
diff --git a/src/parser/tests/statement/Enum.spec.ts b/src/parser/tests/statement/Enum.spec.ts
index a0c136c7..cfa09847 100644
--- a/src/parser/tests/statement/Enum.spec.ts
+++ b/src/parser/tests/statement/Enum.spec.ts
@@ -195,7 +195,7 @@ describe('EnumStatement', () => {
         program.validate();
 
         expectZeroDiagnostics(program);
-        expect(file.parser.references.enumStatements.map(x => x.fullName)).to.eql([
+        expect(file.ast.findChildren<EnumStatement>(isEnumStatement).map(x => x.fullName)).to.eql([
             'entities.Person',
             'Direction'
         ]);
diff --git a/src/parser/tests/statement/Misc.spec.ts b/src/parser/tests/statement/Misc.spec.ts
index cef3684f..6efe5b3b 100644
--- a/src/parser/tests/statement/Misc.spec.ts
+++ b/src/parser/tests/statement/Misc.spec.ts
@@ -284,73 +284,4 @@ describe('parser', () => {
         expect(element.keyToken.text).to.equal('"has-second-layer"');
     });
 
-    it('extracts property names for completion', () => {
-        const { tokens } = Lexer.scan(`
-            function main(arg as string)
-                aa1 = {
-                    "sprop1": 0,
-                    prop1: 1
-                    prop2: {
-                        prop3: 2
-                    }
-                }
-                aa2 = {
-                    prop4: {
-                        prop5: 5,
-                        "sprop2": 0,
-                        prop6: 6
-                    },
-                    prop7: 7
-                }
-                calling({
-                    prop8: 8,
-                    prop9: 9
-                })
-                aa1.field1 = 1
-                aa1.field2.field3 = 2
-                calling(aa2.field4, 3 + aa2.field5.field6)
-            end function
-        `);
-
-        const expected = [
-            'field1', 'field2', 'field3', 'field4', 'field5', 'field6',
-            'prop1', 'prop2', 'prop3', 'prop4', 'prop5', 'prop6', 'prop7', 'prop8', 'prop9'
-        ];
-
-        const parser = Parser.parse(tokens);
-        const { propertyHints: initialHints } = parser.references;
-        expect(Object.keys(initialHints).sort()).to.deep.equal(expected, 'Initial hints');
-
-        parser.invalidateReferences();
-        const { propertyHints: refreshedHints } = parser.references;
-        expect(Object.keys(refreshedHints).sort()).to.deep.equal(expected, 'Refreshed hints');
-    });
-
-    it('extracts property names matching JavaScript reserved names', () => {
-        const { tokens } = Lexer.scan(`
-            function main(arg as string)
-                aa1 = {
-                    "constructor": 0,
-                    constructor: 1
-                    valueOf: {
-                        toString: 2
-                    }
-                }
-                aa1.constructor = 1
-                aa1.valueOf.toString = 2
-            end function
-        `);
-
-        const expected = [
-            'constructor', 'tostring', 'valueof'
-        ];
-
-        const parser = Parser.parse(tokens);
-        const { propertyHints: initialHints } = parser.references;
-        expect(Object.keys(initialHints).sort()).to.deep.equal(expected, 'Initial hints');
-
-        parser.invalidateReferences();
-        const { propertyHints: refreshedHints } = parser.references;
-        expect(Object.keys(refreshedHints).sort()).to.deep.equal(expected, 'Refreshed hints');
-    });
 });
diff --git a/src/parser/tests/statement/TryCatch.spec.ts b/src/parser/tests/statement/TryCatch.spec.ts
index 4c35d3db..ed89ca04 100644
--- a/src/parser/tests/statement/TryCatch.spec.ts
+++ b/src/parser/tests/statement/TryCatch.spec.ts
@@ -1,6 +1,8 @@
 import { expect } from '../../../chai-config.spec';
 import { Parser } from '../../Parser';
 import { TryCatchStatement } from '../../Statement';
+import { isFunctionExpression } from '../../../astUtils/reflection';
+import type { FunctionExpression } from '../../..';
 
 describe('parser try/catch', () => {
     it('can parse try catch statements', () => {
@@ -14,7 +16,9 @@ describe('parser try/catch', () => {
             end sub
         `);
         expect(parser.diagnostics[0]?.message).not.to.exist;
-        const stmt = parser.references.functionExpressions[0].body.statements[0] as TryCatchStatement;
+
+        const functionExpressions = parser.ast.findChildren<FunctionExpression>(isFunctionExpression);
+        const stmt = functionExpressions[0].body.statements[0] as TryCatchStatement;
         expect(stmt).to.be.instanceof(TryCatchStatement);
         expect(stmt.tokens.try?.text).to.eql('try');
         expect(stmt.tryBranch).to.exist.and.ownProperty('statements').to.be.lengthOf(1);