Repository: rokucommunity/brighterscript
Pull request #827 (2023-06-19 14:32:08)

Phase 2 of Typing Improvements
A second phase of type improvements for Brighterscript

# Tasks

- [x] BinaryExpressions and UnaryExpressions should be able to infer resultant types (eg. Integer * Integer => Integer, String + String => String, etc.) 

- [ ] Change Hover & Completion to use SymbolTable instead of variableDeclarations/callables
  This will complete the refactor to move away from using these lookup tables to unify how a symbol can be discovered/known. When this is done, the concept of VariableDeclarations can be removed from the code.

- [ ] Validate function calls for  argument type validity
    ### Function argument equality
    ```brighterscript
    sub main()
        printMessage(1)
        '            ~ Argument of type 'number' is not assignable to parameter of type 'string'
    end sub
    function printMessage(message as string)
        print message
    end function
    ```
# Phase 3

- [ ] Validate assignment expressions for type validity
    ### AssignmentStatement type equality
    ```brighterscript
    sub main()
        name = "bob"
        name = 1
    end sub
    '      ~ type 'Integer' cannot be assigned to type 'String'
    ```

- [ ] Add `ComponentType` that are generated from XML or future Component creation. Members (and member types) could be inferred from the component's fields

- [ ] CreateObject() should infer correct type (using data scraped from Roku SceneGraph docs for built in types, or from known components)

- [ ] Update `ArrayType` to allow indexedGet, etc. to infer type. Allow code & validation like:
 ```brighterscript
   function sumArray(numbers as integer[]) as integer
        sum = 0
        for each num in numbers
            sum = sum + num ' num is known to be an integer
        end for
        return sum
    end function
    
    sub addName(nameList as string[])
         nameList.push(3.14) ' validation error --- nameList.push expects string
     end sub
 
    ```


Patch:
diff --git a/benchmarks/index.ts b/benchmarks/index.ts
index 341fa8a4..12c8d00a 100644
--- a/benchmarks/index.ts
+++ b/benchmarks/index.ts
@@ -209,7 +209,7 @@ let options = yargs
     })
     .option('config', {
         type: 'string',
-        description: 'add additional BsConfig settings as JSON - eg. \'{"enableTypeValidation":true}\'',
+        description: 'add additional BsConfig settings as JSON - eg. \'{"removeParameterTypes":true}\'',
         default: '{}'
     })
     .strict()
diff --git a/benchmarks/targets/validate.ts b/benchmarks/targets/validate.ts
index ca60f66e..163d9ece 100644
--- a/benchmarks/targets/validate.ts
+++ b/benchmarks/targets/validate.ts
@@ -10,7 +10,6 @@ module.exports = async (options: TargetOptions) => {
         cwd: projectPath,
         createPackage: false,
         copyToStaging: false,
-        enableTypeValidation: false,
         //disable diagnostic reporting (they still get collected)
         diagnosticFilters: ['**/*'],
         logLevel: 'error',
diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts
index 3db691f4..6419fd34 100644
--- a/src/DiagnosticMessages.ts
+++ b/src/DiagnosticMessages.ts
@@ -729,6 +729,11 @@ export let DiagnosticMessages = {
         message: `'${typeText}' cannot be used as a type`,
         code: 1140,
         severity: DiagnosticSeverity.Error
+    }),
+    argumentTypeMismatch: (actualTypeString: string, expectedTypeString: string) => ({
+        message: `Argument of type '${actualTypeString}' is not compatible with parameter of type '${expectedTypeString}'`,
+        code: 1141,
+        severity: DiagnosticSeverity.Error
     })
 };
 
diff --git a/src/Program.spec.ts b/src/Program.spec.ts
index a0d15d05..5b0d6efa 100644
--- a/src/Program.spec.ts
+++ b/src/Program.spec.ts
@@ -1616,7 +1616,7 @@ describe('Program', () => {
             program.setFile('source/main.brs', `
                 sub A()
                     'call with wrong param count
-                    B(1,2,3)
+                    B("one", "two")
 
                     'call unknown function
                     C()
diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts
index f4190631..e0813094 100644
--- a/src/Scope.spec.ts
+++ b/src/Scope.spec.ts
@@ -21,6 +21,8 @@ import { DynamicType } from './types/DynamicType';
 import { ObjectType } from './types/ObjectType';
 import { FloatType } from './types/FloatType';
 import { NamespaceType } from './types/NamespaceType';
+import { DoubleType } from './types/DoubleType';
+import { UnionType } from './types/UnionType';
 import { isFunctionStatement, isNamespaceStatement } from './astUtils/reflection';
 
 describe('Scope', () => {
@@ -816,7 +818,7 @@ describe('Scope', () => {
                     sayMyName = function(name as string)
                     end function
 
-                    sayMyName()
+                    sayMyName("John Doe")
                 end sub`
             );
             program.validate();
@@ -2594,6 +2596,131 @@ describe('Scope', () => {
             program.validate();
             expectZeroDiagnostics(program);
         });
+
+        describe('binary and unary expressions', () => {
+            it('should set symbols with correct types from binary expressions', () => {
+                let mainFile = program.setFile('source/main.bs', `
+                    sub process()
+                        s = "hello" + "world"
+                        exp = 2^3
+                        num = 3.14 + 3.14
+                        bool = true or false
+                        notEq = {} <> invalid
+                    end sub
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24));
+                const symbolTable = processFnScope.symbolTable;
+                const opts = { flags: SymbolTypeFlag.runtime };
+                expectTypeToBe(symbolTable.getSymbolType('s', opts), StringType);
+                expectTypeToBe(symbolTable.getSymbolType('exp', opts), IntegerType);
+                expectTypeToBe(symbolTable.getSymbolType('num', opts), FloatType);
+                expectTypeToBe(symbolTable.getSymbolType('bool', opts), BooleanType);
+                expectTypeToBe(symbolTable.getSymbolType('notEq', opts), BooleanType);
+            });
+
+            it('should set symbols with correct types from unary expressions', () => {
+                let mainFile = program.setFile('source/main.bs', `
+                    sub process(boolVal as boolean, intVal as integer)
+                        a = not boolVal
+                        b = not true
+                        c = not intVal
+                        d = not 3.14
+
+                        e = -34
+                        f = -3.14
+                        g = -intVal
+                        h = - (-f)
+                    end sub
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24));
+                const symbolTable = processFnScope.symbolTable;
+                const opts = { flags: SymbolTypeFlag.runtime };
+                expectTypeToBe(symbolTable.getSymbolType('a', opts), BooleanType);
+                expectTypeToBe(symbolTable.getSymbolType('b', opts), BooleanType);
+                expectTypeToBe(symbolTable.getSymbolType('c', opts), IntegerType);
+                expectTypeToBe(symbolTable.getSymbolType('d', opts), IntegerType);
+
+                expectTypeToBe(symbolTable.getSymbolType('e', opts), IntegerType);
+                expectTypeToBe(symbolTable.getSymbolType('f', opts), FloatType);
+                expectTypeToBe(symbolTable.getSymbolType('g', opts), IntegerType);
+                expectTypeToBe(symbolTable.getSymbolType('h', opts), FloatType);
+            });
+        });
+
+        describe('assignment expressions', () => {
+            it('should set correct type on simple equals', () => {
+                let mainFile = program.setFile('source/main.bs', `
+                    sub process(intVal as integer, dblVal as double, strVal as string)
+                        a = intVal
+                        b = dblVal
+                        c = strVal
+                        d = {}
+                        f = m.foo
+                        e = true
+                    end sub
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24));
+                const symbolTable = processFnScope.symbolTable;
+                const opts = { flags: SymbolTypeFlag.runtime };
+                expectTypeToBe(symbolTable.getSymbolType('a', opts), IntegerType);
+                expectTypeToBe(symbolTable.getSymbolType('b', opts), DoubleType);
+                expectTypeToBe(symbolTable.getSymbolType('c', opts), StringType);
+                expectTypeToBe(symbolTable.getSymbolType('d', opts), DynamicType);
+                expectTypeToBe(symbolTable.getSymbolType('f', opts), DynamicType);
+                expectTypeToBe(symbolTable.getSymbolType('e', opts), BooleanType);
+            });
+
+            it('should set correct type on compound equals', () => {
+                let mainFile = program.setFile('source/main.bs', `
+                    sub process(intVal as integer, dblVal as double, strVal as string)
+                        a = intVal
+                        a += 4
+                        b = dblVal
+                        b *= 23
+                        c = strVal
+                        c += "hello world"
+                        d = 3.14
+                        d \= 3 ' integer division -> d could be either a float or int
+                    end sub
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24));
+                const symbolTable = processFnScope.symbolTable;
+                const opts = { flags: SymbolTypeFlag.runtime };
+                expectTypeToBe(symbolTable.getSymbolType('a', opts), IntegerType);
+                expectTypeToBe(symbolTable.getSymbolType('b', opts), DoubleType);
+                expectTypeToBe(symbolTable.getSymbolType('c', opts), StringType);
+                const dType = symbolTable.getSymbolType('d', opts);
+                expectTypeToBe(dType, UnionType);
+                expect((dType as UnionType).types).to.include(FloatType.instance);
+                expect((dType as UnionType).types).to.include(IntegerType.instance);
+            });
+
+            it('should work for a multiple binary expressions', () => {
+                let mainFile = program.setFile('source/main.bs', `
+                    function process(intVal as integer)
+                        x = (intVal * 2) + 1 + 3^8 + intVal + (3 - 9)  ' should be int
+                        result = 3.5 ' float
+                        result *= (x + 1.123 * 3) ' -> float
+                        return result
+                    end function
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24));
+                const symbolTable = processFnScope.symbolTable;
+                const opts = { flags: SymbolTypeFlag.runtime };
+                expectTypeToBe(symbolTable.getSymbolType('x', opts), IntegerType);
+                expectTypeToBe(symbolTable.getSymbolType('result', opts), FloatType);
+            });
+        });
     });
 
     describe('unlinkSymbolTable', () => {
diff --git a/src/Scope.ts b/src/Scope.ts
index 4653b23c..5b7aeff6 100644
--- a/src/Scope.ts
+++ b/src/Scope.ts
@@ -752,7 +752,6 @@ export class Scope {
 
         //do many per-file checks
         this.enumerateBrsFiles((file) => {
-            this.diagnosticDetectFunctionCallsWithWrongParamCount(file, callableContainerMap);
             this.diagnosticDetectShadowedLocalVars(file, callableContainerMap);
             this.diagnosticDetectFunctionCollisions(file);
             this.detectVariableNamespaceCollisions(file);
@@ -961,43 +960,6 @@ export class Scope {
         this.diagnostics.push(...validator.diagnostics);
     }
 
-    /**
-     * Detect calls to functions with the incorrect number of parameters
-     */
-    private diagnosticDetectFunctionCallsWithWrongParamCount(file: BscFile, callableContainersByLowerName: CallableContainerMap) {
-        //validate all function calls
-        for (let expCall of file.functionCalls) {
-            let callableContainersWithThisName = callableContainersByLowerName.get(expCall.name.toLowerCase());
-
-            //use the first item from callablesByLowerName, because if there are more, that's a separate error
-            let knownCallableContainer = callableContainersWithThisName ? callableContainersWithThisName[0] : undefined;
-
-            if (knownCallableContainer) {
-                //get min/max parameter count for callable
-                let minParams = 0;
-                let maxParams = 0;
-                for (let param of knownCallableContainer.callable.params) {
-                    maxParams++;
-                    //optional parameters must come last, so we can assume that minParams won't increase once we hit
-                    //the first isOptional
-                    if (param.isOptional !== true) {
-                        minParams++;
-                    }
-                }
-                let expCallArgCount = expCall.args.length;
-                if (expCall.args.length > maxParams || expCall.args.length < minParams) {
-                    let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`;
-                    this.diagnostics.push({
-                        ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount),
-                        range: expCall.nameRange,
-                        //TODO detect end of expression call
-                        file: file
-                    });
-                }
-            }
-        }
-    }
-
     /**
      * Detect local variables (function scope) that have the same name as scope calls
      */
