Repository: rokucommunity/brighterscript
Pull request #621 (2022-06-09 19:04:05)

Better enum/class/namespace semantic tokens
Adds much better syntax highlighting (via semantic tokens) for enums, classes, and namespaces.
**Before:**
![image](https://user-images.githubusercontent.com/2544493/172925268-8554583a-8654-41e3-ad1d-fee08c4bb55f.png)

**After:**
![image](https://user-images.githubusercontent.com/2544493/172925158-2b356ef8-b069-4240-8793-a21c2d1302b5.png)



Patch:
diff --git a/src/Scope.ts b/src/Scope.ts
index fd439602..f8e50f7b 100644
--- a/src/Scope.ts
+++ b/src/Scope.ts
@@ -7,7 +7,7 @@ import { DiagnosticMessages } from './DiagnosticMessages';
 import type { CallableContainer, BsDiagnostic, FileReference, BscFile, CallableContainerMap, FileLink } from './interfaces';
 import type { Program } from './Program';
 import { BsClassValidator } from './validators/ClassValidator';
-import type { NamespaceStatement, Statement, FunctionStatement, ClassStatement, EnumStatement, InterfaceStatement } from './parser/Statement';
+import type { NamespaceStatement, Statement, FunctionStatement, ClassStatement, EnumStatement, InterfaceStatement, EnumMemberStatement } from './parser/Statement';
 import type { NewExpression } from './parser/Expression';
 import { ParseMode } from './parser/Parser';
 import { standardizePath as s, util } from './util';
@@ -139,6 +139,23 @@ export class Scope {
         return enumeration;
     }
 
+    /**
+     * Get a map of all enums by their member name.
+     * The keys are lower-case fully-qualified paths to the enum and its member. For example:
+     * namespace.enum.value
+     */
+    public getEnumMemberMap() {
+        return this.cache.getOrAdd('enumMemberMap', () => {
+            const result = new Map<string, EnumMemberStatement>();
+            for (const [key, eenum] of this.getEnumMap()) {
+                for (const member of eenum.item.getMembers()) {
+                    result.set(`${key}.${member.name.toLowerCase()}`, member);
+                }
+            }
+            return result;
+        });
+    }
+
     /**
     * Tests if a class exists with the specified name
     * @param className - the all-lower-case namespace-included class name
diff --git a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts
index 2591ea64..df97941e 100644
--- a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts
+++ b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts
@@ -1,6 +1,7 @@
 import { expect } from 'chai';
 import { SemanticTokenTypes } from 'vscode-languageserver-protocol';
 import type { BrsFile } from '../../files/BrsFile';
+import type { BscFile, SemanticToken } from '../../interfaces';
 import { Program } from '../../Program';
 import { expectZeroDiagnostics } from '../../testHelpers.spec';
 import { standardizePath as s, util } from '../../util';
@@ -18,35 +19,138 @@ describe('BrsFileSemanticTokensProcessor', () => {
         program.dispose();
     });
 
+    function expectSemanticTokens(file: BscFile, tokens: SemanticToken[]) {
+        program.validate();
+        expectZeroDiagnostics(program);
+        expect(
+            util.sortByRange(
+                program.getSemanticTokens(file.srcPath)
+            )
+        ).to.eql(
+            util.sortByRange(
+                tokens
+            )
+        );
+    }
+
     it('matches each namespace section for class', () => {
         const file = program.setFile<BrsFile>('source/main.bs', `
-            namespace Earthlings.Humanoids
-                class Person
-                end class
-            end namespace
-            class Dog
+            class Host
                 sub new()
-                    m.owner = new Earthlings.Humanoids.Person()
+                    m.alien = new Humanoids.Aliens.Alien()
                 end sub
             end class
+
+            namespace Humanoids.Aliens
+                class Alien
+                end class
+            end namespace
         `);
-        program.validate();
-        expectZeroDiagnostics(program);
-        expect(
-            program.getSemanticTokens(file.srcPath)
-        ).to.eql([{
-            range: util.createRange(7, 34, 7, 44),
+        expectSemanticTokens(file, [{
+            range: util.createRange(3, 34, 3, 43),
             tokenType: SemanticTokenTypes.namespace
         }, {
-            range: util.createRange(7, 45, 7, 54),
+            range: util.createRange(3, 44, 3, 50),
             tokenType: SemanticTokenTypes.namespace
         }, {
-            range: util.createRange(7, 55, 7, 61),
+            range: util.createRange(3, 51, 3, 56),
             tokenType: SemanticTokenTypes.class
         }]);
     });
 
-    it('matches each namespace section', () => {
+    it('matches each namespace section for namespaced function calls', () => {
+        const file = program.setFile<BrsFile>('source/main.bs', `
+            sub new()
+                Humanoids.Aliens.Invade("earth")
+            end sub
+            namespace Humanoids.Aliens
+                function Invade(planet)
+                end function
+            end namespace
+        `);
+        expectSemanticTokens(file, [{
+            range: util.createRange(2, 16, 2, 25),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 26, 2, 32),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 33, 2, 39),
+            tokenType: SemanticTokenTypes.function
+        }]);
+    });
+
+    it('matches each namespace section for namespaced function assignment', () => {
+        const file = program.setFile<BrsFile>('source/main.bs', `
+            sub new()
+                action = Humanoids.Aliens.Invade
+            end sub
+            namespace Humanoids.Aliens
+                function Invade(planet)
+                end function
+            end namespace
+        `);
+        expectSemanticTokens(file, [{
+            range: util.createRange(2, 25, 2, 34),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 35, 2, 41),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 42, 2, 48),
+            tokenType: SemanticTokenTypes.function
+        }]);
+    });
+
+    it('matches each namespace section for namespaced function as function parameter', () => {
+        const file = program.setFile<BrsFile>('source/main.bs', `
+            sub new()
+                actionName = type(Humanoids.Aliens.Invade)
+            end sub
+            namespace Humanoids.Aliens
+                function Invade(planet)
+                end function
+            end namespace
+        `);
+        expectSemanticTokens(file, [{
+            //`type` function call
+            range: util.createRange(2, 29, 2, 33),
+            tokenType: SemanticTokenTypes.function
+        }, {
+            range: util.createRange(2, 34, 2, 43),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 44, 2, 50),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 51, 2, 57),
+            tokenType: SemanticTokenTypes.function
+        }]);
+    });
+
+    it('matches each namespace section for namespaced function in print statement', () => {
+        const file = program.setFile<BrsFile>('source/main.bs', `
+            sub new()
+                print Humanoids.Aliens.Invade
+            end sub
+            namespace Humanoids.Aliens
+                function Invade(planet)
+                end function
+            end namespace
+        `);
+        expectSemanticTokens(file, [{
+            range: util.createRange(2, 22, 2, 31),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 32, 2, 38),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 39, 2, 45),
+            tokenType: SemanticTokenTypes.function
+        }]);
+    });
+
+    it('matches each namespace section for enums', () => {
         const file = program.setFile<BrsFile>('source/main.bs', `
             sub main()
                 print Earthlings.Species.Human.Male
@@ -58,11 +162,7 @@ describe('BrsFileSemanticTokensProcessor', () => {
                 end enum
             end namespace
         `);
-        program.validate();
-        expectZeroDiagnostics(program);
-        expect(
-            util.sortByRange(program.getSemanticTokens(file.srcPath))
-        ).to.eql([{
+        expectSemanticTokens(file, [{
             range: util.createRange(2, 22, 2, 32),
             tokenType: SemanticTokenTypes.namespace
         }, {
@@ -90,11 +190,7 @@ describe('BrsFileSemanticTokensProcessor', () => {
                 end enum
             end namespace
         `);
-        program.validate();
-        expectZeroDiagnostics(program);
-        expect(
-            util.sortByRange(program.getSemanticTokens(file.srcPath))
-        ).to.eql([{
+        expectSemanticTokens(file, [{
             range: util.createRange(2, 22, 2, 31),
             tokenType: SemanticTokenTypes.namespace
         }, {
@@ -124,11 +220,7 @@ describe('BrsFileSemanticTokensProcessor', () => {
                 end enum
             end namespace
         `);
-        program.validate();
-        expectZeroDiagnostics(program);
-        expect(
-            util.sortByRange(program.getSemanticTokens(file.srcPath))
-        ).to.eql([{
+        expectSemanticTokens(file, [{
             range: util.createRange(2, 19, 2, 28),
             tokenType: SemanticTokenTypes.namespace
         }, {
@@ -140,4 +232,48 @@ describe('BrsFileSemanticTokensProcessor', () => {
         }]);
     });
 
+    it('matches enum with invalid member name', () => {
+        const file = program.setFile<BrsFile>('source/main.bs', `
+            sub main()
+                print Humanoids.HumanoidType.INVALID_VALUE 'bs:disable-line
+            end sub
+            namespace Humanoids
+                enum HumanoidType
+                    Human = "Human"
+                    Alien = "Alien"
+                    Cylon = "Cylon"
+                end enum
+            end namespace
+        `);
+        expectSemanticTokens(file, [{
+            range: util.createRange(2, 22, 2, 31),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 32, 2, 44),
+            tokenType: SemanticTokenTypes.enum
+        }]);
+    });
+
+    it('matches class with invalid stuff after it', () => {
+        const file = program.setFile<BrsFile>('source/main.bs', `
+            sub init()
+                m.alien = new Humanoids.Aliens.Alien.NOT_A_CLASS() 'bs:disable-line
+            end sub
+
+            namespace Humanoids.Aliens
+                class Alien
+                end class
+            end namespace
+        `);
+        expectSemanticTokens(file, [{
+            range: util.createRange(2, 30, 2, 39),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 40, 2, 46),
+            tokenType: SemanticTokenTypes.namespace
+        }, {
+            range: util.createRange(2, 47, 2, 52),
+            tokenType: SemanticTokenTypes.class
+        }]);
+    });
 });
diff --git a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts
index f78d487d..2d21de55 100644
--- a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts
+++ b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts
@@ -1,8 +1,9 @@
 import type { Range } from 'vscode-languageserver-protocol';
 import { SemanticTokenTypes } from 'vscode-languageserver-protocol';
-import { isBinaryExpression, isCustomType } from '../../astUtils/reflection';
+import { isBinaryExpression, isCallExpression, isCustomType, isNewExpression } from '../../astUtils/reflection';
 import type { BrsFile } from '../../files/BrsFile';
 import type { OnGetSemanticTokensEvent } from '../../interfaces';
+import type { Locatable } from '../../lexer/Token';
 import type { Expression } from '../../parser/Expression';
 import { ParseMode } from '../../parser/Parser';
 import util from '../../util';
@@ -16,7 +17,7 @@ export class BrsFileSemanticTokensProcessor {
 
     public process() {
         this.handleClasses();
-        this.handleEnums();
+        this.iterateExpressions();
     }
 
     private handleClasses() {
@@ -35,14 +36,6 @@ export class BrsFileSemanticTokensProcessor {
                 }
             }
         }
-        //classes used in `new` expressions
-        for (const expr of this.event.file.parser.references.newExpressions) {
-            classes.push({
-                className: expr.className.getName(ParseMode.BrighterScript),
-                namespaceName: expr.namespaceName?.getName(ParseMode.BrighterScript),
-                range: expr.className.range
-            });
-        }
 
         for (const cls of classes) {
             if (
@@ -51,25 +44,39 @@ export class BrsFileSemanticTokensProcessor {
                 this.event.scopes.some(x => x.hasClass(cls.className, cls.namespaceName))
             ) {
                 const tokens = util.splitGetRange('.', cls.className, cls.range);
-                //namespace parts (skip the final array entry)
-                for (let i = 0; i < tokens.length - 1; i++) {
-                    const token = tokens[i];
-                    this.event.semanticTokens.push({
-                        range: token.range,
-                        tokenType: SemanticTokenTypes.namespace
-                    });
-                }
-                //class name
-                this.event.semanticTokens.push({
-                    range: tokens.pop().range,
-                    tokenType: SemanticTokenTypes.class
-                });
+                this.addTokens(tokens.reverse(), SemanticTokenTypes.class, SemanticTokenTypes.namespace);
+            }
+        }
+    }
+
+    /**
+     * Add tokens for each locatable item in the list.
+     * Each locatable is paired with a token type. If there are more locatables than token types, all remaining locatables are given the final token type
+     */
+    private addTokens(locatables: Locatable[], ...semanticTokenTypes: SemanticTokenTypes[]) {
+        for (let i = 0; i < locatables.length; i++) {
+            const locatable = locatables[i];
+            //skip items that don't have a location
+            if (locatable?.range) {
+                this.addToken(
+                    locatables[i],
+                    //use the type at the index, or the last type if missing
+                    semanticTokenTypes[i] ?? semanticTokenTypes[semanticTokenTypes.length - 1]
+                );
             }
         }
     }
 
-    private handleEnums() {
-        const enumLookup = this.event.file.program.getFirstScopeForFile(this.event.file)?.getEnumMap();
+    private addToken(locatable: Locatable, type: SemanticTokenTypes) {
+        this.event.semanticTokens.push({
+            range: locatable.range,
+            tokenType: type
+        });
+    }
+
+    private iterateExpressions() {
+        const scope = this.event.scopes[0];
+
         for (const referenceExpression of this.event.file.parser.references.expressions) {
             const actualExpressions: Expression[] = [];
             //binary expressions actually have two expressions (left and right), so handle them independently
@@ -79,34 +86,29 @@ export class BrsFileSemanticTokensProcessor {
                 //assume all other expressions are a single chain
                 actualExpressions.push(referenceExpression);
             }
-            for (const expression of actualExpressions) {
-                const parts = util.getAllDottedGetParts(expression)?.map(x => x.toLowerCase());
-                if (parts) {
-                    //discard the enum member name
-                    const memberName = parts.pop();
-                    //get the name of the enum (including leading namespace if applicable)
-                    const enumName = parts.join('.');
-                    const lowerEnumName = enumName.toLowerCase();
-                    const theEnum = enumLookup.get(lowerEnumName)?.item;
-                    if (theEnum) {
-                        const tokens = util.splitGetRange('.', lowerEnumName + '.' + memberName, expression.range);
-                        //enum member name
-                        this.event.semanticTokens.push({
-                            range: tokens.pop().range,
-                            tokenType: SemanticTokenTypes.enumMember
-                        });
-                        //enum name
-                        this.event.semanticTokens.push({
-                            range: tokens.pop().range,
-                            tokenType: SemanticTokenTypes.enum
-                        });
-                        //namespace parts
-                        for (const token of tokens) {
-                            this.event.semanticTokens.push({
-                                range: token.range,
-                                tokenType: SemanticTokenTypes.namespace
-                            });
-                        }
+            for (let expression of actualExpressions) {
+                //lift the callee from call expressions to handle namespaced function calls
+                if (isCallExpression(expression)) {
+                    expression = expression.callee;
+                } else if (isNewExpression(expression)) {
+                    expression = expression.call.callee;
+                }
+                const tokens = util.getAllDottedGetParts(expression);
+                const processedNames: string[] = [];
+                for (const token of tokens ?? []) {
+                    processedNames.push(token.text?.toLowerCase());
+                    const entityName = processedNames.join('.');
+
+                    if (scope.getEnumMemberMap().has(entityName)) {
+                        this.addToken(token, SemanticTokenTypes.enumMember);
+                    } else if (scope.getEnumMap().has(entityName)) {
+                        this.addToken(token, SemanticTokenTypes.enum);
+                    } else if (scope.getClassMap().has(entityName)) {
+                        this.addToken(token, SemanticTokenTypes.class);
+                    } else if (scope.getCallableByName(entityName)) {
+                        this.addToken(token, SemanticTokenTypes.function);
+                    } else if (scope.namespaceLookup.has(entityName)) {
+                        this.addToken(token, SemanticTokenTypes.namespace);
                     }
                 }
             }
diff --git a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts
index 3e41f260..d433bf47 100644
--- a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts
+++ b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts
@@ -33,7 +33,7 @@ export class BrsFilePreTranspileProcessor {
                 actualExpressions.push(referenceExpression);
             }
             for (const expression of actualExpressions) {
-                const parts = util.getAllDottedGetParts(expression)?.map(x => x.toLowerCase());
+                const parts = util.getAllDottedGetParts(expression)?.map(x => x.text.toLowerCase());
                 if (parts) {
                     //get the name of the enum member
                     const memberName = parts.pop();
diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts
index 2cf6eec0..099669c0 100644
--- a/src/bscPlugin/validation/ScopeValidator.ts
+++ b/src/bscPlugin/validation/ScopeValidator.ts
@@ -1,11 +1,11 @@
 import { Location } from 'vscode-languageserver';
 import { URI } from 'vscode-uri';
-import { isBrsFile, isLiteralExpression } from '../../astUtils/reflection';
+import { isBinaryExpression, isBrsFile, isLiteralExpression } from '../../astUtils/reflection';
 import { Cache } from '../../Cache';
 import { DiagnosticMessages } from '../../DiagnosticMessages';
 import type { BrsFile } from '../../files/BrsFile';
 import type { BscFile, BsDiagnostic, OnScopeValidateEvent } from '../../interfaces';
-import type { DottedGetExpression } from '../../parser/Expression';
+import type { Expression } from '../../parser/Expression';
 import type { EnumStatement } from '../../parser/Statement';
 import util from '../../util';
 import { nodes, components } from '../../roku-types';
@@ -59,7 +59,6 @@ export class ScopeValidator {
     public validateEnumUsage(event: OnScopeValidateEvent) {
         const diagnostics = [] as BsDiagnostic[];
 
-        const membersByEnum = new Cache<string, Map<string, string>>();
         //if there are any enums defined in this scope
         const enumLookup = event.scope.getEnumMap();
 
@@ -74,34 +73,40 @@ export class ScopeValidator {
                 return;
             }
 
-            for (const expression of file.parser.references.expressions as Set<DottedGetExpression>) {
-                const parts = util.getAllDottedGetParts(expression);
-                //skip expressions that aren't fully dotted gets
-                if (!parts) {
-                    continue;
+            // const namespaceLookup = this.event.scopes[0]?.namespaceLookup;
+            for (const referenceExpression of file.parser.references.expressions) {
+                const actualExpressions: Expression[] = [];
+                //binary expressions actually have two expressions (left and right), so handle them independently
+                if (isBinaryExpression(referenceExpression)) {
+                    actualExpressions.push(referenceExpression.left, referenceExpression.right);
+                } else {
+                    //assume all other expressions are a single chain
+                    actualExpressions.push(referenceExpression);
                 }
-                //get the name of the enum member
-                const memberName = parts.pop();
-                //get the name of the enum (including leading namespace if applicable)
-                const enumName = parts.join('.');
-                const lowerEnumName = enumName.toLowerCase();
-                const theEnum = enumLookup.get(lowerEnumName)?.item;
-                if (theEnum) {
-                    const members = membersByEnum.getOrAdd(lowerEnumName, () => theEnum.getMemberValueMap());
-                    const value = members?.get(memberName.toLowerCase());
-                    if (!value) {
-                        diagnostics.push({
-                            file: file,
-                            ...DiagnosticMessages.unknownEnumValue(memberName, theEnum.fullName),
-                            range: expression.name.range,
-                            relatedInformation: [{
-                                message: 'Enum declared here',
-                                location: Location.create(
-                                    URI.file(file.srcPath).toString(),
-                                    theEnum.tokens.name.range
-                                )
-                            }]
-                        });
+                for (let expression of actualExpressions) {
+                    const tokens = util.getAllDottedGetParts(expression);
+                    if (tokens) {
+                        //build the full name of the thing
+                        const parts = tokens.map(x => x.text);
+                        const fullName = parts.join('.');
+                        const memberName = parts.pop();
+                        const fullParentName = parts.join('.');
+                        const theEnum = event.scope.getEnumMap().get(fullParentName.toLowerCase())?.item;
+                        //is an enum and this field does not exist
+                        if (theEnum && !event.scope.getEnumMemberMap().has(fullName.toLowerCase())) {
+                            diagnostics.push({
+                                file: file,
+                                ...DiagnosticMessages.unknownEnumValue(memberName, fullParentName),
+                                range: tokens[tokens.length - 1].range,
+                                relatedInformation: [{
+                                    message: 'Enum declared here',
+                                    location: Location.create(
+                                        URI.file(file.srcPath).toString(),
+                                        theEnum.tokens.name.range
+                                    )
+                                }]
+                            });
+                        }
                     }
                 }
             }
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 6a41cbe7..3066b62d 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -241,9 +241,21 @@ export interface OnGetCodeActionsEvent {
 }
 
 export interface OnGetSemanticTokensEvent<T extends BscFile = BscFile> {
+    /**
+     * The program this file is from
+     */
     program: Program;
+    /**
+     * The file to get semantic tokens for
+     */
     file: T;
+    /**
+     * The list of scopes that this file is a member of
+     */
     scopes: Scope[];
+    /**
+     * The list of semantic tokens being produced during this event.
+     */
     semanticTokens: SemanticToken[];
 }
 
diff --git a/src/util.ts b/src/util.ts
index 9938d87f..46ae0d95 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -24,9 +24,9 @@ import { VoidType } from './types/VoidType';
 import { ParseMode } from './parser/Parser';
 import type { DottedGetExpression, Expression, VariableExpression } from './parser/Expression';
 import { Logger, LogLevel } from './Logger';
-import type { Locatable, Token } from './lexer/Token';
+import type { Identifier, Locatable, Token } from './lexer/Token';
 import { TokenKind } from './lexer/TokenKind';
-import { isDottedGetExpression, isExpression, isVariableExpression } from './astUtils/reflection';
+import { isDottedGetExpression, isExpression, isNamespacedVariableNameExpression, isVariableExpression } from './astUtils/reflection';
 import { WalkMode } from './astUtils/visitors';
 import { CustomType } from './types/CustomType';
 import { SourceNode } from 'source-map';
@@ -1299,15 +1299,17 @@ export class Util {
      * @param expression
      * @returns an array of the parts of the dotted get. If not fully a dotted get, then returns undefined
      */
-    public getAllDottedGetParts(expression: Expression): string[] | undefined {
-        const parts: string[] = [];
+    public getAllDottedGetParts(expression: Expression): Identifier[] | undefined {
+        const parts: Identifier[] = [];
         let nextPart = expression;
         while (nextPart) {
             if (isDottedGetExpression(nextPart)) {
-                parts.push(nextPart?.name?.text);
+                parts.push(nextPart?.name);
                 nextPart = nextPart.obj;
+            } else if (isNamespacedVariableNameExpression(nextPart)) {
+                nextPart = nextPart.expression;
             } else if (isVariableExpression(nextPart)) {
-                parts.push(nextPart?.name?.text);
+                parts.push(nextPart?.name);
                 break;
             } else {
                 //we found a non-DottedGet expression, so return because this whole operation is invalid.


Test patch:
diff --git a/src/parser/tests/statement/Enum.spec.ts b/src/parser/tests/statement/Enum.spec.ts
index 2606942a..4a6d0afe 100644
--- a/src/parser/tests/statement/Enum.spec.ts
+++ b/src/parser/tests/statement/Enum.spec.ts
@@ -399,25 +399,25 @@ describe('EnumStatement', () => {
 
         it('catches unknown namespaced enum members', () => {
             program.setFile('source/main.bs', `
+                sub main()
+                    print Enums.Direction.DOWN
+                    print Enums.Direction.down
+                    print Enums.Direction.up
+                end sub
                 namespace Enums
                     enum Direction
                         up
                     end enum
                 end namespace
 
-                sub main()
-                    print Enums.Direction.up
-                    print Enums.Direction.DOWN
-                    print Enums.Direction.down
-                end sub
             `);
             program.validate();
             expectDiagnostics(program, [{
                 ...DiagnosticMessages.unknownEnumValue('DOWN', 'Enums.Direction'),
-                range: util.createRange(9, 42, 9, 46)
+                range: util.createRange(2, 42, 2, 46)
             }, {
                 ...DiagnosticMessages.unknownEnumValue('down', 'Enums.Direction'),
-                range: util.createRange(10, 42, 10, 46)
+                range: util.createRange(3, 42, 3, 46)
             }]);
         });
     });