Repository: rokucommunity/brighterscript
Pull request #496 (2022-01-28 14:27:47)

Adds Symbol Tables to InterfaceTypes - general refactor of Interfaces
- Added Symbol tables to `InterfaceType`
- Refactored assignability to use symbol tables
- Refactored `InterfaceStatment` members - particularly `InterfaceMethodStatement` to more closely align with `ClassMethodStatement`
- Refactored InterfaceStatement parsing to use existing `functionDeclaration()` method
- `functionDeclaration()` changed to have an `options` parameter instead of flags, to make it more generic
- Added validation on Interfaces and interface members.
- Refactored `ClassValidator` to work with both interfaces and classes
- Token symbol lookup is compatible with Interfaces

Still TODO for interfaces:
- Hover support
- `implements` keyword
- Validation on `implements` keyword (e.g. if you're a class and you say you implement an Interface, make sure you do)

Lots of changes. Sorry!

markwpearce: 

https://user-images.githubusercontent.com/810290/151564081-408326ef-15fb-43b4-b928-1a1e2089ba5b.mov


markwpearce: Hey Bronley - can you take a look at some of my further changes?

Patch:
diff --git a/docs/interfaces.md b/docs/interfaces.md
new file mode 100644
index 00000000..38fd64b9
--- /dev/null
+++ b/docs/interfaces.md
@@ -0,0 +1,64 @@
+# BrighterScript Interfaces
+
+Not to be confused with SceneGraph interfaces (see [<interfaces>](https://developer.roku.com/en-ca/docs/references/scenegraph/xml-elements/interface.md)), BrighterScript introduces the `interface` keyword to describe the public methods and/or fields available on a [class](class.md). This has the benefit of allowing type-checking and validation on different classes as long as they expose the same public functions.
+
+## Interfaces
+
+Interfaces are declared similarly to classes, with the exception of not needing any implementation fo methods, no access modifiers (i.e. nothing is `private` in an `interface`), and nothing is initialized.
+
+```vb
+interface Vehicle
+    name as string
+
+    sub driveKm(distanceKm as float)
+end interface
+```
+
+Since an `interface` is simply a description of a class, it is not transpiled. Interfaces are only useful at compile-time to do type checking and validation. They will not result in any additional transpiled code.
+
+## Type Checking
+
+BrighterScript will validate function calls that use interface types.
+
+For example, (using the same `Vehicle` interface above), there may be two classes that implement the same interface, `Car` and `Truck`:
+
+```vb
+class Car
+    name as string
+
+    sub new(name as string)
+        m.name = name
+    end sub
+
+    sub driveKm(distanceKm as float)
+        ' some implementation
+    end sub
+end class
+
+class Truck
+    name as string = "F150"
+
+    sub driveKm(distanceKm as float)
+        ' some implementation
+    end sub
+end class
+```
+
+Then we could create a function that takes a `Vehicle` parameter and either a `Car` or a `Truck` would pass validation:
+
+```vb
+
+sub driveVehicle100Km(someVehicle as Vehicle)
+    print `Driving ${someVehicle.name} 100 km`
+    someVehicle.distanceKm(100)
+end sub
+
+
+sub main()
+    bmw = new Car("BMW")
+    ford = new Truck()
+
+    driveVehicle100Km(bmw) ' bmw is a Car, which implements Vehicle
+    driveVehicle100Km(ford) ' ford is a Truck, which implements Vehicle
+end sub
+```
diff --git a/docs/readme.md b/docs/readme.md
index d7e876e9..5f7aee5e 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -7,6 +7,7 @@ See the following pages for more information
  - [Callfunc Operator](callfunc-operator.md)
  - [Classes](classes.md)
  - [Enums](enums.md)
+ - [Interfaces](interfaces.md)
  - [Imports](imports.md)
  - [Namespaces](namespaces.md)
  - [Null-coalescing operator](null-coalescing-operator.md)
diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts
index 9d842fc1..db84b308 100644
--- a/src/DiagnosticMessages.ts
+++ b/src/DiagnosticMessages.ts
@@ -660,6 +660,21 @@ export let DiagnosticMessages = {
         message: `Argument of type '${actualTypeString}' is not assignable to parameter of type '${expectedTypeString}'`,
         code: 1128,
         severity: DiagnosticSeverity.Error
+    }),
+    duplicateInterfaceDeclaration: (scopeName: string, ifaceName: string) => ({
+        message: `Scope '${scopeName}' already contains an interface with name '${ifaceName}'`,
+        code: 1129,
+        severity: DiagnosticSeverity.Error
+    }),
+    namespacedInterfaceCannotShareNameWithNonNamespacedInterface: (nonNamespacedInterfaceName: string) => ({
+        message: `Namespaced interface cannot have the same name as a non-namespaced interface '${nonNamespacedInterfaceName}'`,
+        code: 1130,
+        severity: DiagnosticSeverity.Error
+    }),
+    classCannotShareNamewithInterface: (className: string) => ({
+        message: `Class cannot have the same name as interface '${className}'`,
+        code: 1131,
+        severity: DiagnosticSeverity.Error
     })
 };
 
diff --git a/src/Program.spec.ts b/src/Program.spec.ts
index 0ce641f2..2ccfb4ae 100644
--- a/src/Program.spec.ts
+++ b/src/Program.spec.ts
@@ -1591,7 +1591,7 @@ describe('Program', () => {
             program.setFile('source/main.brs', '');
             let completions = program.getCompletions(`${rootDir}/source/main.brs`, position);
             //get the name of all global completions
-            const globalCompletions = program.globalScope.getAllFiles().flatMap(x => x.getCompletions(position)).map(x => x.label);
+            const globalCompletions = program.globalScope.getAllFiles().flatMap(x => x.getCompletions(position, program.globalScope)).map(x => x.label);
             //filter out completions from global scope
             completions = completions.filter(x => !globalCompletions.includes(x.label));
             expect(completions).to.be.empty;
diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts
index f569b2e3..7e0393bd 100644
--- a/src/Scope.spec.ts
+++ b/src/Scope.spec.ts
@@ -1,7 +1,7 @@
 import { expect } from 'chai';
 import * as sinonImport from 'sinon';
 import { Position, Range } from 'vscode-languageserver';
-import { standardizePath as s } from './util';
+import { standardizePath as s, util } from './util';
 import { DiagnosticMessages } from './DiagnosticMessages';
 import { Program } from './Program';
 import { ParseMode } from './parser/Parser';
@@ -12,7 +12,7 @@ import type { BrsFile } from './files/BrsFile';
 import type { FunctionStatement, NamespaceStatement } from './parser/Statement';
 import type { OnScopeValidateEvent } from './interfaces';
 import type { TypedFunctionType } from './types/TypedFunctionType';
-import { isFloatType, isFunctionType } from './astUtils/reflection';
+import { isFloatType, isFunctionType, isInterfaceStatement, isInterfaceType } from './astUtils/reflection';
 import type { SymbolTable } from './SymbolTable';
 import type { Scope } from './Scope';
 import { FunctionType } from './types/FunctionType';
@@ -744,6 +744,9 @@ describe('Scope', () => {
             program.setFile('source/main.bs', `
             class PiHolder
                 pi = 3.14
+                function getPi() as float
+                    return m.pi
+                end function
             end class
 
             sub takesFloat(fl as float)
@@ -752,6 +755,7 @@ describe('Scope', () => {
             sub someFunc()
                 holder = new PiHolder()
                 takesFloat(holder.pi)
+                takesFloat(holder.getPI())
             end sub`);
             program.validate();
             //should have no error
@@ -763,6 +767,9 @@ describe('Scope', () => {
             class PiHolder
                 pi = 3.14
                 name = "hello"
+                function getPi() as float
+                    return m.pi
+                end function
             end class
 
             sub takesFloat(fl as float)
@@ -771,15 +778,64 @@ describe('Scope', () => {
             sub someFunc()
                 holder = new PiHolder()
                 takesFloat(holder.name)
+                takesFloat(Str(holder.getPI()))
             end sub`);
             program.validate();
             //should have error: holder.name is string
-            expect(program.getDiagnostics().length).to.equal(1);
+            expect(program.getDiagnostics().length).to.equal(2);
             expect(program.getDiagnostics().map(x => x.message)).to.include(
                 DiagnosticMessages.argumentTypeMismatch('string', 'float').message
             );
         });
 
+        it('correctly validates correct parameters that are interface members', () => {
+            program.setFile('source/main.bs', `
+            interface IPerson
+                height as float
+                name as string
+                function getWeight() as float
+                function getAddress() as string
+            end interface
+
+            sub takesFloat(fl as float)
+            end sub
+
+            sub someFunc(person as IPerson)
+                takesFloat(person.height)
+                takesFloat(person.getWeight())
+            end sub`);
+            program.validate();
+            //should have no error
+            expect(program.getDiagnostics().length).to.equal(0);
+        });
+
+        it('correctly validates wrong parameters that are interface members', () => {
+            program.setFile('source/main.bs', `
+            interface IPerson
+                height as float
+                name as string
+                function getWeight() as float
+                function getAddress() as object
+            end interface
+
+            sub takesFloat(fl as float)
+            end sub
+
+            sub someFunc(person as IPerson)
+                takesFloat(person.name)
+                takesFloat(person.getAddress())
+            end sub`);
+            program.validate();
+            //should have 2 errors: person.name is string (not float) and person.getAddress() is object (not float)
+            expect(program.getDiagnostics().length).to.equal(2);
+            expect(program.getDiagnostics()[0].message).to.equal(
+                DiagnosticMessages.argumentTypeMismatch('string', 'float').message
+            );
+            expect(program.getDiagnostics()[1].message).to.equal(
+                DiagnosticMessages.argumentTypeMismatch('object', 'float').message
+            );
+        });
+
         it('allows conversions for arguments', () => {
             program.setFile('source/main.bs', `
             sub takesFloat(fl as float)
@@ -1185,6 +1241,191 @@ describe('Scope', () => {
                 expectZeroDiagnostics(program);
             });
         });
+
+        describe('interface types', () => {
+            it('detects an unknown function return type', () => {
+                program.setFile('source/main.bs', `
+                    interface myIFace
+                        function myMethod() as unknownTypeA 'error
+                    end interface
+
+                    function d(iface as myIFace) as unknownTypeB
+                        return iface.myMethod
+                    end function
+                `);
+                program.validate();
+                expectDiagnostics(program, [
+                    {
+                        ...DiagnosticMessages.invalidFunctionReturnType('unknownTypeA'),
+                        range: util.createRange(2, 47, 2, 59)
+                    },
+                    {
+                        ...DiagnosticMessages.invalidFunctionReturnType('unknownTypeB'),
+                        range: util.createRange(5, 52, 5, 64)
+                    }
+                ]);
+            });
+
+            it('detects an unknown function parameter type', () => {
+                program.setFile('source/main.bs', `
+                    interface myIFace
+                       function myMethod(unknownParam as unknownType)  'error
+                    end interface
+                `);
+                program.validate();
+                expectDiagnostics(program, [
+                    {
+                        ...DiagnosticMessages.functionParameterTypeIsInvalid('unknownParam', 'unknownType'),
+                        range: util.createRange(2, 57, 2, 68)
+                    }
+                ]);
+            });
+
+            it('detects an unknown field parameter type', () => {
+                program.setFile('source/main.bs', `
+                    interface myIFace
+                        foo as unknownTypeA 'error
+                    end interface
+
+                    interface myIFace2
+                        foo as unknownTypeB 'error
+                        bar as myIFace
+                        buz as myIFace2
+                    end interface
+                `);
+                program.validate();
+                expectDiagnostics(program, [
+                    DiagnosticMessages.cannotFindType('unknownTypeA').message,
+                    DiagnosticMessages.cannotFindType('unknownTypeB').message
+                ]);
+            });
+
+            it('finds interfaces inside namespaces', () => {
+                program.setFile('source/main.bs', `
+                    namespace MyNamespace
+                        interface MyIFace
+                            name as string
+                        end interface
+
+                        function foo(param as MyIFace) as MyIFace
+                        end function
+
+                        function bar(param as MyNamespace.MyIFace) as MyNamespace.MyIFace
+                        end function
+
+                    end namespace
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                let scope = program.getScopeByName('source');
+                let ifaceFromScope = scope.getInterface('MyIFace', 'MyNameSpace');
+                expect(isInterfaceStatement(ifaceFromScope)).to.be.true;
+                expect(isInterfaceType(ifaceFromScope.getThisBscType())).to.be.true;
+            });
+
+            it('finds custom types from other namespaces', () => {
+                program.setFile('source/main.bs', `
+                    namespace MyNamespace
+                        interface MyIFace
+                        end interface
+                    end namespace
+
+                    function foo(param as MyNamespace.MyIFace) as MyNamespace.MyIFace
+                    end function
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+            });
+
+            it('finds interfaces types from other other files', () => {
+                program.setFile('source/main.bs', `
+                    function foo(param as MyIFace) as MyIFace
+                        return param
+                    end function
+                `);
+                program.setFile('source/MyClass.bs', `
+                    interface MyIFace
+                        name as string
+                    end interface
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+            });
+
+            it('finds interfaces types from other other files in namespaces', () => {
+                program.setFile('source/main.bs', `
+                    function foo(param as MyNameSpace.MyIFace) as MyNameSpace.MyIFace
+                        return param
+                    end function
+                `);
+                program.setFile('source/MyNameSpace.bs', `
+                    namespace MyNameSpace
+                        interface MyIFace
+                            name as string
+                        end interface
+                    end namespace
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+            });
+
+            it('scopes types to correct scope', () => {
+                program.setFile('components/foo.xml', trim`
+                    <?xml version="1.0" encoding="utf-8" ?>
+                    <component name="foo" extends="Scene">
+                        <script uri="foo.bs"/>
+                    </component>
+                `);
+                program.setFile(s`components/foo.bs`, `
+                    interface MyIFace
+                        name as string
+                    end interface
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                program.setFile('components/bar.xml', trim`
+                    <?xml version="1.0" encoding="utf-8" ?>
+                    <component name="bar" extends="Scene">
+                        <script uri="bar.bs"/>
+                    </component>
+                `);
+                program.setFile(s`components/bar.bs`, `
+                    function getFoo() as MyIFace
+                    end function
+                `);
+                program.validate();
+                expectDiagnostics(program, [
+                    DiagnosticMessages.invalidFunctionReturnType('MyIFace').message
+                ]);
+            });
+
+            it('can reference types from parent component', () => {
+                program.setFile('components/parent.xml', trim`
+                    <?xml version="1.0" encoding="utf-8" ?>
+                    <component name="parent" extends="Scene">
+                        <script uri="parent.bs"/>
+                    </component>
+                `);
+                program.setFile(s`components/parent.bs`, `
+                    interface MyIFace
+                        name as string
+                    end interface
+                `);
+                program.setFile('components/child.xml', trim`
+                    <?xml version="1.0" encoding="utf-8" ?>
+                    <component name="child" extends="parent">
+                        <script uri="child.bs"/>
+                    </component>
+                `);
+                program.setFile(s`components/child.bs`, `
+                    function getFoo() as MyIFace
+                    end function
+                `);
+
+                program.validate();
+                expectZeroDiagnostics(program);
+            });
+        });
     });
 
     describe('inheritance', () => {
diff --git a/src/Scope.ts b/src/Scope.ts
index 56785750..22d828d0 100644
--- a/src/Scope.ts
+++ b/src/Scope.ts
@@ -3,10 +3,10 @@ import { CompletionItemKind, Location } from 'vscode-languageserver';
 import chalk from 'chalk';
 import type { DiagnosticInfo } from './DiagnosticMessages';
 import { DiagnosticMessages } from './DiagnosticMessages';
-import type { CallableContainer, BsDiagnostic, FileReference, BscFile, CallableContainerMap, FileLink, FunctionCall } from './interfaces';
+import type { CallableContainer, BsDiagnostic, FileReference, BscFile, CallableContainerMap, FileLink, FunctionCall, InheritableStatement, InheritableType } from './interfaces';
 import type { Program } from './Program';
 import { BsClassValidator } from './validators/ClassValidator';
-import type { NamespaceStatement, Statement, FunctionStatement, ClassStatement, EnumStatement } from './parser/Statement';
+import type { NamespaceStatement, Statement, FunctionStatement, ClassStatement, InterfaceStatement, EnumStatement } from './parser/Statement';
 import type { FunctionExpression, NewExpression } from './parser/Expression';
 import { ParseMode } from './parser/Parser';
 import { standardizePath as s, util } from './util';
@@ -17,11 +17,10 @@ import { URI } from 'vscode-uri';
 import { LogLevel } from './Logger';
 import type { BrsFile, TokenSymbolLookup } from './files/BrsFile';
 import type { DependencyGraph, DependencyChangedEvent } from './DependencyGraph';
-import { isBrsFile, isClassMethodStatement, isClassStatement, isCustomType, isDynamicType, isEnumStatement, isFunctionStatement, isTypedFunctionType, isInvalidType, isFunctionType, isVariableExpression, isXmlFile, isArrayType } from './astUtils/reflection';
+import { isBrsFile, isClassMethodStatement, isClassStatement, isCustomType, isDynamicType, isEnumStatement, isFunctionStatement, isTypedFunctionType, isInvalidType, isFunctionType, isVariableExpression, isXmlFile, isArrayType, isInterfaceType, isInterfaceStatement } from './astUtils/reflection';
 import { SymbolTable } from './SymbolTable';
 import type { BscType, TypeContext } from './types/BscType';
 import { getTypeFromContext } from './types/BscType';
-import type { CustomType } from './types/CustomType';
 import { DynamicType } from './types/DynamicType';
 import { ObjectType } from './types/ObjectType';
 import { UninitializedType } from './types/UninitializedType';
@@ -72,6 +71,24 @@ export class Scope {
         return this.getClassFileLink(className, containingNamespace)?.item;
     }
 
+    /**
+     * Get the interface with the specified name.
+     * @param ifaceName - The interface name, including the namespace of the interface if possible
+     * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface)
+     */
+    public getInterface(ifaceName: string, containingNamespace?: string): InterfaceStatement {
+        return this.getInterfaceFileLink(ifaceName, containingNamespace)?.item;
+    }
+
+    /**
+     * Get either the class or interface, etc. with a given name
+     * @param name - The name, including the namespace of the interface if possible
+     * @param containingNamespace - The namespace used to resolve relative names. (i.e. the namespace around the current statement trying to find the interface or class)
+     */
+    public getNamedTypeStatement(name: string, containingNamespace?: string): InheritableStatement {
+        return this.getFileLink(name, containingNamespace)?.item;
+    }
+
     /**
      * A cache of a map of tokens -> TokenSymbolLookups, which are the result of getSymbolTypeFromToken()
      * Sometimes the lookup of symbols may take a while if there are lazyTypes or multiple tokens in a chain
@@ -101,11 +118,39 @@ export class Scope {
     }
 
     /**
-   * Gets the parent class of the given class
-   * @param klass - The class to get the parent of, if possible
+   * Get an interface and its containing file by the interface name
+   * @param ifaceName - The interface name, including the namespace of the interface if possible
+   * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface)
    */
+    public getInterfaceFileLink(ifaceName: string, containingNamespace?: string): FileLink<InterfaceStatement> {
+        const lowerName = ifaceName?.toLowerCase();
+        const ifaceMap = this.getInterfaceMap();
+
+        let iface = ifaceMap.get(
+            util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase())
+        );
+        //if we couldn't find the iface by its full namespaced name, look for a global class with that name
+        if (!iface) {
+            iface = ifaceMap.get(lowerName);
+        }
+        return iface;
+    }
+
+    /**
+   * Get a InheritableStatement and its containing file by the name of the interface or class
+   * @param name - The name of the interface or class, including the namespace of the class if possible
+   * @param containingNamespace - The namespace used to resolve relative names. (i.e. the namespace around the current statement trying to find a class)
+   */
+    public getFileLink(name: string, containingNamespace?: string): FileLink<InheritableStatement> {
+        return this.getClassFileLink(name, containingNamespace) || this.getInterfaceFileLink(name, containingNamespace);
+    }
+
+    /**
+     * Gets the parent class of the given class
+     * @param klass - The class to get the parent of, if possible
+     */
     public getParentClass(klass: ClassStatement): ClassStatement {
-        if (klass?.hasParentClass()) {
+        if (klass?.hasParent()) {
             const lowerParentClassNames = klass.getPossibleFullParentNames().map(name => name.toLowerCase());
             for (const lowerParentClassName of lowerParentClassNames) {
                 const foundParent = this.getClassMap().get(lowerParentClassName);
@@ -116,6 +161,34 @@ export class Scope {
         }
     }
 
+    /**
+     * Gets the parent interface of the given interface
+     * @param iface - The interface to get the parent of, if possible
+     */
+    public getParentInterface(iface: InterfaceStatement): InterfaceStatement {
+        if (iface?.hasParent()) {
+            const lowerParentClassNames = iface.getPossibleFullParentNames().map(name => name.toLowerCase());
+            for (const lowerParentClassName of lowerParentClassNames) {
+                const foundParent = this.getInterfaceMap().get(lowerParentClassName);
+                if (foundParent) {
+                    return foundParent.item;
+                }
+            }
+        }
+    }
+
+    /**
+    * Gets the parent of an Interface or Class
+    * @param stmt - The class or interface to get the parent of, if possible
+    */
+    public getParentStatement(stmt: InheritableStatement): InheritableStatement {
+        if (isInterfaceStatement(stmt)) {
+            return this.getParentInterface(stmt);
+        } else if (isClassStatement(stmt)) {
+            return this.getParentClass(stmt);
+        }
+    }
+
     /**
     * Tests if a class exists with the specified name
     * @param className - the all-lower-case namespace-included class name
@@ -125,6 +198,24 @@ export class Scope {
         return !!this.getClass(className, namespaceName);
     }
 
+    /**
+    * Tests if an interface exists with the specified name
+    * @param ifaceName - the all-lower-case namespace-included class name
+    * @param namespaceName - the current namespace name
+    */
+    public hasInterface(ifaceName: string, namespaceName?: string): boolean {
+        return !!this.hasInterface(ifaceName, namespaceName);
+    }
+
+    /**
+    * Tests if a class OR an interface, etc. exists with the specified name
+    * @param name - the all-lower-case namespace-included class or interface name
+    * @param namespaceName - the current namespace name
+    */
+    public hasNamedType(name: string, namespaceName?: string): boolean {
+        return !!this.getNamedTypeStatement(name, namespaceName);
+    }
+
     /**
      * A dictionary of all classes in this scope. This includes namespaced classes always with their full name.
      * The key is stored in lower case
@@ -147,24 +238,46 @@ export class Scope {
         });
     }
 
-    public getAncestorTypeListByContext(thisType: BscType, context?: TypeContext): CustomType[] {
+
+    public getAncestorTypeListByContext(thisType: BscType, context?: TypeContext): InheritableType[] {
         const funcExpr = context?.file?.getFunctionExpressionAtPosition(context?.position);
-        if (isCustomType(thisType)) {
+        if (isCustomType(thisType) || isInterfaceType(thisType)) {
             return this.getAncestorTypeList(thisType.name, funcExpr);
         }
         return [];
     }
+    /**
+   * A dictionary of all Interfaces in this scope. This includes namespaced Interfaces always with their full name.
+   * The key is stored in lower case
+   */
+    public getInterfaceMap(): Map<string, FileLink<InterfaceStatement>> {
+        return this.cache.getOrAdd('interfaceMap', () => {
+            const map = new Map<string, FileLink<InterfaceStatement>>();
+            this.enumerateBrsFiles((file) => {
+                if (isBrsFile(file)) {
+                    for (let cls of file.parser.references.interfaceStatements) {
+                        const lowerClassName = cls.getName(ParseMode.BrighterScript)?.toLowerCase();
+                        //only track classes with a defined name (i.e. exclude nameless malformed classes)
+                        if (lowerClassName) {
+                            map.set(lowerClassName, { item: cls, file: file });
+                        }
+                    }
+                }
+            });
+            return map;
+        });
+    }
 
-    public getAncestorTypeList(className: string, functionExpression?: FunctionExpression): CustomType[] {
+    public getAncestorTypeList(className: string, functionExpression?: FunctionExpression): InheritableType[] {
         const lowerNamespaceName = functionExpression.namespaceName?.getName().toLowerCase();
-        const ancestors: CustomType[] = [];
-        let currentClass = this.getClassFileLink(className, lowerNamespaceName)?.item;
-        if (currentClass) {
-            ancestors.push(currentClass?.getCustomType());
+        const ancestors: InheritableType[] = [];
+        let currentClassOrIFace = this.getFileLink(className, lowerNamespaceName)?.item;
+        if (currentClassOrIFace) {
+            ancestors.push(currentClassOrIFace?.getThisBscType());
         }
-        while (currentClass?.hasParentClass()) {
-            currentClass = this.getParentClass(currentClass);
-            ancestors.push(currentClass?.getCustomType());
+        while (currentClassOrIFace?.hasParent()) {
+            currentClassOrIFace = this.getParentStatement(currentClassOrIFace);
+            ancestors.push(currentClassOrIFace?.getThisBscType());
         }
         // TODO TYPES: this should probably be cached
         return ancestors;
@@ -642,6 +755,13 @@ export class Scope {
             const classStmt = pair[1]?.item;
             classStmt?.buildSymbolTable(this.getParentClass(classStmt));
         }
+
+        // also link interfaces
+        const ifaceMap = this.getInterfaceMap();
+        for (const pair of ifaceMap) {
+            const ifaceStmt = pair[1]?.item;
+            ifaceStmt?.buildSymbolTable(this.getParentInterface(ifaceStmt));
+        }
     }
 
     public unlinkSymbolTable() {
@@ -1175,7 +1295,7 @@ export class Scope {
     /**
      * @param className - The name of the class (including namespace if possible)
      * @param callsiteNamespace - the name of the namespace where the call site resides (this is NOT the known namespace of the class).
-     *                            This is used to help resolve non-namespaced class names that reside in the same namespac as the call site.
+     *                            This is used to help resolve non-namespaced class names that reside in the same namespace as the call site.
      */
     public getClassHierarchy(className: string, callsiteNamespace?: string) {
         let items = [] as FileLink<ClassStatement>[];
diff --git a/src/astUtils/visitors.ts b/src/astUtils/visitors.ts
index 7328856c..cd6e711f 100644
--- a/src/astUtils/visitors.ts
+++ b/src/astUtils/visitors.ts
@@ -1,6 +1,6 @@
 /* eslint-disable no-bitwise */
 import type { CancellationToken } from 'vscode-languageserver';
-import type { Statement, Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, ClassMethodStatement, ClassFieldStatement, EnumStatement, EnumMemberStatement } from '../parser/Statement';
+import type { Statement, Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, ClassMethodStatement, ClassFieldStatement, EnumStatement, EnumMemberStatement, InterfaceStatement, InterfaceMethodStatement, InterfaceFieldStatement } from '../parser/Statement';
 import type { AALiteralExpression, ArrayLiteralExpression, BinaryExpression, CallExpression, CallfuncExpression, DottedGetExpression, EscapedCharCodeLiteralExpression, Expression, FunctionExpression, GroupingExpression, IndexedGetExpression, LiteralExpression, NamespacedVariableNameExpression, NewExpression, SourceLiteralExpression, TaggedTemplateStringExpression, TemplateStringExpression, TemplateStringQuasiExpression, UnaryExpression, VariableExpression, XmlAttributeGetExpression } from '../parser/Expression';
 import { isExpression, isStatement } from './reflection';
 
@@ -99,6 +99,9 @@ export function createVisitor(
         ClassFieldStatement?: (statement: ClassFieldStatement, parent?: Statement) => Statement | void;
         EnumStatement?: (statement: EnumStatement, parent?: Statement) => Statement | void;
         EnumMemberStatement?: (statement: EnumMemberStatement, parent?: Statement) => Statement | void;
+        InterfaceStatement?: (statement: InterfaceStatement, parent?: Statement) => Statement | void;
+        InterfaceMethodStatement?: (statement: InterfaceMethodStatement, parent?: Statement) => Statement | void;
+        InterfaceFieldStatement?: (statement: InterfaceFieldStatement, parent?: Statement) => Statement | void;
         //expressions
         BinaryExpression?: (expression: BinaryExpression, parent?: Statement | Expression) => Expression | void;
         CallExpression?: (expression: CallExpression, parent?: Statement | Expression) => Expression | void;
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index e5f94288..d90701ed 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -3248,7 +3248,7 @@ describe('BrsFile', () => {
             expect(symbol.expandedTokenText).to.equal('klass');
             const classStmt = file.parser.references.classStatements[0];
             expect(classStmt.name.text).to.equal('MyKlass');
-            expect(symbol.type.isAssignableTo(classStmt.getCustomType())).be.true;
+            expect(symbol.type.isAssignableTo(classStmt.getThisBscType())).be.true;
             expectZeroDiagnostics(program);
         });
 
diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts
index 83890a4e..a28b7fc4 100644
--- a/src/files/BrsFile.ts
+++ b/src/files/BrsFile.ts
@@ -22,7 +22,7 @@ import { BrsTranspileState } from '../parser/BrsTranspileState';
 import { Preprocessor } from '../preprocessor/Preprocessor';
 import { LogLevel } from '../Logger';
 import { serializeError } from 'serialize-error';
-import { isClassMethodStatement, isClassStatement, isCommentStatement, isDottedGetExpression, isFunctionStatement, isTypedFunctionType, isLibraryStatement, isNamespaceStatement, isStringType, isVariableExpression, isXmlFile, isImportStatement, isClassFieldStatement, isEnumStatement, isArrayType, isCustomType, isDynamicType, isObjectType, isPrimitiveType, isRegexLiteralExpression } from '../astUtils/reflection';
+import { isClassMethodStatement, isClassStatement, isCommentStatement, isDottedGetExpression, isFunctionStatement, isTypedFunctionType, isLibraryStatement, isNamespaceStatement, isStringType, isVariableExpression, isXmlFile, isImportStatement, isClassFieldStatement, isEnumStatement, isArrayType, isCustomType, isDynamicType, isObjectType, isPrimitiveType, isRegexLiteralExpression, isInterfaceType } from '../astUtils/reflection';
 import { createVisitor, WalkMode } from '../astUtils/visitors';
 import type { DependencyGraph } from '../DependencyGraph';
 import { CommentFlagProcessor } from '../CommentFlagProcessor';
@@ -604,18 +604,20 @@ export class BrsFile {
             //we aren't in any function scope, so return the keyword completions and namespaces
             if (this.parser.getTokenBefore(currentToken, TokenKind.New)) {
                 // there's a new keyword, so only class types are viable here
-                return [...this.getGlobalClassStatementCompletions(currentToken, this.parseMode)];
+                return [...this.getGlobalClassStatementCompletions(currentToken, this.parseMode, scope)];
             } else {
                 return [
                     ...KeywordCompletions,
-                    ...this.getGlobalClassStatementCompletions(currentToken, this.parseMode),
+                    ...this.getGlobalClassStatementCompletions(currentToken, this.parseMode, scope),
                     ...namespaceCompletions,
-                    ...this.getNonNamespacedEnumStatementCompletions(currentToken, this.parseMode, scope)
+                    ...this.getNonNamespacedEnumStatementCompletions(currentToken, this.parseMode, scope),
+                    ...this.getGlobalInterfaceStatementCompletions(currentToken, this.parseMode, scope)
                 ];
             }
         }
 
-        const classNameCompletions = this.getGlobalClassStatementCompletions(currentToken, this.parseMode);
+        const classNameCompletions = this.getGlobalClassStatementCompletions(currentToken, this.parseMode, scope);
+        const interfaceNameCompletions = this.getGlobalInterfaceStatementCompletions(currentToken, this.parseMode, scope);
         const newToken = this.parser.getTokenBefore(currentToken, TokenKind.New);
         if (newToken) {
             //we are after a new keyword; so we can only be top-level namespaces or classes at this point
@@ -655,6 +657,9 @@ export class BrsFile {
             //include class names
             result.push(...classNameCompletions);
 
+            //include interfaces
+            result.push(...interfaceNameCompletions);
+
             //include enums
             result.push(...this.getNonNamespacedEnumStatementCompletions(currentToken, this.parseMode, scope));
 
@@ -737,7 +742,7 @@ export class BrsFile {
                     if (!results.has(member.name.text.toLowerCase())) {
                         results.set(member.name.text.toLowerCase(), {
                             label: member.name.text,
-                            kind: isClassFieldStatement(member) ? CompletionItemKind.Field : CompletionItemKind.Function
+                            kind: isClassFieldStatement(member) ? CompletionItemKind.Field : CompletionItemKind.Method
                         });
                     }
                 }
@@ -815,12 +820,12 @@ export class BrsFile {
             let expandedText = '';
             let useExpandedTextOnly = false;
             if (containingClass.name === currentToken) {
-                symbolType = containingClass.getCustomType();
+                symbolType = containingClass.getThisBscType();
                 expandedText = `class ${containingClass.getName(ParseMode.BrighterScript)}`;
                 useExpandedTextOnly = true;
                 currentClassRef = containingClass;
             } else if (currentTokenLower === 'm') {
-                symbolType = containingClass.getCustomType();
+                symbolType = containingClass.getThisBscType();
                 expandedText = currentToken.text;
                 currentClassRef = containingClass;
             } else if (currentTokenLower === 'super') {
@@ -942,8 +947,8 @@ export class BrsFile {
             }
 
             if (symbolType?.memberTable) {
-                if (isCustomType(symbolType)) {
-                    // we're currently looking at a customType, that has it's own symbol table
+                if (isCustomType(symbolType) || isInterfaceType(symbolType)) {
+                    // we're currently looking at a customType or interface, that has it's own symbol table
                     // use the name of the custom type
                     // TODO TYPES: get proper parent name for methods/fields defined in super classes
                     tokenText.push(tokenChain.length === 1 ? token.text : symbolType.name);
@@ -1012,7 +1017,7 @@ export class BrsFile {
         return symbolData;
     }
 
-    private getGlobalClassStatementCompletions(currentToken: Token, parseMode: ParseMode): CompletionItem[] {
+    private getGlobalClassStatementCompletions(currentToken: Token, parseMode: ParseMode, scope: Scope): CompletionItem[] {
         if (parseMode === ParseMode.BrightScript) {
             return [];
         }
@@ -1021,19 +1026,18 @@ export class BrsFile {
         if (completionName?.includes('.')) {
             return [];
         }
-        let scopes = this.program.getScopesForFile(this);
-        for (let scope of scopes) {
-            let classMap = scope.getClassMap();
-            for (const key of [...classMap.keys()]) {
-                let cs = classMap.get(key).item;
-                if (!results.has(cs.name.text)) {
-                    results.set(cs.name.text, {
-                        label: cs.name.text,
-                        kind: CompletionItemKind.Class
-                    });
-                }
+        let classMap = scope.getClassMap();
+        // let viableKeys = [...classMap.keys()].filter((k) => k.startsWith(completionName));
+        for (const key of [...classMap.keys()]) {
+            let cs = classMap.get(key).item;
+            if (!results.has(cs.name.text)) {
+                results.set(cs.name.text, {
+                    label: cs.name.text,
+                    kind: CompletionItemKind.Class
+                });
             }
         }
+
         return [...results.values()];
     }
 
@@ -1087,6 +1091,29 @@ export class BrsFile {
         return [...results.values()];
     }
 
+    private getGlobalInterfaceStatementCompletions(currentToken: Token, parseMode: ParseMode, scope: Scope): CompletionItem[] {
+        if (parseMode === ParseMode.BrightScript) {
+            return [];
+        }
+        let results = new Map<string, CompletionItem>();
+        let completionName = this.getPartialVariableName(currentToken, [TokenKind.New])?.toLowerCase();
+        if (completionName?.includes('.')) {
+            return [];
+        }
+        let ifaceMap = scope.getInterfaceMap();
+        for (const key of [...ifaceMap.keys()]) {
+            let cs = ifaceMap.get(key).item;
+            if (!results.has(cs.name.text)) {
+                results.set(cs.name.text, {
+                    label: cs.name.text,
+                    kind: CompletionItemKind.Interface
+                });
+            }
+        }
+
+        return [...results.values()];
+    }
+
     private getNamespaceCompletions(currentToken: Token, parseMode: ParseMode, scope: Scope): CompletionItem[] {
         //BrightScript does not support namespaces, so return an empty list in that case
         if (parseMode === ParseMode.BrightScript) {
@@ -1429,20 +1456,22 @@ export class BrsFile {
         }
 
         const func = this.getFunctionExpressionAtPosition(position);
-        //look through local variables first
-        //find any variable with this name
-        for (const symbol of func.symbolTable.getOwnSymbols()) {
-            //we found a variable declaration with this token text
-            if (symbol.name.toLowerCase() === textToSearchFor) {
-                const uri = util.pathToUri(this.srcPath);
-                results.push(Location.create(uri, symbol.range));
-            }
-        }
-        if (this.parser.tokenFollows(token, TokenKind.Goto)) {
-            for (const label of func.labelStatements) {
-                if (label.tokens.identifier.text.toLocaleLowerCase() === textToSearchFor) {
+        if (func) {
+            //look through local variables first
+            //find any variable with this name
+            for (const symbol of func.symbolTable.getOwnSymbols()) {
+                //we found a variable declaration with this token text
+                if (symbol.name.toLowerCase() === textToSearchFor) {
                     const uri = util.pathToUri(this.srcPath);
-                    results.push(Location.create(uri, label.tokens.identifier.range));
+                    results.push(Location.create(uri, symbol.range));
+                }
+            }
+            if (this.parser.tokenFollows(token, TokenKind.Goto)) {
+                for (const label of func.labelStatements) {
+                    if (label.tokens.identifier.text.toLocaleLowerCase() === textToSearchFor) {
+                        const uri = util.pathToUri(this.srcPath);
+                        results.push(Location.create(uri, label.tokens.identifier.range));
+                    }
                 }
             }
         }
@@ -1710,7 +1739,7 @@ export class BrsFile {
             key += param.name.text;
         }
 
-        const label = util.getTextForRange(lines, util.createRangeFromPositions(func.functionType.range.start, func.body.range.start)).trim();
+        const label = util.getTextForRange(lines, func.functionDeclarationRange).trim();
         const signature = SignatureInformation.create(label, documentation, ...params);
         const index = 1;
         return { key: key, signature: signature, index: index };
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 062dbf33..94ebde1f 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -6,13 +6,15 @@ import type { TypedFunctionType } from './types/TypedFunctionType';
 import type { ParseMode } from './parser/Parser';
 import type { Program } from './Program';
 import type { ProgramBuilder } from './ProgramBuilder';
-import type { FunctionStatement } from './parser/Statement';
+import type { FunctionStatement, ClassStatement, InterfaceStatement } from './parser/Statement';
 import type { Expression, FunctionExpression } from './parser/Expression';
 import type { TranspileState } from './parser/TranspileState';
 import type { SourceMapGenerator, SourceNode } from 'source-map';
-import type { BscType } from './types/BscType';
+import type { BscType, SymbolContainer } from './types/BscType';
 import type { Token } from './lexer/Token';
 import type { AstEditor } from './astUtils/AstEditor';
+import type { CustomType } from './types/CustomType';
+import type { InterfaceType } from './types/InterfaceType';
 
 export interface BsDiagnostic extends Diagnostic {
     file: BscFile;
@@ -425,3 +427,44 @@ export interface FileLink<T> {
     item: T;
     file: BrsFile;
 }
+
+/**
+ * Common interface to support Statements which define entities that have a member table
+ * e.g. Class, Interface
+ */
+export interface MemberSymbolTableProvider extends SymbolContainer {
+    buildSymbolTable(parent?: InheritableStatement): void;
+    hasParent(): boolean;
+    getPossibleFullParentNames(): string[];
+    getName(parseMode: ParseMode): string;
+    getThisBscType(): BscType;
+}
+
+export type InheritableStatement = ClassStatement | InterfaceStatement;
+
+export type InheritableType = CustomType | InterfaceType;
+
+/**
+ * Options for the parser functionDeclaration() method
+ */
+export interface FunctionDeclarationParseOptions {
+    /**
+     * Function should have a name. Add a diagnostic if it is not there
+     * False for for anonymous functions
+     */
+    hasName?: boolean;
+    /**
+     * Function should have a body. Add a diagnostic if it is not there
+     * False for for functions defined in Interfaces
+     */
+    hasBody?: boolean;
+    /**
+    * Function should have an end token. Add a diagnostic if it is not there
+    * False for for functions defined in Interfaces
+    */
+    hasEnd?: boolean;
+    /**
+     *This function is only callable as a member for a class or interface, etc.
+     */
+    onlyCallableAsMember?: boolean;
+}
diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts
index 857b440c..7720c7f6 100644
--- a/src/parser/Expression.ts
+++ b/src/parser/Expression.ts
@@ -207,6 +207,21 @@ export class FunctionExpression extends Expression implements TypedefProvider {
      */
     public childFunctionExpressions = [] as FunctionExpression[];
 
+    /**
+    * The range of the function declaration, starting at the 'f' in function or 's' in sub (or the open paren if the keyword is missing),
+    * and ending with the last character in the returnTypeToken, or the 's' in 'as', or the rightParen
+    */
+    public get functionDeclarationRange() {
+        return util.createBoundingRange(
+            this.functionType,
+            this.leftParen,
+            ...(this.parameters ?? []),
+            this.rightParen,
+            this.asToken,
+            this.returnType
+        );
+    }
+
     transpile(state: BrsTranspileState, name?: Identifier, includeBody = true) {
         let results = [];
         //'function'|'sub'
@@ -255,12 +270,14 @@ export class FunctionExpression extends Expression implements TypedefProvider {
             state.lineage.shift();
             results.push(...body);
         }
-        results.push('\n');
-        //'end sub'|'end function'
-        results.push(
-            state.indent(),
-            state.transpileToken(this.end)
-        );
+        if (this.end) {
+            results.push('\n');
+            //'end sub'|'end function'
+            results.push(
+                state.indent(),
+                state.transpileToken(this.end)
+            );
+        }
         return results;
     }
 
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index a7c17d99..98c4f98b 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -89,7 +89,7 @@ import {
 } from './Expression';
 import type { Diagnostic, Position, Range } from 'vscode-languageserver';
 import { Logger } from '../Logger';
-import { isAALiteralExpression, isAAMemberExpression, isAnnotationExpression, isArrayLiteralExpression, isArrayType, isCallExpression, isCallfuncExpression, isClassMethodStatement, isCommentStatement, isDottedGetExpression, isFunctionExpression, isIfStatement, isIndexedGetExpression, isInvalidType, isLiteralExpression, isNewExpression, isVariableExpression } from '../astUtils/reflection';
+import { isAALiteralExpression, isAAMemberExpression, isAnnotationExpression, isArrayLiteralExpression, isArrayType, isCallExpression, isCallfuncExpression, isClassMethodStatement, isCommentStatement, isDottedGetExpression, isFunctionExpression, isIfStatement, isIndexedGetExpression, isInvalidType, isLiteralExpression, isNewExpression, isVariableExpression, isInterfaceMethodStatement } from '../astUtils/reflection';
 import { createVisitor, WalkMode } from '../astUtils/visitors';
 import { createStringLiteral, createToken } from '../astUtils/creators';
 import { Cache } from '../Cache';
@@ -99,6 +99,8 @@ import { getTypeFromCallExpression, getTypeFromDottedGetExpression, getTypeFromN
 import { SymbolTable } from '../SymbolTable';
 import { ObjectType } from '../types/ObjectType';
 import type { BscType } from '../types/BscType';
+import type { FunctionDeclarationParseOptions } from '../interfaces';
+
 
 export class Parser {
     /**
@@ -332,7 +334,7 @@ export class Parser {
     private declaration(): Statement | AnnotationExpression | undefined {
         try {
             if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
-                return this.functionDeclaration(false);
+                return this.functionStatement({ hasName: true, hasBody: true, hasEnd: true });
             }
 
             if (this.checkLibrary()) {
@@ -406,57 +408,27 @@ export class Parser {
     }
 
     /**
-     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
+     * Create a new InterfaceFieldStatement. This should only be called from within `interfaceDeclaration`
      */
     private interfaceFieldStatement() {
         const name = this.identifier(...AllowedProperties);
-        let asToken = this.consumeToken(TokenKind.As);
-        let typeExpr = this.typeExpression();
-
-        if (!typeExpr.isValidType()) {
-            this.diagnostics.push({
-                ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeExpr.getText()),
-                range: typeExpr.range
-            });
-            throw this.lastDiagnosticAsError();
-        }
-
-        return new InterfaceFieldStatement(name, asToken, typeExpr);
-    }
-
-    /**
-     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration()`
-     */
-    private interfaceMethodStatement() {
-        const functionType = this.advance();
-        const name = this.identifier(...AllowedProperties);
-        const leftParen = this.consumeToken(TokenKind.LeftParen);
-
-        const params = [];
-        const rightParen = this.consumeToken(TokenKind.RightParen);
-        let asToken = null as Token;
-        let returnTypeExpr: TypeExpression;
+        let asToken: Token;
+        let typeExpr: TypeExpression;
+        //look for `as SOME_TYPE`
         if (this.check(TokenKind.As)) {
-            asToken = this.advance();
-            returnTypeExpr = this.typeExpression();
-            if (!returnTypeExpr.isValidType(this.options.mode)) {
+            asToken = this.consumeToken(TokenKind.As);
+            typeExpr = this.typeExpression();
+
+            //no field type specified
+            if (!typeExpr.isValidType()) {
                 this.diagnostics.push({
-                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, returnTypeExpr.getText()),
-                    range: returnTypeExpr.range
+                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeExpr.getText()),
+                    range: typeExpr.range
                 });
-                throw this.lastDiagnosticAsError();
             }
         }
 
-        return new InterfaceMethodStatement(
-            functionType,
-            name,
-            leftParen,
-            params,
-            rightParen,
-            asToken,
-            returnTypeExpr
-        );
+        return new InterfaceFieldStatement(name, asToken, typeExpr, this.currentNamespaceName);
     }
 
     private interfaceDeclaration(): InterfaceStatement {
@@ -468,7 +440,8 @@ export class Parser {
             DiagnosticMessages.expectedKeyword(TokenKind.Interface),
             TokenKind.Interface
         );
-        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
+        //get the interface name
+        let nameToken = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('interface'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
 
         let extendsToken: Token;
         let parentInterfaceName: NamespacedVariableNameExpression;
@@ -482,6 +455,11 @@ export class Parser {
         let body = [] as Statement[];
         while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
             try {
+                //break out of this loop if we encountered the `EndInterface` token not followed by `as`
+                if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
+                    break;
+                }
+
                 let decl: Statement;
 
                 //collect leading annotations
@@ -495,7 +473,19 @@ export class Parser {
 
                     //methods (function/sub keyword followed by opening paren)
                 } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
-                    decl = this.interfaceMethodStatement();
+                    const functionStatement = this.functionStatement({
+                        hasName: true,
+                        hasBody: false,
+                        hasEnd: false,
+                        onlyCallableAsMember: true
+                    });
+                    decl = new InterfaceMethodStatement(
+                        functionStatement.name,
+                        functionStatement.func
+                    );
+
+                    //refer to this statement as parent of the expression
+                    functionStatement.func.functionStatement = decl as InterfaceMethodStatement;
 
                     //comments
                 } else if (this.check(TokenKind.Comment)) {
@@ -505,9 +495,6 @@ export class Parser {
                 if (decl) {
                     this.consumePendingAnnotations(decl);
                     body.push(decl);
-                } else {
-                    //we didn't find a declaration...flag tokens until next line
-                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
                 }
             } catch (e) {
                 //throw out any failed members and move on to the next line
@@ -516,14 +503,16 @@ export class Parser {
 
             //ensure statement separator
             this.consumeStatementSeparators();
-            //break out of this loop if we encountered the `EndInterface` token not followed by `as`
-            if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
-                break;
-            }
         }
 
         //consume the final `end interface` token
-        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
+        let endingKeyword = this.advance();
+        if (endingKeyword.kind !== TokenKind.EndInterface) {
+            this.diagnostics.push({
+                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('interface'),
+                range: endingKeyword.range
+            });
+        }
 
         const statement = new InterfaceStatement(
             interfaceToken,
@@ -531,7 +520,7 @@ export class Parser {
             extendsToken,
             parentInterfaceName,
             body,
-            endInterfaceToken,
+            endingKeyword,
             this.currentNamespaceName
         );
         this._references.interfaceStatements.push(statement);
@@ -654,13 +643,10 @@ export class Parser {
 
                 //methods (function/sub keyword OR identifier followed by opening paren)
                 if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
-                    const funcDeclaration = this.functionDeclaration(false, false, true);
-
-                    //remove this function from the lists because it's not a callable
-                    const functionStatement = this._references.functionStatements.pop();
+                    const functionStatement = this.functionStatement({ hasName: true, hasBody: true, hasEnd: true, onlyCallableAsMember: true });
 
                     //if we have an overrides keyword AND this method is called 'new', that's not allowed
-                    if (overrideKeyword && funcDeclaration.name.text.toLowerCase() === 'new') {
+                    if (overrideKeyword && functionStatement.name.text.toLowerCase() === 'new') {
                         this.diagnostics.push({
                             ...DiagnosticMessages.cannotUseOverrideKeywordOnConstructorFunction(),
                             range: overrideKeyword.range
@@ -669,8 +655,8 @@ export class Parser {
 
                     decl = new ClassMethodStatement(
                         accessModifier,
-                        funcDeclaration.name,
-                        funcDeclaration.func,
+                        functionStatement.name,
+                        functionStatement.func,
                         overrideKeyword
                     );
 
@@ -783,24 +769,36 @@ export class Parser {
      */
     private callExpressions = [];
 
-    private functionDeclaration(isAnonymous: true, checkIdentifier?: boolean, forClassMethod?: boolean): FunctionExpression;
-    private functionDeclaration(isAnonymous: false, checkIdentifier?: boolean, forClassMethod?: boolean): FunctionStatement;
-    private functionDeclaration(isAnonymous: boolean, checkIdentifier = true, forClassMethod = false) {
+
+    private functionStatement(options: FunctionDeclarationParseOptions): FunctionStatement {
+        options.hasName = true;
+        const funcResult = this.functionDeclaration(options);
+        if (funcResult) {
+            let result = new FunctionStatement(funcResult.name, funcResult.functionExpression, this.currentNamespaceName);
+            funcResult.functionExpression.functionStatement = result;
+            if (!options.onlyCallableAsMember) {
+                this._references.functionStatements.push(result);
+            }
+            return result;
+        }
+    }
+
+    private functionDeclaration(options: FunctionDeclarationParseOptions = {}): { name: Identifier; functionExpression: FunctionExpression } {
         let previousCallExpressions = this.callExpressions;
         this.callExpressions = [];
 
         try {
             //track depth to help certain statements need to know if they are contained within a function body
             this.namespaceAndFunctionDepth++;
-            let functionType: Token;
+            let functionKeyword: Token;
             if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
-                functionType = this.advance();
+                functionKeyword = this.advance();
             } else {
                 this.diagnostics.push({
                     ...DiagnosticMessages.missingCallableKeyword(),
                     range: this.peek().range
                 });
-                functionType = {
+                functionKeyword = {
                     isReserved: true,
                     kind: TokenKind.Function,
                     text: 'function',
@@ -812,24 +810,24 @@ export class Parser {
                     leadingWhitespace: ''
                 };
             }
-            let isSub = functionType?.kind === TokenKind.Sub;
-            let functionTypeText = isSub ? 'sub' : 'function';
+            let isSub = functionKeyword?.kind === TokenKind.Sub;
+            let functionKeywordText = isSub ? 'sub' : 'function';
             let name: Identifier;
             let leftParen: Token;
 
-            if (isAnonymous) {
+            if (!options.hasName) {
                 leftParen = this.consume(
-                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
+                    DiagnosticMessages.expectedLeftParenAfterCallable(functionKeywordText),
                     TokenKind.LeftParen
                 );
             } else {
                 name = this.consume(
-                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
+                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionKeywordText),
                     TokenKind.Identifier,
                     ...AllowedProperties
                 ) as Identifier;
                 leftParen = this.consume(
-                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
+                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionKeywordText),
                     TokenKind.LeftParen
                 );
 
@@ -838,13 +836,13 @@ export class Parser {
                 if (['$', '%', '!', '#', '&'].includes(lastChar)) {
                     //don't throw this error; let the parser continue
                     this.diagnostics.push({
-                        ...DiagnosticMessages.functionNameCannotEndWithTypeDesignator(functionTypeText, name.text, lastChar),
+                        ...DiagnosticMessages.functionNameCannotEndWithTypeDesignator(functionKeywordText, name.text, lastChar),
                         range: name.range
                     });
                 }
 
-                //flag functions with keywords for names (only for standard functions)
-                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
+                //flag functions with keywords for names (only for standard functions - not for class methods)
+                if (!options.onlyCallableAsMember && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
                     this.diagnostics.push({
                         ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
                         range: name.range
@@ -893,11 +891,14 @@ export class Parser {
                 return haveFoundOptional || !!param.defaultValue;
             }, false);
 
-            this.consumeStatementSeparators(true);
+            if (options.hasEnd && options.hasBody) {
+                // do not go to next statement - we don't care about any other statement
+                this.consumeStatementSeparators(true);
+            }
             let func = new FunctionExpression(
                 params,
                 undefined, //body
-                functionType,
+                functionKeyword,
                 undefined, //ending keyword
                 leftParen,
                 rightParen,
@@ -915,7 +916,7 @@ export class Parser {
             }
 
             // add the function to the relevant symbol tables
-            if (!isAnonymous && !forClassMethod) {
+            if (!options.onlyCallableAsMember && name) {
                 const funcType = func.getFunctionType();
                 funcType.setName(name.text);
 
@@ -931,51 +932,43 @@ export class Parser {
 
             this._references.functionExpressions.push(func);
 
-            let previousFunctionExpression = this.currentFunctionExpression;
-            this.currentFunctionExpression = func;
+            if (options.hasBody) {
+                let previousFunctionExpression = this.currentFunctionExpression;
+                this.currentFunctionExpression = func;
 
-            //make sure to restore the currentFunctionExpression even if the body block fails to parse
-            try {
-                //support ending the function with `end sub` OR `end function`
-                func.body = this.block();
-            } finally {
-                this.currentFunctionExpression = previousFunctionExpression;
-            }
+                //make sure to restore the currentFunctionExpression even if the body block fails to parse
+                try {
+                    //support ending the function with `end sub` OR `end function`
+                    func.body = this.block();
+                } finally {
+                    this.currentFunctionExpression = previousFunctionExpression;
+                }
 
-            if (!func.body) {
-                this.diagnostics.push({
-                    ...DiagnosticMessages.callableBlockMissingEndKeyword(functionTypeText),
-                    range: this.peek().range
-                });
-                throw this.lastDiagnosticAsError();
+                if (!func.body) {
+                    this.diagnostics.push({
+                        ...DiagnosticMessages.callableBlockMissingEndKeyword(functionKeywordText),
+                        range: this.peek().range
+                    });
+                    throw this.lastDiagnosticAsError();
+                }
             }
+            if (options.hasEnd) {
+                // consume 'end sub' or 'end function'
+                func.end = this.advance();
+                let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
 
-            // consume 'end sub' or 'end function'
-            func.end = this.advance();
-            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
-
-            //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
-            //add an error but don't hard-fail so the AST can continue more gracefully
-            if (func.end.kind !== expectedEndKind) {
-                this.diagnostics.push({
-                    ...DiagnosticMessages.mismatchedEndCallableKeyword(functionTypeText, func.end.text),
-                    range: this.peek().range
-                });
+                //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
+                //add an error but don't hard-fail so the AST can continue more gracefully
+                if (func.end.kind !== expectedEndKind) {
+                    this.diagnostics.push({
+                        ...DiagnosticMessages.mismatchedEndCallableKeyword(functionKeywordText, func.end.text),
+                        range: this.peek().range
+                    });
+                }
             }
             func.callExpressions = this.callExpressions;
-
-            if (isAnonymous) {
-                //cache the range property so that plugins can't affect it
-                func.cacheRange();
-                return func;
-            } else {
-                let result = new FunctionStatement(name, func, this.currentNamespaceName);
-                func.functionStatement = result;
-                this._references.functionStatements.push(result);
-                //cache the range property so that plugins can't affect it
-                result.cacheRange();
-                return result;
-            }
+            func.cacheRange();
+            return { name: name, functionExpression: func };
         } finally {
             this.namespaceAndFunctionDepth--;
             //restore the previous CallExpression list
@@ -2286,7 +2279,7 @@ export class Parser {
 
     private anonymousFunction(): Expression {
         if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
-            const func = this.functionDeclaration(true);
+            const func = this.functionDeclaration({ hasName: false, hasBody: true, hasEnd: true }).functionExpression;
             //if there's an open paren after this, this is an IIFE
             if (this.check(TokenKind.LeftParen)) {
                 return this.finishCall(this.advance(), func);
@@ -3259,6 +3252,9 @@ export class Parser {
                     this._references.expressions.add(s.initialValue);
                 }
             },
+            InterfaceStatement: s => {
+                this._references.interfaceStatements.push(s);
+            },
             NamespaceStatement: s => {
                 this._references.namespaceStatements.push(s);
             },
@@ -3272,7 +3268,7 @@ export class Parser {
                 this._references.libraryStatements.push(s);
             },
             FunctionExpression: (expression, parent) => {
-                if (!isClassMethodStatement(parent)) {
+                if (!isClassMethodStatement(parent) && !isInterfaceMethodStatement(parent)) {
                     this._references.functionExpressions.push(expression);
                 }
             },
@@ -3384,6 +3380,7 @@ export class References {
 
     public functionExpressions = [] as FunctionExpression[];
     public functionStatements = [] as FunctionStatement[];
+
     /**
      * A map of function statements, indexed by fully-namespaced lower function name.
      */
@@ -3404,7 +3401,7 @@ export class References {
         if (!this._interfaceStatementLookup) {
             this._interfaceStatementLookup = new Map();
             for (const stmt of this.interfaceStatements) {
-                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
+                this._interfaceStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
             }
         }
         return this._interfaceStatementLookup;
@@ -3539,7 +3536,7 @@ export function getBscTypeFromExpression(expression: Expression, functionExpress
             return new ArrayType(...innerTypes);
             //function call
         } else if (isNewExpression(expression)) {
-            return getTypeFromNewExpression(expression, functionExpression); // new CustomType(expression.className.getName(ParseMode.BrighterScript));
+            return getTypeFromNewExpression(expression, functionExpression);
             //Function call
         } else if (isCallExpression(expression)) {
             return getTypeFromCallExpression(expression, functionExpression);
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 180efd21..e70b61c2 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -1,7 +1,7 @@
 /* eslint-disable no-bitwise */
 import type { Token, Identifier } from '../lexer/Token';
 import { CompoundAssignmentOperators, TokenKind } from '../lexer/TokenKind';
-import type { BinaryExpression, Expression, NamespacedVariableNameExpression, FunctionExpression, AnnotationExpression, FunctionParameterExpression, LiteralExpression, TypeExpression } from './Expression';
+import type { BinaryExpression, Expression, NamespacedVariableNameExpression, FunctionExpression, AnnotationExpression, LiteralExpression, TypeExpression } from './Expression';
 import { CallExpression, VariableExpression } from './Expression';
 import { util } from '../util';
 import type { Range } from 'vscode-languageserver';
@@ -9,16 +9,15 @@ import type { BrsTranspileState } from './BrsTranspileState';
 import { ParseMode } from './Parser';
 import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
 import { InternalWalkMode, walk, createVisitor, WalkMode } from '../astUtils/visitors';
-import { isCallExpression, isClassFieldStatement, isClassMethodStatement, isCommentStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isTypedefProvider, isVoidType } from '../astUtils/reflection';
-import type { TranspileResult, TypedefProvider } from '../interfaces';
+import { isCallExpression, isClassFieldStatement, isClassMethodStatement, isClassStatement, isCommentStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isTypedefProvider, isVoidType } from '../astUtils/reflection';
+import type { TranspileResult, TypedefProvider, MemberSymbolTableProvider, InheritableStatement } from '../interfaces';
 import { createClassMethodStatement, createInvalidLiteral, createToken, interpolatedRange } from '../astUtils/creators';
 import { DynamicType } from '../types/DynamicType';
-import type { SymbolContainer } from '../types/BscType';
 import type { SourceNode } from 'source-map';
 import type { TranspileState } from './TranspileState';
 import { SymbolTable } from '../SymbolTable';
 import { CustomType } from '../types/CustomType';
-
+import { InterfaceType } from '../types/InterfaceType';
 /**
  * A BrightScript statement
  */
@@ -1242,10 +1241,13 @@ export class ImportStatement extends Statement implements TypedefProvider {
     }
 }
 
-export class InterfaceStatement extends Statement implements TypedefProvider {
+
+export class InterfaceStatement extends Statement implements TypedefProvider, MemberSymbolTableProvider {
+    readonly memberTable: SymbolTable = new SymbolTable();
+
     constructor(
         interfaceToken: Token,
-        name: Identifier,
+        public name: Identifier,
         extendsToken: Token,
         public parentInterfaceName: NamespacedVariableNameExpression,
         public body: Statement[],
@@ -1266,6 +1268,17 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
             ...this.body,
             this.tokens.endInterface
         ) ?? interpolatedRange;
+        for (let statement of this.body) {
+            if (isInterfaceMethodStatement(statement)) {
+                this.methods.push(statement);
+
+                this.memberMap[statement?.name?.text.toLowerCase()] = statement;
+            } else if (isInterfaceFieldStatement(statement)) {
+                this.fields.push(statement);
+
+                this.memberMap[statement?.tokens.name?.text.toLowerCase()] = statement;
+            }
+        }
     }
 
     public tokens = {} as {
@@ -1277,18 +1290,14 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
 
     public readonly range: Range;
 
-    public get fields() {
-        return this.body.filter(x => isInterfaceFieldStatement(x));
-    }
-
-    public get methods() {
-        return this.body.filter(x => isInterfaceMethodStatement(x));
-    }
+    public memberMap = {} as Record<string, InterfaceMemberStatement>;
+    public methods = [] as InterfaceMethodStatement[];
+    public fields = [] as InterfaceFieldStatement[];
 
     /**
      * The name of the interface WITH its leading namespace (if applicable)
      */
-    public get fullName() {
+    public getName(parseMode: ParseMode) {
         const name = this.tokens.name?.text;
         if (name) {
             if (this.namespaceName) {
@@ -1303,11 +1312,53 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
         }
     }
 
+    public buildSymbolTable(parentIface?: InheritableStatement) {
+        this.memberTable.clear();
+        if (parentIface) {
+            this.memberTable.setParent(parentIface?.memberTable);
+        }
+
+        for (const statement of this.methods) {
+            const funcType = statement?.func.getFunctionType();
+            this.memberTable.addSymbol(statement?.name?.text, statement?.range, funcType);
+        }
+        for (const statement of this.fields) {
+            this.memberTable.addSymbol(statement?.tokens.name?.text, statement?.range, statement.getType());
+        }
+    }
+
+    public hasParent() {
+        return !!this.parentInterfaceName;
+    }
+
+    public getParentName() {
+        return !!this.parentInterfaceName;
+    }
+
     /**
-     * The name of the interface (without the namespace prefix)
+     * Gets an array of possible parent interface names, taking into account the namespace this interface was created under
+     * @returns array of possible parent interface names
      */
-    public get name() {
-        return this.tokens.name?.text;
+    public getPossibleFullParentNames(): string[] {
+        if (!this.hasParent()) {
+            return [];
+        }
+        if (this.parentInterfaceName?.getNameParts().length > 1) {
+            // The specified parent interface already has a dot, so it must already reference a namespace
+            return [this.parentInterfaceName.getName()];
+        }
+        const names = [];
+
+        if (this.namespaceName) {
+            // We're under a namespace, so the full parent name MIGHT be with this namespace too
+            names.push(this.namespaceName.getName() + '.' + this.parentInterfaceName.getName());
+        }
+        names.push(this.parentInterfaceName.getName());
+        return names;
+    }
+
+    public getThisBscType(): InterfaceType {
+        return new InterfaceType(this.getName(ParseMode.BrighterScript), this.memberTable);
     }
 
     public transpile(state: BrsTranspileState): TranspileResult {
@@ -1383,7 +1434,8 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide
     constructor(
         nameToken: Identifier,
         asToken: Token,
-        public type?: TypeExpression
+        public type?: TypeExpression,
+        public namespaceName?: NamespacedVariableNameExpression
     ) {
         super();
         this.tokens.name = nameToken;
@@ -1404,7 +1456,7 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide
     }
 
     public get name() {
-        return this.tokens.name.text;
+        return this.tokens.name;
     }
 
     walk(visitor: WalkVisitor, options: WalkOptions) {
@@ -1432,104 +1484,25 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide
         }
         return result;
     }
-
 }
 
-export class InterfaceMethodStatement extends Statement implements TypedefProvider {
+export class InterfaceMethodStatement extends FunctionStatement implements TypedefProvider {
+    public transpile(state: BrsTranspileState): TranspileResult {
+        throw new Error('Method not implemented.');
+    }
     constructor(
-        functionTypeToken: Token,
-        nameToken: Identifier,
-        leftParen: Token,
-        public params: FunctionParameterExpression[],
-        rightParen: Token,
-        asToken?: Token,
-        public returnType?: TypeExpression
+        name: Identifier,
+        func: FunctionExpression
     ) {
-        super();
-        this.tokens.functionType = functionTypeToken;
-        this.tokens.name = nameToken;
-        this.tokens.leftParen = leftParen;
-        this.tokens.rightParen = rightParen;
-        this.tokens.as = asToken;
-
-        this.range = util.createBoundingRange(
-            this.tokens.name,
-            this.tokens.as,
-            this.tokens.leftParen,
-            ...this.params,
-            this.tokens.rightParen,
-            this.tokens.as,
-            this.returnType
-        ) ?? interpolatedRange;
-
-    }
-
-    public readonly range: Range;
-
-    public tokens = {} as {
-        functionType: Token;
-        name: Identifier;
-        leftParen: Token;
-        rightParen: Token;
-        as: Token;
-    };
-
-    public getReturnType() {
-        return this.returnType.type;
+        super(name, func, undefined);
     }
 
     walk(visitor: WalkVisitor, options: WalkOptions) {
         //nothing to walk
     }
-
-    getTypedef(state: BrsTranspileState) {
-        const result = [] as TranspileResult;
-        for (let annotation of this.annotations ?? []) {
-            result.push(
-                ...annotation.getTypedef(state),
-                state.newline,
-                state.indent()
-            );
-        }
-
-        result.push(
-            this.tokens.functionType.text,
-            ' ',
-            this.tokens.name.text,
-            '('
-        );
-        const params = this.params ?? [];
-        for (let i = 0; i < params.length; i++) {
-            if (i > 0) {
-                result.push(', ');
-            }
-            const param = params[i];
-            result.push(param.name.text);
-            if (param.asToken && param.type) {
-                result.push(
-                    ' as ',
-                    ...param.type.transpile(state)
-                );
-            }
-        }
-        result.push(
-            ')'
-        );
-        if (this.returnType) {
-            result.push(
-                ' as ',
-                ...this.returnType.transpile(state)
-            );
-        }
-        return result;
-    }
-
-    public transpile(state: BrsTranspileState): TranspileResult {
-        throw new Error('Method not implemented.');
-    }
 }
 
-export class ClassStatement extends Statement implements TypedefProvider, SymbolContainer {
+export class ClassStatement extends Statement implements TypedefProvider, MemberSymbolTableProvider {
     readonly symbolTable: SymbolTable = new SymbolTable();
     readonly memberTable: SymbolTable = new SymbolTable();
 
@@ -1593,7 +1566,7 @@ export class ClassStatement extends Statement implements TypedefProvider, Symbol
 
     public readonly range: Range;
 
-    public getCustomType(): CustomType {
+    public getThisBscType(): CustomType {
         return new CustomType(this.getName(ParseMode.BrighterScript), this.memberTable);
     }
 
@@ -1605,11 +1578,11 @@ export class ClassStatement extends Statement implements TypedefProvider, Symbol
         return constructorFuncType;
     }
 
-    public buildSymbolTable(parentClass?: ClassStatement) {
+    public buildSymbolTable(parentClass?: InheritableStatement) {
         this.symbolTable.clear();
-        this.symbolTable.addSymbol('m', this.name?.range, this.getCustomType());
+        this.symbolTable.addSymbol('m', this.name?.range, this.getThisBscType());
         this.memberTable.clear();
-        if (parentClass) {
+        if (isClassStatement(parentClass)) {
             this.symbolTable.addSymbol('super', this.parentClassName?.range, parentClass.getConstructorFunctionType());
             this.memberTable.setParent(parentClass?.memberTable);
         }
@@ -1704,7 +1677,7 @@ export class ClassStatement extends Statement implements TypedefProvider, Symbol
         return myIndex - 1;
     }
 
-    public hasParentClass() {
+    public hasParent() {
         return !!this.parentClassName;
     }
 
@@ -1713,7 +1686,7 @@ export class ClassStatement extends Statement implements TypedefProvider, Symbol
      * @returns array of possible parent class names
      */
     public getPossibleFullParentNames(): string[] {
-        if (!this.hasParentClass()) {
+        if (!this.hasParent()) {
             return [];
         }
         if (this.parentClassName?.getNameParts().length > 1) {
@@ -2094,7 +2067,7 @@ export class ClassMethodStatement extends FunctionStatement {
      * Inject field initializers at the top of the `new` function (after any present `super()` call)
      */
     private injectFieldInitializersForConstructor(state: BrsTranspileState) {
-        let startingIndex = state.classStatement.hasParentClass() ? 1 : 0;
+        let startingIndex = state.classStatement.hasParent() ? 1 : 0;
 
         let newStatements = [] as Statement[];
         //insert the field initializers in order
@@ -2204,6 +2177,9 @@ export class ClassFieldStatement extends Statement implements TypedefProvider {
     }
 }
 export type ClassMemberStatement = ClassFieldStatement | ClassMethodStatement;
+export type InterfaceMemberStatement = InterfaceFieldStatement | InterfaceMethodStatement;
+export type MemberFieldStatement = ClassFieldStatement | InterfaceFieldStatement;
+export type MemberMethodStatement = ClassMethodStatement | InterfaceMethodStatement;
 
 export class TryCatchStatement extends Statement {
     constructor(
diff --git a/src/types/BscType.ts b/src/types/BscType.ts
index 7bde267b..86f017cc 100644
--- a/src/types/BscType.ts
+++ b/src/types/BscType.ts
@@ -1,5 +1,5 @@
 import type { Position } from 'vscode-languageserver';
-import { isLazyType } from '../astUtils/reflection';
+import { isLazyType, isInterfaceType } from '../astUtils/reflection';
 import type { BrsFile } from '../files/BrsFile';
 import type { Scope } from '../Scope';
 import type { SymbolTable } from '../SymbolTable';
@@ -30,3 +30,18 @@ export function getTypeFromContext(type: BscType, context?: TypeContext) {
     }
     return type;
 }
+
+export function checkAssignabilityToInterface(sourceType: BscType, targetType: BscType, context?: TypeContext) {
+    if (!sourceType.memberTable || !targetType.memberTable || !isInterfaceType(targetType)) {
+        return false;
+    }
+    let isSuperSet = true;
+    const targetSymbols = targetType.memberTable?.getAllSymbols();
+    for (const targetSymbol of targetSymbols) {
+        // TODO TYPES: this ignores union types
+
+        const mySymbolsWithName = sourceType.memberTable.getSymbol(targetSymbol.name);
+        isSuperSet = isSuperSet && mySymbolsWithName && mySymbolsWithName.length > 0 && mySymbolsWithName[0].type.isAssignableTo(targetSymbol.type, context);
+    }
+    return isSuperSet;
+}
diff --git a/src/types/CustomType.ts b/src/types/CustomType.ts
index 49a7c310..884b1ff2 100644
--- a/src/types/CustomType.ts
+++ b/src/types/CustomType.ts
@@ -1,6 +1,7 @@
 import { isCustomType, isDynamicType, isObjectType } from '../astUtils/reflection';
 import type { SymbolTable } from '../SymbolTable';
 import type { BscType, SymbolContainer, TypeContext } from './BscType';
+import { checkAssignabilityToInterface } from './BscType';
 
 export class CustomType implements BscType, SymbolContainer {
 
@@ -20,6 +21,12 @@ export class CustomType implements BscType, SymbolContainer {
         if (ancestorTypes?.find(ancestorType => targetType.equals(ancestorType, context))) {
             return true;
         }
+        if (this.memberTable && targetType.memberTable) {
+            // both have symbol tables, so check if the target is an interface and has all the members of the target
+            if (checkAssignabilityToInterface(this, targetType, context)) {
+                return true;
+            }
+        }
         return (
             this.equals(targetType, context) ||
             isObjectType(targetType) ||
diff --git a/src/types/InterfaceType.spec.ts b/src/types/InterfaceType.spec.ts
index 4185a098..8841f4cb 100644
--- a/src/types/InterfaceType.spec.ts
+++ b/src/types/InterfaceType.spec.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 import { assert } from 'sinon';
+import { SymbolTable } from '../SymbolTable';
 import type { BscType } from './BscType';
+import { CustomType } from './CustomType';
 import { DynamicType } from './DynamicType';
 import { IntegerType } from './IntegerType';
 import { InterfaceType } from './InterfaceType';
@@ -10,11 +12,11 @@ import { StringType } from './StringType';
 describe('InterfaceType', () => {
     describe('toString', () => {
         it('returns empty curly braces when no members', () => {
-            expect(iface({}).toString()).to.eql('{}');
+            expect(iface({}).toJsString()).to.eql('{}');
         });
 
         it('includes member types', () => {
-            expect(iface({ name: new StringType() }).toString()).to.eql('{ name: string; }');
+            expect(iface({ name: new StringType() }).toJsString()).to.eql('{ name: string; }');
         });
 
         it('includes nested object types', () => {
@@ -25,7 +27,7 @@ describe('InterfaceType', () => {
                         age: new IntegerType()
                     })
                 }
-                ).toString()
+                ).toJsString()
             ).to.eql('{ name: string; parent: { age: integer; }; }');
         });
     });
@@ -52,6 +54,12 @@ describe('InterfaceType', () => {
                 iface({ name: new StringType() }).equals(iface({ name: new IntegerType() }))
             ).to.be.false;
         });
+
+        it('ignores order', () => {
+            expect(
+                iface({ name: new StringType(), age: new IntegerType(), data: new CustomType('data') }).equals(iface({ age: new IntegerType(), data: new CustomType('data'), name: new StringType() }))
+            ).to.be.true;
+        });
     });
 
     describe('isAssignableTo', () => {
@@ -90,14 +98,17 @@ describe('InterfaceType', () => {
         });
 
         it('matches properties in mismatched order', () => {
+            const symbolTable1 = makeSymbolTable({
+                name: new StringType(),
+                age: new IntegerType()
+            });
+            const symbolTable2 = makeSymbolTable({
+                age: new IntegerType(),
+                name: new StringType()
+            });
+
             expect(
-                new InterfaceType(new Map([
-                    ['name', new StringType()],
-                    ['age', new IntegerType()]
-                ])).isAssignableTo(new InterfaceType(new Map([
-                    ['age', new IntegerType()],
-                    ['name', new StringType()]
-                ])))
+                new InterfaceType('interfaceOne', symbolTable1).isAssignableTo(new InterfaceType('interfaceTwo', symbolTable2))
             ).to.be.true;
         });
 
@@ -148,7 +159,7 @@ describe('InterfaceType', () => {
             });
         });
 
-        it('accepts with source member having dyanmic prop type', () => {
+        it('accepts with source member having dynamic prop type', () => {
             expectAssignable({
                 parent: iface({
                     name: new StringType(),
@@ -159,7 +170,7 @@ describe('InterfaceType', () => {
             });
         });
 
-        it('accepts with target member having dyanmic prop type', () => {
+        it('accepts with target member having dynamic prop type', () => {
             expectAssignable({
                 parent: new DynamicType()
             }, {
@@ -183,19 +194,28 @@ describe('InterfaceType', () => {
     });
 });
 
-function iface(members: Record<string, BscType>) {
-    return new InterfaceType(
-        new Map(
-            Object.entries(members)
-        )
-    );
+let ifaceCount = 0;
+
+function iface(members: Record<string, BscType>, name?: string) {
+    name = name ?? 'SomeIFace' + ifaceCount;
+    ifaceCount++;
+    return new InterfaceType(name, makeSymbolTable(members));
+}
+
+function makeSymbolTable(members: Record<string, BscType>) {
+    const symbols = new SymbolTable();
+
+    for (const key in members) {
+        symbols.addSymbol(key, null, members[key]);
+    }
+    return symbols;
 }
 
 function expectAssignable(targetMembers: Record<string, BscType>, sourceMembers: Record<string, BscType>) {
     const targetIface = iface(targetMembers);
     const sourceIface = iface(sourceMembers);
     if (!sourceIface.isAssignableTo(targetIface)) {
-        assert.fail(`expected type ${targetIface.toString()} to be assignable to type ${sourceIface.toString()}`);
+        assert.fail(`expected type ${targetIface.toJsString()} to be assignable to type ${sourceIface.toJsString()}`);
     }
 }
 
@@ -203,6 +223,6 @@ function expectNotAssignable(targetMembers: Record<string, BscType>, sourceMembe
     const targetIface = iface(targetMembers);
     const sourceIface = iface(sourceMembers);
     if (sourceIface.isAssignableTo(targetIface)) {
-        assert.fail(`expected type ${targetIface.toString()} to not be assignable to type ${sourceIface.toString()}`);
+        assert.fail(`expected type ${targetIface.toJsString()} to not be assignable to type ${sourceIface.toJsString()}`);
     }
 }
diff --git a/src/types/InterfaceType.ts b/src/types/InterfaceType.ts
index dfbafb00..571498ce 100644
--- a/src/types/InterfaceType.ts
+++ b/src/types/InterfaceType.ts
@@ -1,54 +1,54 @@
 import { isDynamicType, isInterfaceType, isObjectType } from '../astUtils/reflection';
-import type { BscType } from './BscType';
+import type { SymbolTable } from '../SymbolTable';
+import type { BscType, SymbolContainer, TypeContext } from './BscType';
+import { checkAssignabilityToInterface } from './BscType';
 
-export class InterfaceType implements BscType {
+export class InterfaceType implements BscType, SymbolContainer {
     public constructor(
-        public members: Map<string, BscType>
+        public name: string, public memberTable: SymbolTable = null
     ) {
 
     }
 
-    /**
-     * The name of the interface. Can be null.
-     */
-    public name: string;
-
-    public isAssignableTo(targetType: BscType) {
-        //we must have all of the members of the target type, and they must be equivalent types
-        if (isInterfaceType(targetType)) {
-            for (const [targetMemberName, targetMemberType] of targetType.members) {
-                //we don't have the target member
-                if (!this.members.has(targetMemberName)) {
-                    return false;
-                }
-                //our member's type is not assignable to the target member type
-                if (!this.members.get(targetMemberName).isAssignableTo(targetMemberType)) {
-                    return false;
-                }
-            }
-            //we have all of the target member's types. we are assignable!
+    public isAssignableTo(targetType: BscType, context?: TypeContext) {
+        const ancestorTypes = context?.scope?.getAncestorTypeListByContext(this, context);
+        if (ancestorTypes?.find(ancestorType => targetType.equals(ancestorType, context))) {
             return true;
-
-            //we are always assignable to dynamic or object
-        } else if (isDynamicType(targetType) || isObjectType(targetType)) {
-            return true;
-
-            //not assignable to any other object types
-        } else {
-            return false;
         }
+        if (this.memberTable && targetType.memberTable) {
+            // both have symbol tables, so check if this interface has all the members of the target
+            return checkAssignabilityToInterface(this, targetType, context);
+        }
+        return (
+            this.equals(targetType, context) ||
+            isObjectType(targetType) ||
+            isDynamicType(targetType)
+        );
     }
 
     public isConvertibleTo(targetType: BscType) {
         return this.isAssignableTo(targetType);
     }
 
-    public toString() {
+    public toString(): string {
+        return this.name;
+    }
+
+    /**
+     * Gets a string representation of the Interface that looks like javascript
+     * @returns {string}
+     */
+    public toJsString() {
         let result = '{';
-        for (const [key, type] of this.members.entries()) {
-            result += ' ' + key + ': ' + type.toString() + ';';
+        const memberSymbols = (this.memberTable?.getAllSymbols() || []).sort((a, b) => a.name.localeCompare(b.name));
+        for (const symbol of memberSymbols) {
+            let symbolTypeString = symbol.type.toString();
+            if (isInterfaceType(symbol.type)) {
+                symbolTypeString = symbol.type.toJsString();
+            }
+            result += ' ' + symbol.name + ': ' + symbolTypeString + ';';
         }
-        if (this.members.size > 0) {
+        if (memberSymbols.length > 0) {
             result += ' ';
         }
         return result + '}';
@@ -58,13 +58,12 @@ export class InterfaceType implements BscType {
         return 'object';
     }
 
-    public equals(targetType: BscType): boolean {
+    public equals(targetType: BscType, context?: TypeContext): boolean {
+
         if (isInterfaceType(targetType)) {
-            if (targetType.members.size !== this.members.size) {
-                return false;
-            }
-            return targetType.isAssignableTo(this);
+            return this.isAssignableTo(targetType, context) && targetType.isAssignableTo(this, context);
         }
         return false;
     }
+
 }
diff --git a/src/util.ts b/src/util.ts
index 4ac35d8e..2245dfa5 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1030,9 +1030,8 @@ export class Util {
                 }
                 if (!typeClass && allowBrighterscriptTypes) {
                     typeClass = new LazyType((context) => {
-                        return context?.scope?.getClass(typeText, currentNamespaceName?.getName())?.getCustomType();
+                        return context?.scope?.getNamedTypeStatement(typeText, currentNamespaceName?.getName())?.getThisBscType();
                     });
-
                 }
 
                 return typeClass;
diff --git a/src/validators/ClassValidator.ts b/src/validators/ClassValidator.ts
index 85068739..b5797554 100644
--- a/src/validators/ClassValidator.ts
+++ b/src/validators/ClassValidator.ts
@@ -1,12 +1,14 @@
 import type { Scope } from '../Scope';
 import { DiagnosticMessages } from '../DiagnosticMessages';
-import type { CallExpression } from '../parser/Expression';
+import type { CallExpression, NamespacedVariableNameExpression } from '../parser/Expression';
 import { ParseMode } from '../parser/Parser';
-import type { ClassFieldStatement, ClassMethodStatement, ClassStatement } from '../parser/Statement';
+import type { References } from '../parser/Parser';
+import type { ClassFieldStatement, ClassMethodStatement, ClassStatement, InterfaceStatement, MemberFieldStatement, MemberMethodStatement, Statement } from '../parser/Statement';
 import { CancellationTokenSource, Location } from 'vscode-languageserver';
+import type { DiagnosticSeverity } from 'vscode-languageserver';
 import { URI } from 'vscode-uri';
 import util from '../util';
-import { isCallExpression, isClassFieldStatement, isClassMethodStatement, isCustomType } from '../astUtils/reflection';
+import { isCallExpression, isClassFieldStatement, isClassMethodStatement, isClassStatement, isCustomType, isInterfaceFieldStatement, isInterfaceMethodStatement, isInterfaceStatement, isInterfaceType } from '../astUtils/reflection';
 import type { BscFile, BsDiagnostic } from '../interfaces';
 import { createVisitor, WalkMode } from '../astUtils/visitors';
 import type { BrsFile } from '../files/BrsFile';
@@ -14,12 +16,15 @@ import { TokenKind } from '../lexer/TokenKind';
 import { DynamicType } from '../types/DynamicType';
 import type { BscType, TypeContext } from '../types/BscType';
 import { getTypeFromContext } from '../types/BscType';
+import type { Identifier } from '../lexer/Token';
 
-export class BsClassValidator {
+
+export class BsClassValidator implements BsClassValidator {
     private scope: Scope;
     private file: BrsFile;
     public diagnostics: BsDiagnostic[];
     private classes: Map<string, AugmentedClassStatement>;
+    private interfaces: Map<string, AugmentedInterfaceStatement>;
 
     get typeContext(): TypeContext {
         return { scope: this.scope, file: this.file };
@@ -31,6 +36,7 @@ export class BsClassValidator {
         this.diagnostics = [];
 
         this.findClasses();
+        this.findInterfaces();
         this.findNamespaceNonNamespaceCollisions();
         this.linkClassesWithParents();
         this.validateMemberCollisions();
@@ -54,6 +60,18 @@ export class BsClassValidator {
         return cls;
     }
 
+    /**
+    * Given an interface name optionally prefixed with a namespace name, find the interface that matches
+    */
+    private getInterfaceByName(ifaceName: string, namespaceName?: string) {
+        let fullName = util.getFullyQualifiedClassName(ifaceName, namespaceName);
+        let iface = this.interfaces.get(fullName.toLowerCase());
+        //if we couldn't find the interface by its full namespaced name, look for a global interface with that name
+        if (!iface) {
+            iface = this.interfaces.get(ifaceName.toLowerCase());
+        }
+        return iface;
+    }
 
     /**
      * Find all "new" statements in the program,
@@ -115,6 +133,26 @@ export class BsClassValidator {
                 });
             }
         }
+        for (const [ifaceName, ifaceStatement] of this.interfaces) {
+            //catch namespace class collision with global class
+            let nonNamespaceInterface = this.interfaces.get(util.getTextAfterFinalDot(ifaceName).toLowerCase());
+            if (ifaceStatement.namespaceName && nonNamespaceInterface) {
+                this.diagnostics.push({
+                    ...DiagnosticMessages.namespacedInterfaceCannotShareNameWithNonNamespacedInterface(
+                        nonNamespaceInterface.tokens.name.text
+                    ),
+                    file: ifaceStatement.file,
+                    range: ifaceStatement.tokens.name.range,
+                    relatedInformation: [{
+                        location: Location.create(
+                            URI.file(nonNamespaceInterface.file.srcPath).toString(),
+                            nonNamespaceInterface.tokens.name.range
+                        ),
+                        message: 'Original interface declared here'
+                    }]
+                });
+            }
+        }
     }
 
     private verifyChildConstructor() {
@@ -125,7 +163,7 @@ export class BsClassValidator {
                 //this class has a "new method"
                 newMethod &&
                 //this class has a parent class
-                classStatement.parentClass
+                classStatement.parent
             ) {
                 //prevent use of `m.` anywhere before the `super()` call
                 const cancellationToken = new CancellationTokenSource();
@@ -163,13 +201,25 @@ export class BsClassValidator {
         }
     }
 
-    private validateMemberCollisions() {
-        for (const [, classStatement] of this.classes) {
+    private isMemberFieldStatement(stmt: Statement): stmt is MemberFieldStatement {
+        return isClassFieldStatement(stmt) || isInterfaceFieldStatement(stmt);
+    }
+
+    private isMemberMethodStatement(stmt: Statement): stmt is MemberMethodStatement {
+        return isClassMethodStatement(stmt) || isInterfaceMethodStatement(stmt);
+    }
+
+    private isMemberStatement(stmt: Statement): stmt is Statement & MemberFieldOrMethod {
+        return this.isMemberFieldStatement(stmt) || this.isMemberMethodStatement(stmt);
+    }
+
+    private validateMemberCollisionsForStatements<T extends AugmentedClassStatement | AugmentedInterfaceStatement>(map: Map<string, T>) {
+        for (const [, bodyStatement] of map) {
             let methods = {};
             let fields = {};
 
-            for (let statement of classStatement.body) {
-                if (isClassMethodStatement(statement) || isClassFieldStatement(statement)) {
+            for (let statement of bodyStatement.body) {
+                if (this.isMemberStatement(statement)) {
                     let member = statement;
                     let lowerMemberName = member.name.text.toLowerCase();
 
@@ -177,15 +227,15 @@ export class BsClassValidator {
                     if (methods[lowerMemberName] || fields[lowerMemberName]) {
                         this.diagnostics.push({
                             ...DiagnosticMessages.duplicateIdentifier(member.name.text),
-                            file: classStatement.file,
+                            file: bodyStatement.file,
                             range: member.name.range
                         });
                     }
 
-                    let memberType = isClassFieldStatement(member) ? 'field' : 'method';
-                    let ancestorAndMember = this.getAncestorMember(classStatement, lowerMemberName);
+                    let memberType = this.isMemberFieldStatement(member) ? 'field' : 'method';
+                    let ancestorAndMember = this.getAncestorMember(bodyStatement, lowerMemberName);
                     if (ancestorAndMember) {
-                        let ancestorMemberKind = isClassFieldStatement(ancestorAndMember.member) ? 'field' : 'method';
+                        let ancestorMemberKind = this.isMemberFieldStatement(ancestorAndMember.member) ? 'field' : 'method';
 
                         //mismatched member type (field/method in child, opposite in ancestor)
                         if (memberType !== ancestorMemberKind) {
@@ -193,19 +243,19 @@ export class BsClassValidator {
                                 ...DiagnosticMessages.classChildMemberDifferentMemberTypeThanAncestor(
                                     memberType,
                                     ancestorMemberKind,
-                                    ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
+                                    ancestorAndMember.ancestorStatement.getName(ParseMode.BrighterScript)
                                 ),
-                                file: classStatement.file,
+                                file: bodyStatement.file,
                                 range: member.range
                             });
                         }
 
                         //child field has same name as parent
-                        if (isClassFieldStatement(member)) {
+                        if (this.isMemberFieldStatement(member)) {
                             let ancestorMemberType: BscType = new DynamicType();
-                            if (isClassFieldStatement(ancestorAndMember.member)) {
+                            if (this.isMemberFieldStatement(ancestorAndMember.member)) {
                                 ancestorMemberType = ancestorAndMember.member.getType();
-                            } else if (isClassMethodStatement(ancestorAndMember.member)) {
+                            } else if (this.isMemberMethodStatement(ancestorAndMember.member)) {
                                 ancestorMemberType = ancestorAndMember.member.func.getFunctionType();
                             }
                             const childFieldType = member.getType();
@@ -216,13 +266,13 @@ export class BsClassValidator {
                                 const ancestorFieldTypeName = ancestorMemberType?.toString(this.typeContext) ?? (ancestorAndMember.member as ClassFieldStatement).type.getText();
                                 this.diagnostics.push({
                                     ...DiagnosticMessages.childFieldTypeNotAssignableToBaseProperty(
-                                        classStatement.getName(ParseMode.BrighterScript),
-                                        ancestorAndMember.classStatement.getName(ParseMode.BrighterScript),
+                                        bodyStatement.getName(ParseMode.BrighterScript),
+                                        ancestorAndMember.ancestorStatement.getName(ParseMode.BrighterScript),
                                         member.name.text,
                                         childFieldTypeName,
                                         ancestorFieldTypeName
                                     ),
-                                    file: classStatement.file,
+                                    file: bodyStatement.file,
                                     range: member.range
                                 });
                             }
@@ -239,9 +289,9 @@ export class BsClassValidator {
                         ) {
                             this.diagnostics.push({
                                 ...DiagnosticMessages.missingOverrideKeyword(
-                                    ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
+                                    ancestorAndMember.ancestorStatement.getName(ParseMode.BrighterScript)
                                 ),
-                                file: classStatement.file,
+                                file: bodyStatement.file,
                                 range: member.range
                             });
                         }
@@ -249,27 +299,27 @@ export class BsClassValidator {
                         //child member has different visiblity
                         if (
                             //is a method
-                            isClassMethodStatement(member) &&
+                            isClassMethodStatement(member) && isClassMethodStatement(ancestorAndMember.member) &&
                             (member.accessModifier?.kind ?? TokenKind.Public) !== (ancestorAndMember.member.accessModifier?.kind ?? TokenKind.Public)
                         ) {
                             this.diagnostics.push({
                                 ...DiagnosticMessages.mismatchedOverriddenMemberVisibility(
-                                    classStatement.name.text,
+                                    bodyStatement.name.text,
                                     ancestorAndMember.member.name?.text,
                                     member.accessModifier?.text || 'public',
                                     ancestorAndMember.member.accessModifier?.text || 'public',
-                                    ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
+                                    ancestorAndMember.ancestorStatement.getName(ParseMode.BrighterScript)
                                 ),
-                                file: classStatement.file,
+                                file: bodyStatement.file,
                                 range: member.range
                             });
                         }
                     }
 
-                    if (isClassMethodStatement(member)) {
+                    if (this.isMemberMethodStatement(member)) {
                         methods[lowerMemberName] = member;
 
-                    } else if (isClassFieldStatement(member)) {
+                    } else if (this.isMemberFieldStatement(member)) {
                         fields[lowerMemberName] = member;
                     }
                 }
@@ -277,35 +327,45 @@ export class BsClassValidator {
         }
     }
 
+    private validateMemberCollisions() {
+        this.validateMemberCollisionsForStatements(this.classes);
+        this.validateMemberCollisionsForStatements(this.interfaces);
+    }
+
 
-    /**
-     * Check the types for fields, and validate they are valid types
-     */
-    private validateFieldTypes() {
-        for (const [, classStatement] of this.classes) {
-            for (let statement of classStatement.body) {
-                if (isClassFieldStatement(statement)) {
-                    let fieldType = getTypeFromContext(statement.getType(), this.typeContext);
-                    const fieldTypeName = fieldType?.toString(this.typeContext) ?? statement.type?.getText();
+    private validateFieldTypesForStatements<T extends AugmentedClassStatement | AugmentedInterfaceStatement>(map: Map<string, T>) {
+        for (const [, statement] of map) {
+            for (let bodyStatement of statement.body) {
+                if (this.isMemberFieldStatement(bodyStatement)) {
+                    let fieldType = getTypeFromContext(bodyStatement.getType(), this.typeContext);
+                    const fieldTypeName = fieldType?.toString(this.typeContext) ?? bodyStatement.type?.getText();
                     const lowerFieldTypeName = fieldTypeName?.toLowerCase();
 
                     let addDiagnostic = false;
                     if (isCustomType(fieldType)) {
                         if (lowerFieldTypeName) {
-                            const currentNamespaceName = classStatement.namespaceName?.getName(ParseMode.BrighterScript);
+                            const currentNamespaceName = bodyStatement.namespaceName?.getName(ParseMode.BrighterScript);
                             //check if this custom type is in our class map
                             if (!this.getClassByName(lowerFieldTypeName, currentNamespaceName)) {
                                 addDiagnostic = true;
                             }
                         }
+                    } else if (isInterfaceType(fieldType)) {
+                        if (lowerFieldTypeName) {
+                            const currentNamespaceName = bodyStatement.namespaceName?.getName(ParseMode.BrighterScript);
+                            //check if this custom type is in our class map
+                            if (!this.getInterfaceByName(lowerFieldTypeName, currentNamespaceName)) {
+                                addDiagnostic = true;
+                            }
+                        }
                     } else if (!fieldType) {
                         addDiagnostic = true;
                     }
                     if (addDiagnostic) {
                         this.diagnostics.push({
                             ...DiagnosticMessages.cannotFindType(fieldTypeName),
-                            range: statement.type.range,
-                            file: classStatement.file
+                            range: bodyStatement.type.range,
+                            file: statement.file
                         });
                     }
                 }
@@ -313,37 +373,52 @@ export class BsClassValidator {
         }
     }
 
+    /**
+     * Check the types for fields, and validate they are valid types
+     */
+    private validateFieldTypes() {
+        this.validateFieldTypesForStatements(this.classes);
+        this.validateFieldTypesForStatements(this.interfaces);
+    }
+
     /**
      * Get the closest member with the specified name (case-insensitive)
      */
-    private getAncestorMember(classStatement: AugmentedClassStatement, memberName: string) {
+    private getAncestorMember(classStatement: AugmentedClassStatement | AugmentedInterfaceStatement, memberName: string) {
         let lowerMemberName = memberName.toLowerCase();
-        let ancestor = classStatement.parentClass;
+        let ancestor = classStatement.parent;
         while (ancestor) {
             let member = ancestor.memberMap[lowerMemberName];
             if (member) {
                 return {
                     member: member,
-                    classStatement: ancestor
+                    ancestorStatement: ancestor
                 };
             }
-            ancestor = ancestor.parentClass;
+            ancestor = ancestor.parent;
         }
     }
 
     private cleanUp() {
         //unlink all classes from their parents so it doesn't mess up the next scope
         for (const [, classStatement] of this.classes) {
-            delete classStatement.parentClass;
+            delete classStatement.parent;
             delete classStatement.file;
         }
+        //unlink all interfaces from their parents so it doesn't mess up the next scope
+        for (const [, interfaceStatement] of this.interfaces) {
+            delete interfaceStatement.parent;
+            delete interfaceStatement.file;
+        }
     }
 
-    private findClasses() {
-        this.classes = new Map();
+
+    private findStatements<T extends AugmentedClassStatement | AugmentedInterfaceStatement>(referencesFunc: (references: References) => T[], dupeDiagnosticFunc: (a: string, b: string) => { message: string; code: number; severity: DiagnosticSeverity }): Map<string, T> {
+        const map = new Map();
         this.scope.enumerateBrsFiles((file) => {
-            for (let x of file.parser.references.classStatements ?? []) {
-                let classStatement = x as AugmentedClassStatement;
+            const references = referencesFunc(file.parser.references);
+            for (let x of references ?? []) {
+                let classStatement = x;
                 let name = classStatement.getName(ParseMode.BrighterScript);
                 //skip this class if it doesn't have a name
                 if (!name) {
@@ -351,23 +426,23 @@ export class BsClassValidator {
                 }
                 let lowerName = name.toLowerCase();
                 //see if this class was already defined
-                let alreadyDefinedClass = this.classes.get(lowerName);
+                let alreadyDefinedClass = map.get(lowerName);
 
                 //if we don't already have this class, register it
                 if (!alreadyDefinedClass) {
-                    this.classes.set(lowerName, classStatement);
+                    map.set(lowerName, classStatement);
                     classStatement.file = file;
 
                     //add a diagnostic about this class already existing
                 } else {
                     this.diagnostics.push({
-                        ...DiagnosticMessages.duplicateClassDeclaration(this.scope.name, name),
+                        ...dupeDiagnosticFunc(this.scope.name, name),
                         file: file,
                         range: classStatement.name.range,
                         relatedInformation: [{
                             location: Location.create(
                                 URI.file(alreadyDefinedClass.file.srcPath).toString(),
-                                this.classes.get(lowerName).range
+                                map.get(lowerName).range
                             ),
                             message: ''
                         }]
@@ -375,62 +450,72 @@ export class BsClassValidator {
                 }
             }
         });
+        return map;
     }
 
-    private linkClassesWithParents() {
+    private findClasses() {
+        this.classes = this.findStatements(
+            (references) => references.classStatements,
+            DiagnosticMessages.duplicateClassDeclaration
+        );
+    }
+
+    private findInterfaces() {
+        this.interfaces = this.findStatements(
+            (references) => references.interfaceStatements,
+            DiagnosticMessages.duplicateInterfaceDeclaration
+        );
+    }
+
+    private linkWithParents<T extends AugmentedClassStatement | AugmentedInterfaceStatement>(map: Map<string, T>) {
         //link all classes with their parents
-        for (const [, classStatement] of this.classes) {
-            let parentClassName = classStatement.parentClassName?.getName(ParseMode.BrighterScript);
-            if (parentClassName) {
-                let relativeName: string;
-                let absoluteName: string;
-
-                //if the parent class name was namespaced in the declaration of this class,
-                //compute the relative name of the parent class and the absolute name of the parent class
-                if (parentClassName.indexOf('.') > 0) {
-                    absoluteName = parentClassName;
-                    let parts = parentClassName.split('.');
-                    relativeName = parts[parts.length - 1];
-
-                    //the parent class name was NOT namespaced.
-                    //compute the relative name of the parent class and prepend the current class's namespace
-                    //to the beginning of the parent class's name
-                } else {
-                    if (classStatement.namespaceName) {
-                        absoluteName = `${classStatement.namespaceName.getName(ParseMode.BrighterScript)}.${parentClassName}`;
-                    } else {
-                        absoluteName = parentClassName;
+        for (const [, classStatement] of map) {
+            if (classStatement.hasParent()) {
+                let parentNames = classStatement.getPossibleFullParentNames();
+                let parentClass: T;
+                for (const parentName of parentNames) {
+                    parentClass = map.get(parentName.toLowerCase());
+                    if (parentClass) {
+                        break;
                     }
-                    relativeName = parentClassName;
                 }
-
-                let relativeParent = this.classes.get(relativeName.toLowerCase());
-                let absoluteParent = this.classes.get(absoluteName.toLowerCase());
-
-                let parentClass: AugmentedClassStatement;
-                //if we found a relative parent class
-                if (relativeParent) {
-                    parentClass = relativeParent;
-
-                    //we found an absolute parent class
-                } else if (absoluteParent) {
-                    parentClass = absoluteParent;
-
-                    //couldn't find the parent class
+                if (parentClass) {
+                    classStatement.parent = parentClass;
                 } else {
+
+                    let parentExpression: NamespacedVariableNameExpression;
+                    if (isClassStatement(classStatement)) {
+                        parentExpression = classStatement.parentClassName;
+                    } else if (isInterfaceStatement(classStatement)) {
+                        parentExpression = classStatement.parentInterfaceName;
+                    }
                     this.diagnostics.push({
-                        ...DiagnosticMessages.classCouldNotBeFound(parentClassName, this.scope.name),
+                        ...DiagnosticMessages.classCouldNotBeFound(parentExpression.getName(ParseMode.BrighterScript), this.scope.name),
                         file: classStatement.file,
-                        range: classStatement.parentClassName.range
+                        range: parentExpression?.range
                     });
                 }
-                classStatement.parentClass = parentClass;
+
             }
         }
     }
 
+    private linkClassesWithParents() {
+        this.linkWithParents(this.classes);
+        this.linkWithParents(this.interfaces);
+    }
 }
 type AugmentedClassStatement = ClassStatement & {
-    file: BscFile;
-    parentClass: AugmentedClassStatement;
+    file?: BscFile;
+    parent?: AugmentedClassStatement;
+};
+
+
+type AugmentedInterfaceStatement = InterfaceStatement & {
+    file?: BscFile;
+    parent?: AugmentedInterfaceStatement;
 };
+
+interface MemberFieldOrMethod {
+    name: Identifier;
+}


Test patch:
diff --git a/src/parser/tests/statement/InterfaceStatement.spec.ts b/src/parser/tests/statement/InterfaceStatement.spec.ts
index af5831b2..b143a4cd 100644
--- a/src/parser/tests/statement/InterfaceStatement.spec.ts
+++ b/src/parser/tests/statement/InterfaceStatement.spec.ts
@@ -1,6 +1,12 @@
-import { expectZeroDiagnostics, getTestGetTypedef } from '../../../testHelpers.spec';
+import { expectDiagnostics, expectZeroDiagnostics, getTestGetTypedef } from '../../../testHelpers.spec';
 import { standardizePath as s } from '../../../util';
 import { Program } from '../../../Program';
+import { expect } from 'chai';
+import { isBooleanType, isTypedFunctionType, isIntegerType, isStringType } from '../../../astUtils/reflection';
+import { DiagnosticMessages } from '../../../DiagnosticMessages';
+import { Lexer } from '../../../lexer/Lexer';
+import { Parser, ParseMode } from '../../Parser';
+import { ClassStatement, InterfaceStatement } from '../../Statement';
 
 describe('InterfaceStatement', () => {
     const rootDir = s`${process.cwd()}/.tmp/rootDir`;
@@ -74,4 +80,195 @@ describe('InterfaceStatement', () => {
         program.validate();
         expectZeroDiagnostics(program);
     });
+
+    it('throws exception when used in brightscript scope', () => {
+        let { tokens } = Lexer.scan(`
+                interface Iface
+                end interface
+            `);
+        let { diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrightScript });
+        expect(diagnostics[0]?.code).to.equal(DiagnosticMessages.bsFeatureNotSupportedInBrsFiles('').code);
+    });
+
+    it('parses empty interface', () => {
+        let { tokens } = Lexer.scan(`
+                interface Person
+                end interface
+            `);
+        let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        expectZeroDiagnostics(diagnostics);
+        expect(statements[0]).instanceof(InterfaceStatement);
+    });
+
+    it('parses interface with fields', () => {
+        let { tokens } = Lexer.scan(`
+                interface Iface
+                    age as integer
+                    name as string
+                end interface
+            `);
+        let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        expectZeroDiagnostics(diagnostics);
+        expect(statements[0]).instanceof(InterfaceStatement);
+    });
+
+    it('parses interface with methods', () => {
+        let { tokens } = Lexer.scan(`
+                interface Iface
+                    function getNum() as integer
+                    function getData() as Object
+                end interface
+            `);
+        let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        expectZeroDiagnostics(diagnostics);
+        expect(statements[0]).instanceof(InterfaceStatement);
+    });
+
+    it('parses interface with classes as fields', () => {
+        let { tokens } = Lexer.scan(`
+                class Klass
+                end class
+
+                interface Iface
+                    myKlass as Klass
+                end interface
+            `);
+        let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        expectZeroDiagnostics(diagnostics);
+        expect(statements[0]).instanceof(ClassStatement);
+        expect(statements[1]).instanceof(InterfaceStatement);
+    });
+
+    it('parses classes with interfaces as fields', () => {
+        let { tokens } = Lexer.scan(`
+                interface Iface
+                   age as integer
+                end interface
+
+                class Klass
+                   myFace as Iface
+                end class
+            `);
+        let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        expectZeroDiagnostics(diagnostics);
+        expect(statements[0]).instanceof(InterfaceStatement);
+        expect(statements[1]).instanceof(ClassStatement);
+    });
+
+    it('parses interfaces with inheritance', () => {
+        let { tokens } = Lexer.scan(`
+                interface Basic
+                   varA as string
+                end interface
+
+                interface Advanced extends Basic
+                    varB as integer
+                end interface
+
+                interface Expert extends Advanced
+                    varC as float
+                end interface
+            `);
+        let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        expectZeroDiagnostics(diagnostics);
+        expect(statements[0]).instanceof(InterfaceStatement);
+        expect(statements[1]).instanceof(InterfaceStatement);
+        expect(statements[2]).instanceof(InterfaceStatement);
+    });
+
+    it('bad property does not invalidate next sibling method', () => {
+        let { tokens } = Lexer.scan(`
+                interface Person
+                     firstName as
+                     sub say(words as string)
+                end interface
+            `);
+        let { statements } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        let iFaceStmt = statements[0] as InterfaceStatement;
+        expect(iFaceStmt.methods[0]).to.exist;
+        expect(iFaceStmt.methods[0].name.text).to.equal('say');
+    });
+
+    it('catches interface without name', () => {
+        let { tokens } = Lexer.scan(`
+                interface
+                end interface
+            `);
+        let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        expectDiagnostics(diagnostics, [DiagnosticMessages.expectedIdentifierAfterKeyword('interface')]);
+        expect(statements[0]).instanceof(InterfaceStatement);
+    });
+
+    it('catches malformed interface', () => {
+        let { tokens } = Lexer.scan(`
+                interface Person
+            `);
+        let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        expectDiagnostics(diagnostics, [DiagnosticMessages.couldNotFindMatchingEndKeyword('interface')]);
+        expect(statements[0]).instanceof(InterfaceStatement);
+    });
+
+    it('parses multiple interfaces in a row', () => {
+        let { tokens } = Lexer.scan(`
+                interface IFaceA
+                    a as string
+                    sub doA()
+                end interface
+                interface IFaceB
+                    b as string
+                    sub doB()
+                end interface
+            `);
+        let { statements, diagnostics } = Parser.parse(tokens, { mode: ParseMode.BrighterScript });
+        expectZeroDiagnostics(diagnostics);
+        expect(statements[0]).instanceof(InterfaceStatement);
+        expect(statements[1]).instanceof(InterfaceStatement);
+    });
+
+    describe('symbol table', () => {
+
+        it('does not add methods to parser symbol table', () => {
+            let parser = Parser.parse(`
+                interface Animal
+                    sub eat()
+                    sub sleep()
+                end class
+            `, { mode: ParseMode.BrighterScript });
+
+            expect(parser.symbolTable).to.exist;
+            expect(parser.symbolTable.getSymbolType('eat')).to.be.undefined;
+            expect(parser.symbolTable.getSymbolType('sleep')).to.be.undefined;
+        });
+
+
+        it('adds methods interface statement member symbol table after building', () => {
+            let parser = Parser.parse(`
+                interface Animal
+                    sub eat()
+                    sub sleep()
+                end interface
+            `, { mode: ParseMode.BrighterScript });
+            let ifaceStatement = parser.statements[0] as InterfaceStatement;
+            ifaceStatement.buildSymbolTable();
+            expect(ifaceStatement.memberTable).to.exist;
+            expect(isTypedFunctionType(ifaceStatement.memberTable.getSymbolType('eat'))).to.be.true;
+            expect(isTypedFunctionType(ifaceStatement.memberTable.getSymbolType('sleep'))).to.be.true;
+        });
+
+        it('adds fields to class statement member symbol table after building', () => {
+            let parser = Parser.parse(`
+                interface Animal
+                    teethCount as integer
+                    furType as string
+                    hasWings as boolean
+                end interface
+            `, { mode: ParseMode.BrighterScript });
+            let ifaceStatement = parser.statements[0] as InterfaceStatement;
+            ifaceStatement.buildSymbolTable();
+            expect(ifaceStatement.memberTable).to.exist;
+            expect(isIntegerType(ifaceStatement.memberTable.getSymbolType('teethCount'))).to.be.true;
+            expect(isStringType(ifaceStatement.memberTable.getSymbolType('furType'))).to.be.true;
+            expect(isBooleanType(ifaceStatement.memberTable.getSymbolType('hasWings'))).to.be.true;
+        });
+    });
 });