diff --git a/src/bscPlugin/hover/HoverProcessor.spec.ts b/src/bscPlugin/hover/HoverProcessor.spec.ts
index 925737e7..b0913df1 100644
--- a/src/bscPlugin/hover/HoverProcessor.spec.ts
+++ b/src/bscPlugin/hover/HoverProcessor.spec.ts
@@ -82,7 +82,7 @@ describe('HoverProcessor', () => {
             expect(hover).to.exist;
 
             expect(hover.range).to.eql(util.createRange(1, 25, 1, 29));
-            expect(hover.contents).to.eql(fence('function Main(count? as integer) as dynamic'));
+            expect(hover.contents).to.eql([fence('function Main(count? as integer) as dynamic')]);
         });
 
         it('finds variable function hover in same scope', () => {
@@ -94,11 +94,11 @@ describe('HoverProcessor', () => {
                     sayMyName()
                 end sub
             `);
-
+            program.validate();
             let hover = program.getHover(file.srcPath, util.createPosition(5, 24))[0];
 
             expect(hover.range).to.eql(util.createRange(5, 20, 5, 29));
-            expect(hover.contents).to.eql(fence('sub sayMyName(name as string) as void'));
+            expect(hover.contents).to.eql([fence('sub sayMyName(name as string) as void')]);
         });
 
         it('finds function hover in file scope', () => {
@@ -108,22 +108,17 @@ describe('HoverProcessor', () => {
                 end sub
 
                 sub sayMyName()
-
                 end sub
             `);
-
+            program.validate();
+            //sayM|yName()
             let hover = program.getHover(file.srcPath, util.createPosition(2, 25))[0];
 
             expect(hover.range).to.eql(util.createRange(2, 20, 2, 29));
-            expect(hover.contents).to.eql(fence('sub sayMyName() as void'));
+            expect(hover.contents).to.eql([fence('sub sayMyName() as void')]);
         });
 
         it('finds function hover in scope', () => {
-            let rootDir = process.cwd();
-            program = new Program({
-                rootDir: rootDir
-            });
-
             let mainFile = program.setFile('source/main.brs', `
                 sub Main()
                     sayMyName()
@@ -138,8 +133,8 @@ describe('HoverProcessor', () => {
             program.validate();
 
             let hover = program.getHover(mainFile.srcPath, util.createPosition(2, 25))[0];
-            expect(hover?.range).to.eql(util.createRange(2, 20, 2, 29));
-            expect(hover?.contents).to.eql(fence('sub sayMyName(name as string) as void'));
+            expect(hover.range).to.eql(util.createRange(2, 20, 2, 29));
+            expect(hover.contents).to.eql([fence('sub sayMyName(name as string) as void')]);
         });
 
         it('finds top-level constant value', () => {
@@ -153,7 +148,7 @@ describe('HoverProcessor', () => {
             // print SOM|E_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(2, 29))[0];
             expect(hover?.range).to.eql(util.createRange(2, 26, 2, 36));
-            expect(hover?.contents).to.eql(fence('const SOME_VALUE = true'));
+            expect(hover?.contents).to.eql([fence('const SOME_VALUE = true')]);
         });
 
         it('finds top-level constant in assignment expression', () => {
@@ -168,7 +163,7 @@ describe('HoverProcessor', () => {
             // value += SOME|_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(3, 33))[0];
             expect(hover?.range).to.eql(util.createRange(3, 29, 3, 39));
-            expect(hover?.contents).to.eql(fence('const SOME_VALUE = "value"'));
+            expect(hover?.contents).to.eql([fence('const SOME_VALUE = "value"')]);
         });
 
         it('finds namespaced constant in assignment expression', () => {
@@ -185,7 +180,7 @@ describe('HoverProcessor', () => {
             // value += SOME|_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(3, 47))[0];
             expect(hover?.range).to.eql(util.createRange(3, 43, 3, 53));
-            expect(hover?.contents).to.eql(fence('const someNamespace.SOME_VALUE = "value"'));
+            expect(hover?.contents).to.eql([fence('const someNamespace.SOME_VALUE = "value"')]);
         });
 
         it('finds namespaced constant value', () => {
@@ -201,7 +196,7 @@ describe('HoverProcessor', () => {
             // print name.SOM|E_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(2, 34))[0];
             expect(hover?.range).to.eql(util.createRange(2, 31, 2, 41));
-            expect(hover?.contents).to.eql(fence('const name.SOME_VALUE = true'));
+            expect(hover?.contents).to.eql([fence('const name.SOME_VALUE = true')]);
         });
 
         it('finds deep namespaced constant value', () => {
@@ -217,7 +212,7 @@ describe('HoverProcessor', () => {
             // print name.sp.a.c.e.SOM|E_VALUE
             let hover = program.getHover('source/main.bs', util.createPosition(2, 43))[0];
             expect(hover?.range).to.eql(util.createRange(2, 40, 2, 50));
-            expect(hover?.contents).to.eql(fence('const name.sp.a.c.e.SOME_VALUE = true'));
+            expect(hover?.contents).to.eql([fence('const name.sp.a.c.e.SOME_VALUE = true')]);
         });
 
         it('finds namespaced class types', () => {
@@ -242,16 +237,14 @@ describe('HoverProcessor', () => {
             // run|Noop(myKlass)
             let hover = program.getHover('source/main.bs', util.createPosition(3, 24))[0];
             expect(hover?.range).to.eql(util.createRange(3, 20, 3, 27));
-            expect(hover?.contents).to.eql(fence('sub runNoop(myKlass as name.Klass) as void'));
+            expect(hover?.contents).to.eql([fence('sub runNoop(myKlass as name.Klass) as void')]);
             // myKl|ass.noop()
             hover = program.getHover('source/main.bs', util.createPosition(7, 25))[0];
             expect(hover?.range).to.eql(util.createRange(7, 20, 7, 27));
-            expect(hover?.contents).to.eql(fence('myKlass as name.Klass'));
+            expect(hover?.contents).to.eql([fence('myKlass as name.Klass')]);
             //  sub no|op()
             hover = program.getHover('source/main.bs', util.createPosition(12, 31))[0];
-            // Unfortunately, we can't get hover details on class members yet
-            // TODO: Add hover ability on class members
-            expect(hover).to.be.undefined;
+            expect(hover?.contents).to.eql([fence('sub name.Klass.noop() as void')]);
         });
 
         it('finds types properly', () => {
@@ -266,7 +259,7 @@ describe('HoverProcessor', () => {
             // a|ge as integer
             let hover = program.getHover('source/main.bs', util.createPosition(4, 29))[0];
             expect(hover?.range).to.eql(util.createRange(4, 27, 4, 30));
-            expect(hover?.contents).to.eql(fence('age as integer'));
+            expect(hover?.contents).to.eql([fence('age as integer')]);
             // age as int|eger
             hover = program.getHover('source/main.bs', util.createPosition(4, 39))[0];
             // no hover on base types
@@ -274,7 +267,7 @@ describe('HoverProcessor', () => {
             // n|ame as string
             hover = program.getHover('source/main.bs', util.createPosition(4, 46))[0];
             expect(hover?.range).to.eql(util.createRange(4, 43, 4, 47));
-            expect(hover?.contents).to.eql(fence('name as string'));
+            expect(hover?.contents).to.eql([fence('name as string')]);
             // name as st|ring
             hover = program.getHover('source/main.bs', util.createPosition(4, 54))[0];
             // no hover on base types
@@ -282,11 +275,10 @@ describe('HoverProcessor', () => {
             // gu|y as Person
             hover = program.getHover('source/main.bs', util.createPosition(4, 60))[0];
             expect(hover?.range).to.eql(util.createRange(4, 59, 4, 62));
-            expect(hover?.contents).to.eql(fence('guy as Person'));
+            expect(hover?.contents).to.eql([fence('guy as Person')]);
             // guy as Pe|rson
             hover = program.getHover('source/main.bs', util.createPosition(4, 69))[0];
-            //TODO: Add hover on custom types (classes, interfaces, enums, etc.)
-            expect(hover).to.be.undefined;
+            expect(hover?.contents).to.eql([fence('class Person')]);
         });
 
         it('finds types from assignments defined in different file', () => {
@@ -316,11 +308,52 @@ describe('HoverProcessor', () => {
             //th|ing = new MyKlass()
             let hover = program.getHover('source/main.bs', util.createPosition(2, 24))[0];
             expect(hover?.range).to.eql(util.createRange(2, 20, 2, 25));
-            expect(hover?.contents).to.eql(fence('thing as MyKlass'));
+            expect(hover?.contents).to.eql([fence('thing as MyKlass')]);
             //print some|Val
             hover = program.getHover('source/main.bs', util.createPosition(5, 31))[0];
             expect(hover?.range).to.eql(util.createRange(5, 26, 5, 33));
-            expect(hover?.contents).to.eql(fence('someVal as string'));
+            expect(hover?.contents).to.eql([fence('someVal as string')]);
+        });
+
+        it('hovers of functions include comments', () => {
+            program.setFile(`source/main.bs`, `
+                sub main()
+                    thing = new MyKlass()
+                    useKlass(thing)
+                end sub
+
+                ' Prints a MyKlass.name
+                sub useKlass(thing as MyKlass)
+                    print thing.getName()
+                end sub
+
+                ' A sample class
+                class MyKlass
+                    name as string
+
+                    ' Gets the name of this thing
+                    function getName() as string
+                        return m.name
+                    end function
+
+                    ' Wraps another function
+                    function getNameWrap() as string
+                        return m.getName()
+                    end function
+                end class
+            `);
+            program.validate();
+            let commentSep = `\n***\n`;
+            //th|ing = new MyKlass()
+            let hover = program.getHover('source/main.bs', util.createPosition(2, 24))[0];
+            expect(hover?.contents).to.eql([fence('thing as MyKlass')]);
+            //use|Klass(thing)
+            hover = program.getHover('source/main.bs', util.createPosition(3, 24))[0];
+            expect(hover?.contents).to.eql([`${fence('sub useKlass(thing as MyKlass) as void')}${commentSep} Prints a MyKlass.name`]);
+            //print thing.getN|ame()
+            hover = program.getHover('source/main.bs', util.createPosition(8, 37))[0];
+            // TODO: Add comments for class methods/properties
+            expect(hover?.contents).to.eql([`${fence('function MyKlass.getName() as string')}`]);
         });
     });
 });
diff --git a/src/bscPlugin/hover/HoverProcessor.ts b/src/bscPlugin/hover/HoverProcessor.ts
index 38a4968e..485fcc45 100644
--- a/src/bscPlugin/hover/HoverProcessor.ts
+++ b/src/bscPlugin/hover/HoverProcessor.ts
@@ -1,13 +1,23 @@
 import { SourceNode } from 'source-map';
-import { isBrsFile, isFunctionType, isTypeExpression, isXmlFile } from '../../astUtils/reflection';
+import { isBrsFile, isClassType, isFunctionType, isInterfaceType, isNewExpression, isTypeExpression, isXmlFile } from '../../astUtils/reflection';
 import type { BrsFile } from '../../files/BrsFile';
 import type { XmlFile } from '../../files/XmlFile';
-import type { Hover, ProvideHoverEvent } from '../../interfaces';
+import type { Hover, ProvideHoverEvent, TypeChainEntry } from '../../interfaces';
 import type { Token } from '../../lexer/Token';
 import { TokenKind } from '../../lexer/TokenKind';
 import { BrsTranspileState } from '../../parser/BrsTranspileState';
 import { ParseMode } from '../../parser/Parser';
 import util from '../../util';
+import { SymbolTypeFlag } from '../../SymbolTable';
+import type { Expression } from '../../parser/AstNode';
+import type { Scope } from '../../Scope';
+import type { FunctionScope } from '../../FunctionScope';
+import type { FunctionType } from '../../types/FunctionType';
+import type { ClassType } from '../../types/ClassType';
+import type { InterfaceType } from '../../types/InterfaceType';
+
+
+const fence = (code: string) => util.mdFence(code, 'brightscript');
 
 export class HoverProcessor {
     public constructor(
@@ -40,97 +50,131 @@ export class HoverProcessor {
         return parts.join('\n');
     }
 
-    private getBrsFileHover(file: BrsFile): Hover {
-        const scope = this.event.scopes[0];
-        try {
-            scope.linkSymbolTable();
-            const fence = (code: string) => util.mdFence(code, 'brightscript');
-            //get the token at the position
-            let token = file.getTokenAt(this.event.position);
-
-            let hoverTokenTypes = [
-                TokenKind.Identifier,
-                TokenKind.Function,
-                TokenKind.EndFunction,
-                TokenKind.Sub,
-                TokenKind.EndSub
-            ];
-
-            //throw out invalid tokens and the wrong kind of tokens
-            if (!token || !hoverTokenTypes.includes(token.kind)) {
-                return null;
-            }
+    private isValidTokenForHover(token: Token) {
+        let hoverTokenTypes = [
+            TokenKind.Identifier,
+            TokenKind.Function,
+            TokenKind.EndFunction,
+            TokenKind.Sub,
+            TokenKind.EndSub
+        ];
+
+        //throw out invalid tokens and the wrong kind of tokens
+        return (token && hoverTokenTypes.includes(token.kind));
+    }
 
-            const expression = file.getClosestExpression(this.event.position);
-            if (expression) {
-                let containingNamespace = file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript);
-                const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.');
-
-                //find a constant with this name
-                const constant = scope?.getConstFileLink(fullName, containingNamespace);
-                if (constant) {
-                    const constantValue = new SourceNode(null, null, null, constant.item.value.transpile(new BrsTranspileState(file))).toString();
-                    return {
-                        contents: this.buildContentsWithDocs(fence(`const ${constant.item.fullName} = ${constantValue}`), constant.item.tokens.const),
-                        range: token.range
-                    };
-                }
+    private getConstHover(token: Token, file: BrsFile, scope: Scope, expression: Expression) {
+        let containingNamespace = file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript);
+        const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.');
+
+        //find a constant with this name
+        const constant = scope?.getConstFileLink(fullName, containingNamespace);
+        if (constant) {
+            const constantValue = new SourceNode(null, null, null, constant.item.value.transpile(new BrsTranspileState(file))).toString();
+            return this.buildContentsWithDocs(fence(`const ${constant.item.fullName} = ${constantValue}`), constant.item.tokens.const);
+        }
+    }
+
+    private getLabelHover(token: Token, functionScope: FunctionScope) {
+        let lowerTokenText = token.text.toLowerCase();
+        for (const labelStatement of functionScope.labelStatements) {
+            if (labelStatement.name.toLocaleLowerCase() === lowerTokenText) {
+                return fence(`${labelStatement.name}: label`);
             }
+        }
+    }
+
+    private getFunctionTypeHover(token: Token, expression: Expression, expressionType: FunctionType, scope: Scope) {
+        const lowerTokenText = token.text.toLowerCase();
+        let result = fence(expressionType.toString());
+
+        // only look for callables when they aren't inside a type expression
+        // this was a problem for the function `string()` as it is a type AND a function https://developer.roku.com/en-ca/docs/references/brightscript/language/global-string-functions.md#stringn-as-integer-str-as-string--as-string
+        let callable = scope.getCallableByName(lowerTokenText);
+        if (callable) {
+            // We can find the start token of the function definition, use it to add docs.
+            // TODO: Add comment lookups for class methods!
+            result = this.buildContentsWithDocs(result, callable.functionStatement?.func?.functionType);
+        }
+        return result;
+    }
+
+    private getCustomTypeHover(expressionType: ClassType | InterfaceType, scope: Scope) {
+        let declarationText = '';
+        let exprTypeString = expressionType.toString();
+        let firstToken: Token;
+        if (isClassType(expressionType)) {
+            let entityStmt = scope.getClass(exprTypeString.toLowerCase());
+            firstToken = entityStmt?.classKeyword;
+            declarationText = firstToken?.text ?? TokenKind.Class;
 
-            let lowerTokenText = token.text.toLowerCase();
+        } else if (isInterfaceType(expressionType)) {
+            let entityStmt = scope.getInterface(exprTypeString.toLowerCase());
+            firstToken = entityStmt.tokens.interface;
+            declarationText = firstToken?.text ?? TokenKind.Interface;
 
-            //look through local variables first
-            {
+        }
+        let result = fence(`${declarationText} ${exprTypeString}`);
+        if (firstToken) {
+            // We can find the start token of the declaration, use it to add docs.
+            result = this.buildContentsWithDocs(result, firstToken);
+        }
+        return result;
+    }
+
+    private getBrsFileHover(file: BrsFile): Hover {
+        //get the token at the position
+        let token = file.getTokenAt(this.event.position);
+
+        if (!this.isValidTokenForHover(token)) {
+            return null;
+        }
+
+        const hoverContents: string[] = [];
+        for (let scope of this.event.scopes) {
+            try {
+                scope.linkSymbolTable();
+
+                const expression = file.getClosestExpression(this.event.position);
+                const constHover = this.getConstHover(token, file, scope, expression);
+                if (constHover) {
+                    hoverContents.push(constHover);
+                    continue;
+                }
                 //get the function scope for this position (if exists)
                 let functionScope = file.getFunctionScopeAtPosition(this.event.position);
                 if (functionScope) {
-                    //find any variable with this name
-                    for (const varDeclaration of functionScope.variableDeclarations) {
-                        //we found a variable declaration with this token text!
-                        if (varDeclaration.name.toLowerCase() === lowerTokenText) {
-                            let typeText: string;
-                            const varDeclarationType = varDeclaration.getType();
-                            if (isFunctionType(varDeclarationType)) {
-                                varDeclarationType.setName(varDeclaration.name);
-                                typeText = varDeclarationType.toString();
-                            } else {
-                                typeText = `${varDeclaration.name} as ${varDeclarationType.toString()}`;
-                            }
-                            return {
-                                range: token.range,
-                                //append the variable name to the front for scope
-                                contents: fence(typeText)
-                            };
-                        }
-                    }
-                    for (const labelStatement of functionScope.labelStatements) {
-                        if (labelStatement.name.toLocaleLowerCase() === lowerTokenText) {
-                            return {
-                                range: token.range,
-                                contents: fence(`${labelStatement.name}: label`)
-                            };
-                        }
+                    const labelHover = this.getLabelHover(token, functionScope);
+                    if (labelHover) {
+                        hoverContents.push(labelHover);
+                        continue;
                     }
                 }
-            }
-
-            //look through all callables in relevant scopes
-            if (!expression?.findAncestor(isTypeExpression)) {
-                // only look for callables when they aren't inside a type expression
-                // this was a problem for the function `string()` as it is a type AND a function https://developer.roku.com/en-ca/docs/references/brightscript/language/global-string-functions.md#stringn-as-integer-str-as-string--as-string
-                for (let scope of this.event.scopes) {
-                    let callable = scope.getCallableByName(lowerTokenText);
-                    if (callable) {
-                        return {
-                            range: token.range,
-                            contents: this.buildContentsWithDocs(fence(callable.type.toString()), callable.functionStatement?.func?.functionType)
-                        };
-                    }
+                const isInTypeExpression = expression?.findAncestor(isTypeExpression);
+                const typeFlag = isInTypeExpression ? SymbolTypeFlag.typetime : SymbolTypeFlag.runtime;
+                const typeChain: TypeChainEntry[] = [];
+                const exprType = expression.getType({ flags: typeFlag, typeChain: typeChain });
+                const processedTypeChain = util.processTypeChain(typeChain);
+                const fullName = processedTypeChain.fullNameOfItem || token.text;
+                const useCustomTypeHover = isInTypeExpression || expression?.findAncestor(isNewExpression);
+                let hoverContent = fence(`${fullName} as ${exprType.toString()}`);
+                if (isFunctionType(exprType)) {
+                    exprType.setName(fullName);
+                    hoverContent = this.getFunctionTypeHover(token, expression, exprType, scope);
+                } else if (useCustomTypeHover && (isClassType(exprType) || isInterfaceType(exprType))) {
+                    hoverContent = this.getCustomTypeHover(exprType, scope);
                 }
+
+                hoverContents.push(hoverContent);
+
+            } finally {
+                scope?.unlinkSymbolTable();
             }
-        } finally {
-            scope?.unlinkSymbolTable();
         }
+        return {
+            range: token.range,
+            contents: hoverContents
+        };
     }
 
     /**
diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts
index e12e2a0a..90d3988d 100644
--- a/src/bscPlugin/validation/BrsFileValidator.ts
+++ b/src/bscPlugin/validation/BrsFileValidator.ts
@@ -36,8 +36,11 @@ export class BrsFileValidator {
         const visitor = createVisitor({
             MethodStatement: (node) => {
                 //add the `super` symbol to class methods
-                //Todo:  get the actual type of the parent class
-                node.func.body.symbolTable.addSymbol('super', undefined, DynamicType.instance, SymbolTypeFlag.runtime);
+                if (isClassStatement(node.parent) && node.parent.hasParentClass()) {
+                    //Todo:  get the actual type of the parent class
+                    // Maybe? const parentClassType = node.parent.parentClassName.getType({ flags: SymbolTypeFlag.typetime });
+                    node.func.body.symbolTable.addSymbol('super', undefined, DynamicType.instance, SymbolTypeFlag.runtime);
+                }
             },
             CallfuncExpression: (node) => {
                 if (node.args.length > 5) {
diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts
new file mode 100644
index 00000000..77769397
--- /dev/null
+++ b/src/bscPlugin/validation/ScopeValidator.spec.ts
@@ -0,0 +1,502 @@
+import * as sinonImport from 'sinon';
+import { DiagnosticMessages } from '../../DiagnosticMessages';
+import { Program } from '../../Program';
+import { expectDiagnostics, expectZeroDiagnostics } from '../../testHelpers.spec';
+import { expect } from 'chai';
+
+describe('ScopeValidator', () => {
+
+    let sinon = sinonImport.createSandbox();
+    let rootDir = process.cwd();
+    let program: Program;
+    beforeEach(() => {
+        program = new Program({
+            rootDir: rootDir
+        });
+        program.createSourceScope();
+    });
+    afterEach(() => {
+        sinon.restore();
+        program.dispose();
+    });
+
+    describe('mismatchArgumentCount', () => {
+        it('detects calling functions with too many arguments', () => {
+            program.setFile('source/file.brs', `
+                sub a()
+                end sub
+                sub b()
+                    a(1)
+                end sub
+            `);
+            program.validate();
+            expectDiagnostics(program, [
+                DiagnosticMessages.mismatchArgumentCount(0, 1).message
+            ]);
+        });
+
+        it('detects calling class constructors with too many arguments', () => {
+            program.setFile('source/main.bs', `
+                    function noop0()
+                    end function
+
+                    function noop1(p1)
+                    end function
+
+                    sub main()
+                       noop0(1)
+                       noop1(1,2)
+                       noop1()
+                    end sub
+                `);
+            program.validate();
+            expectDiagnostics(program, [
+                DiagnosticMessages.mismatchArgumentCount(0, 1),
+                DiagnosticMessages.mismatchArgumentCount(1, 2),
+                DiagnosticMessages.mismatchArgumentCount(1, 0)
+            ]);
+        });
+
+        it('detects calling functions with too few arguments', () => {
+            program.setFile('source/file.brs', `
+                    sub a(name)
+                    end sub
+                    sub b()
+                        a()
+                    end sub
+                `);
+            program.validate();
+            expectDiagnostics(program, [
+                DiagnosticMessages.mismatchArgumentCount(1, 0)
+            ]);
+        });
+
+        it('allows skipping optional parameter', () => {
+            program.setFile('source/file.brs', `
+                    sub a(name="Bob")
+                    end sub
+                    sub b()
+                        a()
+                    end sub
+                `);
+            program.validate();
+            //should have an error
+            expectZeroDiagnostics(program);
+        });
+
+        it('shows expected parameter range in error message', () => {
+            program.setFile('source/file.brs', `
+                    sub a(age, name="Bob")
+                    end sub
+                    sub b()
+                        a()
+                    end sub
+                `);
+            program.validate();
+            //should have an error
+            expectDiagnostics(program, [
+                DiagnosticMessages.mismatchArgumentCount('1-2', 0)
+            ]);
+        });
+
+        it('handles expressions as arguments to a function', () => {
+            program.setFile('source/file.brs', `
+                    sub a(age, name="Bob")
+                    end sub
+                    sub b()
+                        a("cat" + "dog" + "mouse")
+                    end sub
+                `);
+            program.validate();
+            expectZeroDiagnostics(program);
+        });
+
+        it('Catches extra arguments for expressions as arguments to a function', () => {
+            program.setFile('source/file.brs', `
+                    sub a(age)
+                    end sub
+                    sub b()
+                        a(m.lib.movies[0], 1)
+                    end sub
+                `);
+            program.validate();
+            //should have an error
+            expectDiagnostics(program, [
+                DiagnosticMessages.mismatchArgumentCount(1, 2)
+            ]);
+        });
+    });
+
+    describe('argumentTypeMismatch', () => {
+        it('Catches argument type mismatches on function calls', () => {
+            program.setFile('source/file.brs', `
+                    sub a(age as integer)
+                    end sub
+                    sub b()
+                        a("hello")
+                    end sub
+                `);
+            program.validate();
+            //should have an error
+            expect(program.getDiagnostics().map(x => x.message)).to.include(
+                DiagnosticMessages.argumentTypeMismatch('string', 'integer').message
+            );
+        });
+
+        it('Catches argument type mismatches on function calls for functions defined in another file', () => {
+            program.setFile('source/file.brs', `
+                    sub a(age as integer)
+                    end sub
+                `);
+            program.setFile('source/file2.brs', `
+                    sub b()
+                        a("hello")
+                        foo = "foo"
+                        a(foo)
+                    end sub
+                `);
+            program.validate();
+            //should have an error
+            expect(program.getDiagnostics().map(x => x.message)).to.include(
+                DiagnosticMessages.argumentTypeMismatch('string', 'integer').message
+            );
+        });
+
+        it('catches argument type mismatches on function calls within namespaces', () => {
+            program.setFile('source/file.bs', `
+                    namespace Name.Space
+                        sub a(param as integer)
+                            print param
+                        end sub
+
+                        sub b()
+                            a("hello")
+                            foo = "foo"
+                            a(foo)
+                        end sub
+                    end namespace
+                    `);
+            program.validate();
+            //should have an error
+            expect(program.getDiagnostics().map(x => x.message)).to.include(
+                DiagnosticMessages.argumentTypeMismatch('string', 'integer').message
+            );
+        });
+
+        it('catches argument type mismatches on function calls as arguments', () => {
+            program.setFile('source/file1.bs', `
+                        sub a(param as string)
+                            print param
+                        end sub
+
+                        function getNum() as integer
+                            return 1
+                        end function
+
+                        sub b()
+                            a(getNum())
+                        end sub
+                    `);
+            program.validate();
+            //should have an error
+            expect(program.getDiagnostics().map(x => x.message)).to.include(
+                DiagnosticMessages.argumentTypeMismatch('integer', 'string').message
+            );
+        });
+
+
+        it('catches argument type mismatches on function calls within namespaces across files', () => {
+            program.setFile('source/file1.bs', `
+                    namespace Name.Space
+                        function getNum() as integer
+                            return 1
+                        end function
+
+                        function getStr() as string
+                            return "hello"
+                        end function
+                    end namespace
+                    `);
+            program.setFile('source/file2.bs', `
+                    namespace Name.Space
+                        sub needsInt(param as integer)
+                            print param
+                        end sub
+
+                        sub someFunc()
+                            needsInt(getStr())
+                            needsInt(getNum())
+                        end sub
+                    end namespace
+                    `);
+            program.validate();
+            //should have an error
+            expect(program.getDiagnostics().length).to.equal(1);
+            expect(program.getDiagnostics().map(x => x.message)).to.include(
+                DiagnosticMessages.argumentTypeMismatch('string', 'integer').message
+            );
+        });
+
+        it('correctly validates correct parameters that are class members', () => {
+            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)
+                end sub
+
+                sub someFunc()
+                    holder = new PiHolder()
+                    takesFloat(holder.pi)
+                    takesFloat(holder.getPI())
+                end sub`);
+            program.validate();
+            //should have no error
+            expectZeroDiagnostics(program);
+        });
+
+        it('correctly validates wrong parameters that are class members', () => {
+            program.setFile('source/main.bs', `
+                class PiHolder
+                    pi = 3.14
+                    name = "hello"
+                    function getPi() as float
+                        return m.pi
+                    end function
+                end class
+
+                sub takesFloat(fl as float)
+                end sub
+
+                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(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
+            expectZeroDiagnostics(program);
+        });
+
+        it('correctly validates wrong parameters that are interface members', () => {
+            program.setFile('source/main.bs', `
+                    interface IPerson
+                        isAlive as boolean
+                        function getAddress() as string
+                    end interface
+
+                    sub takesFloat(fl as float)
+                    end sub
+
+                    sub someFunc(person as IPerson)
+                        takesFloat(person.isAlive)
+                        takesFloat(person.getAddress())
+                    end sub
+                `);
+            program.validate();
+            //should have 2 errors: person.name is string (not float) and person.getAddress() is object (not float)
+            expectDiagnostics(program, [
+                DiagnosticMessages.argumentTypeMismatch('boolean', 'float').message,
+                DiagnosticMessages.argumentTypeMismatch('string', 'float').message
+            ]);
+        });
+
+        it('`as object` param allows all types', () => {
+            program.setFile('source/main.bs', `
+                    sub takesObject(obj as Object)
+                    end sub
+
+                    sub main()
+                        takesObject(true)
+                        takesObject(1)
+                        takesObject(1.2)
+                        takesObject(1.2#)
+                        takesObject("text")
+                        takesObject({})
+                        takesObject([])
+                    end sub
+                `);
+            program.validate();
+            expectZeroDiagnostics(program);
+        });
+
+        it('allows conversions for arguments', () => {
+            program.setFile('source/main.bs', `
+                sub takesFloat(fl as float)
+                end sub
+
+                sub someFunc()
+                    takesFloat(1)
+                end sub`);
+            program.validate();
+            //should have no error
+            expectZeroDiagnostics(program);
+        });
+
+        it('allows subclasses as arguments', () => {
+            program.setFile('source/main.bs', `
+
+                class Animal
+                end class
+
+                class Dog extends Animal
+                end class
+
+                class Retriever extends Dog
+                end class
+
+                class Lab extends Retriever
+                end class
+
+                sub takesAnimal(thing as Animal)
+                end sub
+
+                sub someFunc()
+                    fido = new Lab()
+                    takesAnimal(fido)
+                end sub`);
+            program.validate();
+            //should have no error
+            expectZeroDiagnostics(program);
+        });
+
+        it('allows subclasses from namespaces as arguments', () => {
+            program.setFile('source/main.bs', `
+
+                class Outside
+                end class
+
+                class ChildOutExtendsInside extends NS.Inside
+                end class
+
+                namespace NS
+                    class Inside
+                    end class
+
+                    class ChildInExtendsOutside extends Outside
+                    end class
+
+                    class ChildInExtendsInside extends Inside
+                        sub methodTakesInside(i as Inside)
+                        end sub
+                    end class
+
+                    sub takesInside(klass as Inside)
+                    end sub
+
+                    sub testFuncInNamespace()
+                        takesOutside(new Outside())
+                        takesOutside(new NS.ChildInExtendsOutside())
+
+                        ' These call NS.takesInside
+                        takesInside(new NS.Inside())
+                        takesInside(new Inside())
+                        takesInside(new NS.ChildInExtendsInside())
+                        takesInside(new ChildInExtendsInside())
+                        takesInside(new ChildOutExtendsInside())
+
+                        child = new ChildInExtendsInside()
+                        child.methodTakesInside(new Inside())
+                        child.methodTakesInside(new ChildInExtendsInside())
+                        child.methodTakesInside(new ChildOutExtendsInside())
+                    end sub
+
+                end namespace
+
+                sub takesOutside(klass as Outside)
+                end sub
+
+                sub takesInside(klass as NS.Inside)
+                end sub
+
+                sub testFunc()
+                    takesOutside(new Outside())
+                    takesOutside(new NS.ChildInExtendsOutside())
+
+                    takesInside(new NS.Inside())
+                    takesInside(new NS.ChildInExtendsInside())
+                    takesInside(new ChildOutExtendsInside())
+
+                    NS.takesInside(new NS.Inside())
+                    NS.takesInside(new NS.ChildInExtendsInside())
+                    NS.takesInside(new ChildOutExtendsInside())
+
+                    child = new NS.ChildInExtendsInside()
+                    child.methodTakesInside(new NS.Inside())
+                    child.methodTakesInside(new NS.ChildInExtendsInside())
+                    child.methodTakesInside(new ChildOutExtendsInside())
+                end sub`);
+            program.validate();
+            //should have no error
+            expectZeroDiagnostics(program);
+        });
+
+        it('respects union types', () => {
+            program.setFile('source/main.bs', `
+                sub takesStringOrKlass(p as string or Klass)
+                end sub
+
+                class Klass
+                end class
+
+                sub someFunc()
+                    myKlass = new Klass()
+                    takesStringOrKlass("test")
+                    takesStringOrKlass(myKlass)
+                    takesStringOrKlass(1)
+                end sub`);
+            program.validate();
+            //should have error when passed an integer
+            expect(program.getDiagnostics().length).to.equal(1);
+            expectDiagnostics(program, [
+                DiagnosticMessages.argumentTypeMismatch('integer', 'string or Klass').message
+            ]);
+        });
+
+
+        it('validates functions assigned to variables', () => {
+            program.setFile('source/main.bs', `
+                sub someFunc()
+                    myFunc = function(i as integer, s as string)
+                        print i+1
+                        print s.len()
+                    end function
+                    myFunc("hello", 2)
+                end sub`);
+            program.validate();
+            //should have error when passed incorrect types
+            expectDiagnostics(program, [
+                DiagnosticMessages.argumentTypeMismatch('string', 'integer').message,
+                DiagnosticMessages.argumentTypeMismatch('integer', 'string').message
+            ]);
+        });
+    });
+});
diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts
index 7989091f..dafbee04 100644
--- a/src/bscPlugin/validation/ScopeValidator.ts
+++ b/src/bscPlugin/validation/ScopeValidator.ts
@@ -1,5 +1,5 @@
 import { URI } from 'vscode-uri';
-import { isBinaryExpression, isBrsFile, isLiteralExpression, isNamespaceStatement, isTypeExpression, isXmlScope } from '../../astUtils/reflection';
+import { isBinaryExpression, isBrsFile, isFunctionType, isLiteralExpression, isNamespaceStatement, isTypeExpression, isXmlScope } from '../../astUtils/reflection';
 import { Cache } from '../../Cache';
 import { DiagnosticMessages } from '../../DiagnosticMessages';
 import type { BrsFile } from '../../files/BrsFile';
@@ -52,6 +52,7 @@ export class ScopeValidator {
             if (isBrsFile(file)) {
                 this.iterateFileExpressions(file);
                 this.validateCreateObjectCalls(file);
+                this.validateFunctionCalls(file);
             }
         });
     }
@@ -147,14 +148,13 @@ export class ScopeValidator {
                 const typeChainScan = util.processTypeChain(typeChain);
                 this.addMultiScopeDiagnostic({
                     file: file as BscFile,
-                    ...DiagnosticMessages.cannotFindName(typeChainScan.missingItemName, typeChainScan.fullNameOfMissingItem),
+                    ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem),
                     range: typeChainScan.range
                 });
                 //skip to the next expression
                 continue;
-            }
-
 
+            }
             const enumStatement = scope.getEnum(firstNamespacePartLower, info.enclosingNamespaceNameLower);
 
             //if this isn't a namespace, skip it
@@ -375,6 +375,63 @@ export class ScopeValidator {
         this.event.scope.addDiagnostics(diagnostics);
     }
 
+    /**
+     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
+     */
+    private validateFunctionCalls(file: BscFile) {
+        const diagnostics: BsDiagnostic[] = [];
+
+        //validate all function calls
+        for (let expCall of file.functionCalls) {
+            const funcType = expCall.expression?.callee?.getType({ flags: SymbolTypeFlag.runtime });
+            if (funcType?.isResolvable() && isFunctionType(funcType)) {
+                funcType.setName(expCall.name);
+
+                //get min/max parameter count for callable
+                let minParams = 0;
+                let maxParams = 0;
+                for (let param of funcType.params) {
+                    maxParams++;
+                    //optional parameters must come last, so we can assume that minParams won't increase once we hit
+                    //the first isOptional
+                    if (param.isOptional !== true) {
+                        minParams++;
+                    }
+                }
+                let expCallArgCount = expCall.args.length;
+                if (expCall.args.length > maxParams || expCall.args.length < minParams) {
+                    let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`;
+                    diagnostics.push({
+                        ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount),
+                        range: expCall.nameRange,
+                        //TODO detect end of expression call
+                        file: file
+                    });
+                }
+                let paramIndex = 0;
+                for (let arg of expCall.args) {
+                    const argType = arg.expression.getType({ flags: SymbolTypeFlag.runtime });
+
+                    const paramType = funcType.params[paramIndex]?.type;
+                    if (!paramType) {
+                        // unable to find a paramType -- maybe there are more args than params
+                        break;
+                    }
+                    if (!paramType?.isTypeCompatible(argType)) {
+                        diagnostics.push({
+                            ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString()),
+                            range: arg.expression.range,
+                            //TODO detect end of expression call
+                            file: file
+                        });
+                    }
+                    paramIndex++;
+                }
+            }
+        }
+        this.event.scope.addDiagnostics(diagnostics);
+    }
+
     /**
      * Adds a diagnostic to the first scope for this key. Prevents duplicate diagnostics
      * for diagnostics where scope isn't important. (i.e. CreateObject validations)
diff --git a/src/files/BrsFile.Class.spec.ts b/src/files/BrsFile.Class.spec.ts
index c7091047..eedfd9ab 100644
--- a/src/files/BrsFile.Class.spec.ts
+++ b/src/files/BrsFile.Class.spec.ts
@@ -473,6 +473,9 @@ describe('BrsFile BrighterScript classes', () => {
                 class Animal
                     sub new(name as string)
                     end sub
+
+                    sub DoSomething()
+                    end sub
                 end class
 
                 class Duck extends Animal
@@ -486,6 +489,8 @@ describe('BrsFile BrighterScript classes', () => {
                     instance = {}
                     instance.new = sub(name as string)
                     end sub
+                    instance.DoSomething = sub()
+                    end sub
                     return instance
                 end function
                 function Animal(name as string)
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index 9758b6a2..7a4172af 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -1812,322 +1812,6 @@ describe('BrsFile', () => {
         expect(mainFile.getDiagnostics()).to.be.lengthOf(0);
     });
 
-    describe('getHover', () => {
-        it('works for param types', () => {
-            let file = program.setFile('source/main.brs', `
-                sub DoSomething(name as string)
-                    name = 1
-                    sayMyName = function(name as string)
-                    end function
-                end sub
-            `);
-
-            //hover over the `name = 1` line
-            let hover = program.getHover(file.srcPath, Position.create(2, 24))[0];
-            expect(hover).to.exist;
-            expect(hover.range).to.eql(Range.create(2, 20, 2, 24));
-
-            //hover over the `name` parameter declaration
-            hover = program.getHover(file.srcPath, Position.create(1, 34))[0];
-            expect(hover).to.exist;
-            expect(hover.range).to.eql(Range.create(1, 32, 1, 36));
-        });
-
-        //ignore this for now...it's not a huge deal
-        it('does not match on keywords or data types', () => {
-            let file = program.setFile('source/main.brs', `
-                sub Main(name as string)
-                end sub
-                sub as()
-                end sub
-            `);
-            //hover over the `as`
-            expect(program.getHover(file.srcPath, Position.create(1, 31))).to.be.empty;
-            //hover over the `string`
-            expect(program.getHover(file.srcPath, Position.create(1, 36))).to.be.empty;
-        });
-
-        it('finds declared function', () => {
-            let file = program.setFile('source/main.brs', `
-                function Main(count = 1)
-                    firstName = "bob"
-                    age = 21
-                    shoeSize = 10
-                end function
-            `);
-
-            let hover = program.getHover(file.srcPath, Position.create(1, 28))[0];
-            expect(hover).to.exist;
-
-            expect(hover.range).to.eql(Range.create(1, 25, 1, 29));
-            expect(hover.contents).to.equal([
-                '```brightscript',
-                'function Main(count? as integer) as dynamic',
-                '```'
-            ].join('\n'));
-        });
-
-        it('finds declared namespace function', () => {
-            let file = program.setFile('source/main.brs', `
-            namespace mySpace
-                function Main(count = 1)
-                    firstName = "bob"
-                    age = 21
-                    shoeSize = 10
-                end function
-            end namespace
-            `);
-
-            let hover = program.getHover(file.srcPath, Position.create(2, 28))[0];
-            expect(hover).to.exist;
-
-            expect(hover.range).to.eql(Range.create(2, 25, 2, 29));
-            expect(hover.contents).to.equal([
-                '```brightscript',
-                'function Main(count? as integer) as dynamic',
-                '```'
-            ].join('\n'));
-        });
-
-        it('finds variable function hover in same scope', () => {
-            let file = program.setFile('source/main.brs', `
-                sub Main()
-                    sayMyName = sub(name as string)
-                    end sub
-
-                    sayMyName()
-                end sub
-            `);
-
-            let hover = program.getHover(file.srcPath, Position.create(5, 24))[0];
-
-            expect(hover.range).to.eql(Range.create(5, 20, 5, 29));
-            expect(hover.contents).to.equal([
-                '```brightscript',
-                'sub sayMyName(name as string) as void',
-                '```'
-            ].join('\n'));
-        });
-
-        it('does not crash when hovering on built-in functions', () => {
-            let file = program.setFile('source/main.brs', `
-                function doUcase(text)
-                    return ucase(text)
-                end function
-            `);
-
-            expect(
-                program.getHover(file.srcPath, Position.create(2, 30))[0].contents
-            ).to.equal([
-                '```brightscript',
-                'function UCase(s as string) as string',
-                '```'
-            ].join('\n'));
-        });
-
-        it('does not crash when hovering on object method call', () => {
-            let file = program.setFile('source/main.brs', `
-                function getInstr(url, text)
-                    return url.instr(text)
-                end function
-            `);
-
-            expect(
-                program.getHover(file.srcPath, Position.create(2, 35))[0].contents
-            ).to.equal([
-                '```brightscript',
-                //TODO this really shouldn't be returning the global function, but it does...so make sure it doesn't crash right now.
-                'function Instr(start as integer, text as string, substring as string) as integer',
-                '```'
-            ].join('\n'));
-        });
-
-        it('finds function hover in file scope', () => {
-            let file = program.setFile('source/main.brs', `
-                sub Main()
-                    sayMyName()
-                end sub
-
-                sub sayMyName()
-
-                end sub
-            `);
-
-            let hover = program.getHover(file.srcPath, Position.create(2, 25))[0];
-
-            expect(hover.range).to.eql(Range.create(2, 20, 2, 29));
-            expect(hover.contents).to.equal([
-                '```brightscript',
-                'sub sayMyName() as void',
-                '```'
-            ].join('\n'));
-        });
-
-        it('finds namespace function hover in file scope', () => {
-            let file = program.setFile('source/main.brs', `
-                namespace mySpace
-                sub Main()
-                    sayMyName()
-                end sub
-
-                sub sayMyName()
-
-                end sub
-                end namespace
-            `);
-
-            let hover = program.getHover(file.srcPath, Position.create(3, 25))[0];
-
-            expect(hover.range).to.eql(Range.create(3, 20, 3, 29));
-            expect(hover.contents).to.equal([
-                '```brightscript',
-                'sub sayMyName() as void',
-                '```'
-            ].join('\n'));
-        });
-
-        it('finds function hover in scope', () => {
-            let rootDir = process.cwd();
-            program = new Program({
-                rootDir: rootDir
-            });
-
-            let mainFile = program.setFile('source/main.brs', `
-                sub Main()
-                    sayMyName()
-                end sub
-            `);
-
-            program.setFile('source/lib.brs', `
-                sub sayMyName(name as string)
-
-                end sub
-            `);
-
-            let hover = program.getHover(mainFile.srcPath, Position.create(2, 25))[0];
-            expect(hover).to.exist;
-
-            expect(hover.range).to.eql(Range.create(2, 20, 2, 29));
-            expect(hover.contents).to.equal([
-                '```brightscript',
-                'sub sayMyName(name as string) as void',
-                '```'
-            ].join('\n'));
-        });
-
-        it('finds namespace function hover in scope', () => {
-            let rootDir = process.cwd();
-            program = new Program({
-                rootDir: rootDir
-            });
-
-            let mainFile = program.setFile('source/main.brs', `
-                sub Main()
-                    mySpace.sayMyName()
-                end sub
-            `);
-
-            program.setFile('source/lib.brs', `
-                namespace mySpace
-                    sub sayMyName(name as string)
-                    end sub
-                end namespace
-            `);
-
-            let hover = program.getHover(mainFile.srcPath, Position.create(2, 34))[0];
-            expect(hover).to.exist;
-
-            expect(hover.range).to.eql(Range.create(2, 28, 2, 37));
-            expect(hover.contents).to.equal([
-                '```brightscript',
-                'sub sayMyName(name as string) as void',
-                '```'
-            ].join('\n'));
-        });
-
-        it('includes markdown comments in hover.', () => {
-            let rootDir = process.cwd();
-            program = new Program({
-                rootDir: rootDir
-            });
-
-            const file = program.setFile('source/lib.brs', `
-                '
-                ' The main function
-                '
-                sub main()
-                    writeToLog("hello")
-                end sub
-
-                '
-                ' Prints a message to the log.
-                ' Works with *markdown* **content**
-                '
-                sub writeToLog(message as string)
-                    print message
-                end sub
-            `);
-
-            //hover over log("hello")
-            expect(
-                program.getHover(file.srcPath, Position.create(5, 22))[0].contents
-            ).to.equal([
-                '```brightscript',
-                'sub writeToLog(message as string) as void',
-                '```',
-                '***',
-                '',
-                ' Prints a message to the log.',
-                ' Works with *markdown* **content**',
-                ''
-            ].join('\n'));
-
-            //hover over sub ma|in()
-            expect(
-                trim(
-                    program.getHover(file.srcPath, Position.create(4, 22))[0].contents.toString()
-                )
-            ).to.equal(trim`
-                \`\`\`brightscript
-                sub main() as void
-                \`\`\`
-                ***
-
-                 The main function
-                `
-            );
-        });
-
-        it('handles mixed case `then` partions of conditionals', () => {
-            let mainFile = program.setFile('source/main.brs', `
-                sub Main()
-                    if true then
-                        print "works"
-                    end if
-                end sub
-            `);
-
-            expectZeroDiagnostics(mainFile);
-            mainFile = program.setFile('source/main.brs', `
-                sub Main()
-                    if true Then
-                        print "works"
-                    end if
-                end sub
-            `);
-            expectZeroDiagnostics(mainFile);
-
-            mainFile = program.setFile('source/main.brs', `
-                sub Main()
-                    if true THEN
-                        print "works"
-                    end if
-                end sub
-            `);
-            expectZeroDiagnostics(mainFile);
-        });
-    });
-
     it('does not throw when encountering incomplete import statement', () => {
         program.setFile('source/main.brs', `
             import
diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts
index 93b55eb0..1fa77c3e 100644
--- a/src/files/BrsFile.ts
+++ b/src/files/BrsFile.ts
@@ -622,7 +622,8 @@ export class BrsFile {
                     name: functionName,
                     nameRange: util.createRange(callee.range.start.line, columnIndexBegin, callee.range.start.line, columnIndexEnd),
                     //TODO keep track of parameters
-                    args: args
+                    args: args,
+                    expression: expression
                 };
                 this.functionCalls.push(functionCall);
             }
diff --git a/src/interfaces.ts b/src/interfaces.ts
index a5b3f4d0..df6c4572 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -15,6 +15,7 @@ import type { BscType } from './types/BscType';
 import type { AstEditor } from './astUtils/AstEditor';
 import type { Token } from './lexer/Token';
 import type { SymbolTypeFlag } from './SymbolTable';
+import type { CallExpression } from './parser/Expression';
 
 export interface BsDiagnostic extends Diagnostic {
     file: BscFile;
@@ -68,6 +69,7 @@ export interface FunctionCall {
      * The full range of this function call (from the start of the function name to its closing paren)
      */
     range: Range;
+    expression: CallExpression;
     functionScope: FunctionScope;
     file: File;
     name: string;
@@ -487,9 +489,9 @@ export class TypeChainEntry {
 }
 
 export interface TypeChainProcessResult {
-    missingItemName: string;
-    missingItemParentTypeName: string;
-    fullNameOfMissingItem: string;
+    itemName: string;
+    itemParentTypeName: string;
+    fullNameOfItem: string;
     fullChainName: string;
     range: Range;
 }
diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts
index 4f9ee510..aa5ca1f5 100644
--- a/src/parser/Expression.ts
+++ b/src/parser/Expression.ts
@@ -10,7 +10,7 @@ import * as fileUrl from 'file-url';
 import type { WalkOptions, WalkVisitor } from '../astUtils/visitors';
 import { WalkMode } from '../astUtils/visitors';
 import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors';
-import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isFunctionType, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNewExpression, isReferenceType, isStringType, isUnaryExpression } from '../astUtils/reflection';
+import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isFunctionType, isIntegerType, isInterfaceMethodStatement, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNewExpression, isReferenceType, isStringType, isUnaryExpression } from '../astUtils/reflection';
 import type { GetTypeOptions, TranspileResult, TypedefProvider } from '../interfaces';
 import { TypeChainEntry } from '../interfaces';
 import type { BscType } from '../types/BscType';
@@ -69,8 +69,12 @@ export class BinaryExpression extends Expression {
                     return new UnionType([this.left.getType(options), this.right.getType(options)]);
                 //TODO: Intersection Types?, eg. case TokenKind.And:
             }
+        } else if (options.flags & SymbolTypeFlag.runtime) {
+            return util.binaryOperatorResultType(
+                this.left.getType(options),
+                this.operator,
+                this.right.getType(options));
         }
-        //TODO: figure out result type on +, *, or, and, etc!
         return DynamicType.instance;
     }
 
@@ -300,7 +304,7 @@ export class FunctionExpression extends Expression implements TypedefProvider {
 
     public getType(options: GetTypeOptions): FunctionType {
         //if there's a defined return type, use that
-        let returnType = this.returnTypeExpression?.getType(options);
+        let returnType = this.returnTypeExpression?.getType({ ...options, typeChain: undefined });
         const isSub = this.functionType.kind === TokenKind.Sub;
         //if we don't have a return type and this is a sub, set the return type to `void`. else use `dynamic`
         if (!returnType) {
@@ -310,8 +314,23 @@ export class FunctionExpression extends Expression implements TypedefProvider {
         const resultType = new FunctionType(returnType);
         resultType.isSub = isSub;
         for (let param of this.parameters) {
-            resultType.addParameter(param.name.text, param.getType(options), !!param.defaultValue);
+            resultType.addParameter(param.name.text, param.getType({ ...options, typeChain: undefined }), !!param.defaultValue);
         }
+        // Figure out this function's name if we can
+        let funcName = '';
+        if (isMethodStatement(this.parent) || isInterfaceMethodStatement(this.parent)) {
+            funcName = this.parent.getName(ParseMode.BrighterScript);
+            if (options.typeChain) {
+                // Get the typechain info from the parent class
+                this.parent.parent?.getType(options);
+            }
+        } else if (isFunctionStatement(this.parent)) {
+            funcName = this.parent.getName(ParseMode.BrighterScript);
+        }
+        if (funcName) {
+            resultType.setName(funcName);
+        }
+        options.typeChain?.push(new TypeChainEntry(funcName, resultType, this.range));
         return resultType;
     }
 }
@@ -330,9 +349,11 @@ export class FunctionParameterExpression extends Expression {
     public readonly kind = AstNodeKind.FunctionParameterExpression;
 
     public getType(options: GetTypeOptions) {
-        return this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime }) ??
-            this.defaultValue?.getType({ ...options, flags: SymbolTypeFlag.runtime }) ??
+        const paramType = this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime, typeChain: undefined }) ??
+            this.defaultValue?.getType({ ...options, flags: SymbolTypeFlag.runtime, typeChain: undefined }) ??
             DynamicType.instance;
+        options.typeChain?.push(new TypeChainEntry(this.name.text, paramType, this.range));
+        return paramType;
     }
 
     public get range(): Range {
@@ -858,6 +879,10 @@ export class UnaryExpression extends Expression {
             walk(this, 'right', visitor, options);
         }
     }
+
+    getType(options: GetTypeOptions): BscType {
+        return util.unaryOperatorResultType(this.operator, this.right.getType(options));
+    }
 }
 
 export class VariableExpression extends Expression {
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 8463be42..48fc2d28 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -10,7 +10,7 @@ import { ParseMode } from './Parser';
 import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
 import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors';
 import { isCallExpression, isCommentStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFieldStatement, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypedefProvider, isUnaryExpression, isVoidType } from '../astUtils/reflection';
-import type { GetTypeOptions, TranspileResult, TypedefProvider } from '../interfaces';
+import { TypeChainEntry, type GetTypeOptions, type TranspileResult, type TypedefProvider } from '../interfaces';
 import { SymbolTypeFlag } from '../SymbolTable';
 import { createInvalidLiteral, createMethodStatement, createToken, interpolatedRange } from '../astUtils/creators';
 import { DynamicType } from '../types/DynamicType';
@@ -163,7 +163,12 @@ export class AssignmentStatement extends Statement {
     }
 
     getType(options: GetTypeOptions) {
-        const rhs = this.value.getType(options);
+        // TODO: Do we still need this.typeExpression?
+
+        // Note: compound assignments (eg. +=) are internally dealt with via the RHS being a BinaryExpression
+        // so this.value will be a BinaryExpression, and BinaryExpressions can figure out their own types
+        const rhs = this.value.getType({ ...options, typeChain: undefined });
+        options.typeChain?.push(new TypeChainEntry(this.name.text, rhs, this.name.range));
         return rhs;
     }
 }
@@ -1496,13 +1501,15 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
         const superIface = this.parentInterfaceName?.getType(options) as InterfaceType;
 
         const resultType = new InterfaceType(this.getName(ParseMode.BrighterScript), superIface);
-
         for (const statement of this.methods) {
-            resultType.addMember(statement?.tokens.name?.text, statement?.range, statement?.getType(options), SymbolTypeFlag.runtime);
+            const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed
+            resultType.addMember(statement?.tokens.name?.text, statement?.range, memberType, SymbolTypeFlag.runtime);
         }
         for (const statement of this.fields) {
-            resultType.addMember(statement?.tokens.name?.text, statement?.range, statement.getType(options), SymbolTypeFlag.runtime);
+            const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed
+            resultType.addMember(statement?.tokens.name?.text, statement?.range, memberType, SymbolTypeFlag.runtime);
         }
+        options.typeChain?.push(new TypeChainEntry(this.getName(ParseMode.BrighterScript), resultType, this.range));
         return resultType;
     }
 }
@@ -1609,6 +1616,12 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
             this.returnTypeExpression
         );
     }
+    /**
+     * Get the name of this method.
+     */
+    public getName(parseMode: ParseMode) {
+        return this.tokens.name.text;
+    }
 
     public tokens = {} as {
         functionType: Token;
@@ -1666,7 +1679,7 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
         return result;
     }
 
-    public getType(options): FunctionType {
+    public getType(options: GetTypeOptions): FunctionType {
         //if there's a defined return type, use that
         let returnType = this.returnTypeExpression?.getType(options);
         const isSub = this.tokens.functionType.kind === TokenKind.Sub;
@@ -1680,6 +1693,13 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
         for (let param of this.params) {
             resultType.addParameter(param.name.text, param.getType(options), !!param.defaultValue);
         }
+        if (options.typeChain) {
+            // need Interface type for type chain
+            this.parent?.getType(options);
+        }
+        let funcName = this.getName(ParseMode.BrighterScript);
+        resultType.setName(funcName);
+        options.typeChain?.push(new TypeChainEntry(resultType.name, resultType, this.range));
         return resultType;
     }
 }
@@ -2071,12 +2091,14 @@ export class ClassStatement extends Statement implements TypedefProvider {
         const resultType = new ClassType(this.getName(ParseMode.BrighterScript), superClass);
 
         for (const statement of this.methods) {
-            const funcType = statement?.func.getType(options);
+            const funcType = statement?.func.getType({ ...options, typeChain: undefined }); //no typechain needed
             resultType.addMember(statement?.name?.text, statement?.range, funcType, SymbolTypeFlag.runtime);
         }
         for (const statement of this.fields) {
-            resultType.addMember(statement?.name?.text, statement?.range, statement.getType(options), SymbolTypeFlag.runtime);
+            const fieldType = statement.getType({ ...options, typeChain: undefined }); //no typechain needed
+            resultType.addMember(statement?.name?.text, statement?.range, fieldType, SymbolTypeFlag.runtime);
         }
+        options.typeChain?.push(new TypeChainEntry(resultType.name, resultType, this.range));
         return resultType;
     }
 }
diff --git a/src/types/InvalidType.ts b/src/types/InvalidType.ts
index 0880bbdb..57e5ca20 100644
--- a/src/types/InvalidType.ts
+++ b/src/types/InvalidType.ts
@@ -11,6 +11,8 @@ export class InvalidType extends BscType {
 
     public readonly kind = BscTypeKind.InvalidType;
 
+    public static instance = new InvalidType('invalid');
+
     public isTypeCompatible(targetType: BscType) {
         return (
             isInvalidType(targetType) ||
diff --git a/src/types/ObjectType.ts b/src/types/ObjectType.ts
index 989dc6e3..875aec41 100644
--- a/src/types/ObjectType.ts
+++ b/src/types/ObjectType.ts
@@ -1,5 +1,5 @@
 import { SymbolTypeFlag } from '../SymbolTable';
-import { isDynamicType, isInheritableType, isObjectType, isUnionType } from '../astUtils/reflection';
+import { isObjectType } from '../astUtils/reflection';
 import type { GetTypeOptions } from '../interfaces';
 import { BscType } from './BscType';
 import { BscTypeKind } from './BscTypeKind';
@@ -15,15 +15,8 @@ export class ObjectType extends BscType {
     public readonly kind = BscTypeKind.ObjectType;
 
     public isTypeCompatible(targetType: BscType) {
-        if (isUnionType(targetType)) {
-            return targetType.checkAllMemberTypes((type) => type.isTypeCompatible(this));
-        } else if (isObjectType(targetType) ||
-            isDynamicType(targetType) ||
-            isInheritableType(targetType)
-        ) {
-            return true;
-        }
-        return false;
+        //Brightscript allows anything passed "as object", so as long as a type is provided, this is true
+        return !!targetType;
     }
 
     public toString() {
diff --git a/src/util.spec.ts b/src/util.spec.ts
index 5e487064..9d04e64c 100644
--- a/src/util.spec.ts
+++ b/src/util.spec.ts
@@ -6,13 +6,16 @@ import type { BsConfig } from './BsConfig';
 import * as fsExtra from 'fs-extra';
 import { createSandbox } from 'sinon';
 import { DiagnosticMessages } from './DiagnosticMessages';
-import { tempDir, rootDir } from './testHelpers.spec';
+import { tempDir, rootDir, expectTypeToBe } from './testHelpers.spec';
 import { Program } from './Program';
 import { TypeChainEntry } from './interfaces';
 import { NamespaceType } from './types/NamespaceType';
 import { ClassType } from './types/ClassType';
 import { ReferenceType } from './types/ReferenceType';
 import { SymbolTypeFlag } from './SymbolTable';
+import { BooleanType, DoubleType, DynamicType, FloatType, IntegerType, InvalidType, LongIntegerType, StringType } from './types';
+import { TokenKind } from './lexer/TokenKind';
+import { createToken } from './astUtils/creators';
 import { createDottedIdentifier, createVariableExpression } from './astUtils/creators';
 
 const sinon = createSandbox();
@@ -866,11 +869,123 @@ describe('util', () => {
             ];
 
             const result = util.processTypeChain(chain);
-            expect(result.missingItemName).to.eql('CharlieProp');
+            expect(result.itemName).to.eql('CharlieProp');
             expect(result.fullChainName).to.eql('AlphaNamespace.BetaProp.CharlieProp');
-            expect(result.missingItemParentTypeName).to.eql('Beta');
-            expect(result.fullNameOfMissingItem).to.eql('Beta.CharlieProp');
+            expect(result.itemParentTypeName).to.eql('Beta');
+            expect(result.fullNameOfItem).to.eql('Beta.CharlieProp');
             expect(result.range).to.eql(util.createRange(3, 3, 4, 4));
         });
     });
+
+    describe('binaryOperatorResultType', () => {
+        it('returns the correct type for math operations', () => {
+            // String + String is string
+            expectTypeToBe(util.binaryOperatorResultType(StringType.instance, createToken(TokenKind.Plus), StringType.instance), StringType);
+            // string plus anything else is an error - return dynamic
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Plus), StringType.instance), DynamicType);
+
+            // Plus
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Plus), IntegerType.instance), DoubleType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Plus), FloatType.instance), FloatType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Plus), LongIntegerType.instance), LongIntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Plus), IntegerType.instance), IntegerType);
+            // Subtract
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Minus), IntegerType.instance), DoubleType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Minus), FloatType.instance), FloatType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Minus), LongIntegerType.instance), LongIntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Minus), IntegerType.instance), IntegerType);
+            // Multiply
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Star), IntegerType.instance), DoubleType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Star), FloatType.instance), FloatType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Star), LongIntegerType.instance), LongIntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Star), IntegerType.instance), IntegerType);
+            // Mod
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Mod), IntegerType.instance), DoubleType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Mod), FloatType.instance), FloatType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Mod), LongIntegerType.instance), LongIntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Mod), IntegerType.instance), IntegerType);
+            // Divide
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Forwardslash), IntegerType.instance), DoubleType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Forwardslash), FloatType.instance), FloatType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Forwardslash), LongIntegerType.instance), LongIntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Forwardslash), IntegerType.instance), FloatType); // int/int -> float
+            // Exponent
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Caret), IntegerType.instance), DoubleType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Caret), FloatType.instance), FloatType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Caret), LongIntegerType.instance), DoubleType);// long^int -> Double, int^long -> Double
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Caret), IntegerType.instance), IntegerType);
+        });
+
+        it('returns the correct type for Bitshift operations', () => {
+            // <<
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.LeftShift), IntegerType.instance), IntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.LeftShift), FloatType.instance), IntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.LeftShift), LongIntegerType.instance), LongIntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.LeftShift), IntegerType.instance), IntegerType);
+            // >>
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.RightShift), IntegerType.instance), IntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.RightShift), FloatType.instance), IntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.RightShift), LongIntegerType.instance), LongIntegerType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.RightShift), IntegerType.instance), IntegerType);
+        });
+
+        it('returns the correct type for Comparison operations', () => {
+            // =
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Equal), IntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Equal), FloatType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Equal), LongIntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Equal), IntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.Equal), IntegerType.instance), BooleanType); // = accepts invalid
+            expectTypeToBe(util.binaryOperatorResultType(StringType.instance, createToken(TokenKind.Equal), IntegerType.instance), DynamicType); // only one string is not accepted
+            expectTypeToBe(util.binaryOperatorResultType(StringType.instance, createToken(TokenKind.Equal), StringType.instance), BooleanType); // both strings is accepted
+            // <>
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.LessGreater), InvalidType.instance), BooleanType); // <> accepts invalid
+            // > - does not accept invalid
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Greater), IntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Greater), FloatType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Greater), LongIntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Greater), IntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.Greater), IntegerType.instance), DynamicType);
+            // etc. - all should be boolean
+        });
+
+        it('returns the correct type for Logical operations', () => {
+            // and
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.And), IntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.And), FloatType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.And), BooleanType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(BooleanType.instance, createToken(TokenKind.And), IntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.And), IntegerType.instance), DynamicType); // invalid not accepted
+            expectTypeToBe(util.binaryOperatorResultType(StringType.instance, createToken(TokenKind.And), IntegerType.instance), DynamicType); // strings are not accepted
+            // or
+            expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Or), IntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Or), FloatType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Or), LongIntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Or), IntegerType.instance), BooleanType);
+            expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.Or), IntegerType.instance), DynamicType);
+        });
+    });
+
+    describe('unaryOperatorResultType', () => {
+        it('returns the correct type for minus operation', () => {
+            let minus = createToken(TokenKind.Minus);
+            expectTypeToBe(util.unaryOperatorResultType(minus, IntegerType.instance), IntegerType);
+            expectTypeToBe(util.unaryOperatorResultType(minus, FloatType.instance), FloatType);
+            expectTypeToBe(util.unaryOperatorResultType(minus, BooleanType.instance), DynamicType);
+            expectTypeToBe(util.unaryOperatorResultType(minus, DoubleType.instance), DoubleType);
+            expectTypeToBe(util.unaryOperatorResultType(minus, StringType.instance), DynamicType);
+        });
+
+        describe('unaryOperatorResultType', () => {
+            it('returns the correct type for not operation', () => {
+                let notToken = createToken(TokenKind.Not);
+                expectTypeToBe(util.unaryOperatorResultType(notToken, IntegerType.instance), IntegerType);
+                expectTypeToBe(util.unaryOperatorResultType(notToken, FloatType.instance), IntegerType);
+                expectTypeToBe(util.unaryOperatorResultType(notToken, BooleanType.instance), BooleanType);
+                expectTypeToBe(util.unaryOperatorResultType(notToken, DoubleType.instance), IntegerType);
+                expectTypeToBe(util.unaryOperatorResultType(notToken, StringType.instance), DynamicType);
+                expectTypeToBe(util.unaryOperatorResultType(notToken, LongIntegerType.instance), LongIntegerType);
+            });
+        });
+    });
 });
diff --git a/src/util.ts b/src/util.ts
index 0358c6e4..c55cd70e 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -26,7 +26,7 @@ import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionP
 import { Logger, LogLevel } from './Logger';
 import type { Identifier, Locatable, Token } from './lexer/Token';
 import { TokenKind } from './lexer/TokenKind';
-import { isBrsFile, isCallExpression, isCallfuncExpression, isDottedGetExpression, isExpression, isIndexedGetExpression, isTypeExpression, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
+import { isBooleanType, isBrsFile, isCallExpression, isCallfuncExpression, isDottedGetExpression, isDoubleType, isExpression, isFloatType, isIndexedGetExpression, isIntegerType, isInvalidType, isLongIntegerType, isStringType, isTypeExpression, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
 import { WalkMode } from './astUtils/visitors';
 import { SourceNode } from 'source-map';
 import * as requireRelative from 'require-relative';
@@ -35,6 +35,7 @@ import type { XmlFile } from './files/XmlFile';
 import type { AstNode } from './parser/AstNode';
 import { AstNodeKind, type Expression, type Statement } from './parser/AstNode';
 import { createIdentifier } from './astUtils/creators';
+import type { BscType } from './types/BscType';
 import type { AssignmentStatement } from './parser/Statement';
 
 export class Util {
@@ -1074,6 +1075,157 @@ export class Util {
         }
     }
 
+    public isNumberType(targetType: BscType): boolean {
+        return isIntegerType(targetType) ||
+            isFloatType(targetType) ||
+            isDoubleType(targetType) ||
+            isLongIntegerType(targetType);
+    }
+
+    /**
+     * Return the type of the result of a binary operator
+     * Note: compound assignments (eg. +=) internally use a binary expression, so that's why TokenKind.PlusEqual, etc. are here too
+     */
+    public binaryOperatorResultType(leftType: BscType, operator: Token, rightType: BscType): BscType {
+        let hasDouble = isDoubleType(leftType) || isDoubleType(rightType);
+        let hasFloat = isFloatType(leftType) || isFloatType(rightType);
+        let hasLongInteger = isLongIntegerType(leftType) || isLongIntegerType(rightType);
+        let hasInvalid = isInvalidType(leftType) || isInvalidType(rightType);
+        let bothNumbers = this.isNumberType(leftType) && this.isNumberType(rightType);
+        let bothStrings = isStringType(leftType) && isStringType(rightType);
+        let eitherBooleanOrNum = (this.isNumberType(leftType) || isBooleanType(leftType)) && (this.isNumberType(rightType) || isBooleanType(rightType));
+
+        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
+        switch (operator.kind) {
+            // Math operators
+            case TokenKind.Plus:
+            case TokenKind.PlusEqual:
+                if (bothStrings) {
+                    // "string" + "string" is the only binary expression allowed with strings
+                    return StringType.instance;
+                }
+            // eslint-disable-next-line no-fallthrough
+            case TokenKind.Minus:
+            case TokenKind.MinusEqual:
+            case TokenKind.Star:
+            case TokenKind.StarEqual:
+            case TokenKind.Mod:
+                if (bothNumbers) {
+                    if (hasDouble) {
+                        return DoubleType.instance;
+                    } else if (hasFloat) {
+                        return FloatType.instance;
+
+                    } else if (hasLongInteger) {
+                        return LongIntegerType.instance;
+                    }
+                    return IntegerType.instance;
+                }
+                break;
+            case TokenKind.Forwardslash:
+            case TokenKind.ForwardslashEqual:
+                if (bothNumbers) {
+                    if (hasDouble) {
+                        return DoubleType.instance;
+                    } else if (hasFloat) {
+                        return FloatType.instance;
+
+                    } else if (hasLongInteger) {
+                        return LongIntegerType.instance;
+                    }
+                    return FloatType.instance;
+                }
+                break;
+            case TokenKind.Backslash:
+            case TokenKind.BackslashEqual:
+                if (bothNumbers) {
+                    if (hasLongInteger) {
+                        return LongIntegerType.instance;
+                    }
+                    return IntegerType.instance;
+                }
+                break;
+            case TokenKind.Caret:
+                if (bothNumbers) {
+                    if (hasDouble || hasLongInteger) {
+                        return DoubleType.instance;
+                    } else if (hasFloat) {
+                        return FloatType.instance;
+                    }
+                    return IntegerType.instance;
+                }
+                break;
+            // Bitshift operators
+            case TokenKind.LeftShift:
+            case TokenKind.LeftShiftEqual:
+            case TokenKind.RightShift:
+            case TokenKind.RightShiftEqual:
+                if (bothNumbers) {
+                    if (hasLongInteger) {
+                        return LongIntegerType.instance;
+                    }
+                    // Bitshifts are allowed with non-integer numerics
+                    // but will always truncate to ints
+                    return IntegerType.instance;
+                }
+                break;
+            // Comparison operators
+            // All comparison operators result in boolean
+            case TokenKind.Equal:
+            case TokenKind.LessGreater:
+                // = and <> can accept invalid
+                if (hasInvalid || bothStrings || eitherBooleanOrNum) {
+                    return BooleanType.instance;
+                }
+                break;
+            case TokenKind.Greater:
+            case TokenKind.Less:
+            case TokenKind.GreaterEqual:
+            case TokenKind.LessEqual:
+                if (bothStrings || bothNumbers) {
+                    return BooleanType.instance;
+                }
+                break;
+            // Logical operators
+            case TokenKind.Or:
+            case TokenKind.And:
+                if (eitherBooleanOrNum) {
+                    return BooleanType.instance;
+                }
+                break;
+        }
+        return DynamicType.instance;
+    }
+
+    /**
+     * Return the type of the result of a binary operator
+     */
+    public unaryOperatorResultType(operator: Token, exprType: BscType): BscType {
+        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
+        switch (operator.kind) {
+            // Math operators
+            case TokenKind.Minus:
+                if (this.isNumberType(exprType)) {
+                    // a negative number will be the same type, eg, double->double, int->int, etc.
+                    return exprType;
+                }
+                break;
+            case TokenKind.Not:
+                if (isBooleanType(exprType)) {
+                    return BooleanType.instance;
+                } else if (this.isNumberType(exprType)) {
+                    //numbers can be "notted"
+                    // by default they go to ints, except longints, which stay that way
+                    if (isLongIntegerType(exprType)) {
+                        return LongIntegerType.instance;
+                    }
+                    return IntegerType.instance;
+                }
+                break;
+        }
+        return DynamicType.instance;
+    }
+
     /**
      * Get the extension for the given file path. Basically the part after the final dot, except for
      * `d.bs` which is treated as single extension
@@ -1518,7 +1670,7 @@ export class Util {
     public processTypeChain(typeChain: TypeChainEntry[]): TypeChainProcessResult {
         let fullChainName = '';
         let fullErrorName = '';
-        let missingItemName = '';
+        let itemName = '';
         let previousTypeName = '';
         let parentTypeName = '';
         let errorRange: Range;
@@ -1531,16 +1683,16 @@ export class Util {
             parentTypeName = previousTypeName;
             fullErrorName = previousTypeName ? `${previousTypeName}.${chainItem.name}` : chainItem.name;
             previousTypeName = chainItem.type.toString();
-            missingItemName = chainItem.name;
+            itemName = chainItem.name;
             if (!chainItem.isResolved) {
                 errorRange = chainItem.range;
                 break;
             }
         }
         return {
-            missingItemName: missingItemName,
-            missingItemParentTypeName: parentTypeName,
-            fullNameOfMissingItem: fullErrorName,
+            itemName: itemName,
+            itemParentTypeName: parentTypeName,
+            fullNameOfItem: fullErrorName,
             fullChainName: fullChainName,
             range: errorRange
         };


Test patch:
diff --git a/src/parser/tests/expression/TernaryExpression.spec.ts b/src/parser/tests/expression/TernaryExpression.spec.ts
index c1426506..ecd239b4 100644
--- a/src/parser/tests/expression/TernaryExpression.spec.ts
+++ b/src/parser/tests/expression/TernaryExpression.spec.ts
@@ -407,11 +407,11 @@ describe('ternary expressions', () => {
         it('complex conditions do not cause scope capture', () => {
             testTranspile(`
                 sub main()
-                    a = str("true") = "true" ? true : false
+                    a = str(123) = "123" ? true : false
                 end sub
             `, `
                 sub main()
-                    a = bslib_ternary(str("true") = "true", true, false)
+                    a = bslib_ternary(str(123) = "123", true, false)
                 end sub
             `);