Repository: rokucommunity/brighterscript
Pull request #1154 (2024-05-03 17:08:11)

`CreateObject` function call implies correct return type
Previously, everything returned from `createObject` was `ObjectType`

Now, it looks at the arguments, and if passed string literals, it is able to infer either the built-in Component/Object or a custom component:


```brighterscript
dateTime = createObject("roDatetime") ' this is the InterfaceType `roDateTime`
dateTime.getHours() ' this is an integer


button = createObject("roSgnode", "Poster") '  this is the ComponentType `roSgNodePoster`
button.uri ' this is a string
```


if the args are not string literals, it still just returns ObjectType....

```brighterscript
function getObject(objectName as string)
    return createObject(objectName) ' this is just ObjectType
end function
```

markwpearce: One cool side effect of this is that it opens the door to generics - the return type can be based on the specifics of the call expression....
markwpearce: Ok. Hmmm.   I don’t know about making support for overloading functions yet, so maybe I’ll change this so it’s just a special case, and does not change the interface/constructors for anything yet. 


Patch:
diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts
index 6927e79b..6bbfb903 100644
--- a/src/Scope.spec.ts
+++ b/src/Scope.spec.ts
@@ -17,12 +17,11 @@ import { BooleanType } from './types/BooleanType';
 import { StringType } from './types/StringType';
 import { IntegerType } from './types/IntegerType';
 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 { isForEachStatement, isFunctionExpression, isFunctionStatement, isNamespaceStatement } from './astUtils/reflection';
+import { isBlock, isForEachStatement, isFunctionExpression, isFunctionStatement, isNamespaceStatement } from './astUtils/reflection';
 import { ArrayType } from './types/ArrayType';
 import { AssociativeArrayType } from './types/AssociativeArrayType';
 import { InterfaceType } from './types/InterfaceType';
@@ -30,6 +29,7 @@ import { ComponentType } from './types/ComponentType';
 import * as path from 'path';
 import { WalkMode, createVisitor } from './astUtils/visitors';
 import type { FunctionExpression } from './parser/Expression';
+import { ObjectType } from './types';
 
 describe('Scope', () => {
     let sinon = sinonImport.createSandbox();
@@ -800,6 +800,71 @@ describe('Scope', () => {
                     DiagnosticMessages.unknownBrightScriptComponent('roFontMetrics')
                 ]);
             });
+
+            it('infers the correct type', () => {
+                const file = program.setFile<BrsFile>(`source/file.brs`, `
+                    sub main()
+                        scene = CreateObject("roSGScreen")
+                        button = CreateObject("roSGNode", "Button")
+                        list = CreateObject("roSGNode", "MarkupList")
+                    end sub
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                const mainSymbolTable = file.ast.findChild(isBlock).getSymbolTable();
+                const sceneType = mainSymbolTable.getSymbolType('scene', { flags: SymbolTypeFlag.runtime }) as InterfaceType;
+                expectTypeToBe(sceneType, InterfaceType);
+                expect(sceneType.name).to.eq('roSGScreen');
+                const buttonType = mainSymbolTable.getSymbolType('button', { flags: SymbolTypeFlag.runtime }) as InterfaceType;
+                expectTypeToBe(buttonType, ComponentType);
+                expect(buttonType.name).to.eq('Button');
+                const listType = mainSymbolTable.getSymbolType('list', { flags: SymbolTypeFlag.runtime }) as InterfaceType;
+                expectTypeToBe(listType, ComponentType);
+                expect(listType.name).to.eq('MarkupList');
+            });
+
+            it('infers custom component types', () => {
+                program.setFile('components/Comp1.xml', trim`
+                    <?xml version="1.0" encoding="utf-8" ?>
+                    <component name="Comp1" extends="Group">
+                    </component>
+                `);
+                program.setFile('components/Comp2.xml', trim`
+                    <?xml version="1.0" encoding="utf-8" ?>
+                    <component name="Comp2" extends="Poster">
+                    </component>
+                `);
+                const file = program.setFile<BrsFile>(`source/file.brs`, `
+                    sub main()
+                        comp1 = CreateObject("roSGNode", "Comp1")
+                        comp2 = CreateObject("roSGNode", "Comp2")
+                    end sub
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                program.getScopeByName('source').linkSymbolTable();
+                const mainSymbolTable = file.ast.findChild(isBlock).getSymbolTable();
+                const comp1Type = mainSymbolTable.getSymbolType('comp1', { flags: SymbolTypeFlag.runtime }) as InterfaceType;
+                expectTypeToBe(comp1Type, ComponentType);
+                expect(comp1Type.name).to.eq('Comp1');
+                const comp2Type = mainSymbolTable.getSymbolType('comp2', { flags: SymbolTypeFlag.runtime }) as InterfaceType;
+                expectTypeToBe(comp2Type, ComponentType);
+                expect(comp2Type.name).to.eq('Comp2');
+            });
+
+            it('implies objectType by default', () => {
+                const file = program.setFile<BrsFile>(`source/file.brs`, `
+                    function getObj(myObjName)
+                        result = CreateObject(myObjName)
+                        return result
+                    end function
+                `);
+                program.validate();
+                expectZeroDiagnostics(program);
+                const mainSymbolTable = file.ast.findChild(isBlock).getSymbolTable();
+                const resultType = mainSymbolTable.getSymbolType('result', { flags: SymbolTypeFlag.runtime });
+                expectTypeToBe(resultType, ObjectType);
+            });
         });
 
         it('marks the scope as validated after validation has occurred', () => {
@@ -2589,9 +2654,9 @@ describe('Scope', () => {
             expect(mainFnScope).to.exist;
             const getTypeOptions = { flags: SymbolTypeFlag.runtime };
             let dtType = mainFnScope.symbolTable.getSymbolType('dt', getTypeOptions);
-            expectTypeToBe(dtType, ObjectType);
+            expectTypeToBe(dtType, InterfaceType);
             let hoursType = mainFnScope.symbolTable.getSymbolType('hours', getTypeOptions);
-            expectTypeToBe(hoursType, DynamicType);
+            expectTypeToBe(hoursType, IntegerType);
         });
 
         describe('union types', () => {
diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts
index 32cc4ef2..ac8a2463 100644
--- a/src/parser/Expression.ts
+++ b/src/parser/Expression.ts
@@ -112,9 +112,9 @@ export class CallExpression extends Expression {
 
     constructor(options: {
         callee: Expression;
-        openingParen: Token;
+        openingParen?: Token;
         args?: Expression[];
-        closingParen: Token;
+        closingParen?: Token;
     }) {
         super();
         this.tokens = {
@@ -184,6 +184,10 @@ export class CallExpression extends Expression {
         if (isNewExpression(this.parent)) {
             return calleeType;
         }
+        const specialCaseReturnType = util.getSpecialCaseCallExpressionReturnType(this);
+        if (specialCaseReturnType) {
+            return specialCaseReturnType;
+        }
         if (isCallableType(calleeType) && (!isReferenceType(calleeType.returnType) || calleeType.returnType?.isResolvable())) {
             return calleeType.returnType;
         }
diff --git a/src/types/TypedFunctionType.spec.ts b/src/types/TypedFunctionType.spec.ts
index 585294aa..6db58e20 100644
--- a/src/types/TypedFunctionType.spec.ts
+++ b/src/types/TypedFunctionType.spec.ts
@@ -154,6 +154,5 @@ describe('TypedFunctionType', () => {
         let variFunc2 = new TypedFunctionType(DynamicType.instance);
         variFunc2.isVariadic = true;
         expect(variFunc1.isTypeCompatible(variFunc2)).to.be.true;
-
     });
 });
diff --git a/src/util.ts b/src/util.ts
index e863b6e9..bdfbefc4 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 { isToken, type Identifier, type Locatable, type Token } from './lexer/Token';
 import { TokenKind } from './lexer/TokenKind';
-import { isAnyReferenceType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallfuncExpression, isClassType, isDottedGetExpression, isDoubleType, isDynamicType, isEnumMemberType, isExpression, isFloatType, isIndexedGetExpression, isInvalidType, isLongIntegerType, isNewExpression, isNumberType, isStringType, isTypeExpression, isTypedArrayExpression, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
+import { isAnyReferenceType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallfuncExpression, isClassType, isDottedGetExpression, isDoubleType, isDynamicType, isEnumMemberType, isExpression, isFloatType, isIndexedGetExpression, isInvalidType, isLiteralString, isLongIntegerType, isNewExpression, isNumberType, isStringType, isTypeExpression, isTypedArrayExpression, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
 import { WalkMode } from './astUtils/visitors';
 import { SourceNode } from 'source-map';
 import * as requireRelative from 'require-relative';
@@ -2127,6 +2127,27 @@ export class Util {
         }
         return false;
     }
+
+    public getSpecialCaseCallExpressionReturnType(callExpr: CallExpression) {
+        if (isVariableExpression(callExpr.callee) && callExpr.callee.tokens.name.text.toLowerCase() === 'createobject') {
+            const componentName = isLiteralString(callExpr.args[0]) ? callExpr.args[0].tokens.value?.text?.replace(/"/g, '') : '';
+            const nodeType = componentName.toLowerCase() === 'rosgnode' && isLiteralString(callExpr.args[1]) ? callExpr.args[1].tokens.value?.text?.replace(/"/g, '') : '';
+            if (componentName?.toLowerCase().startsWith('ro')) {
+                const fullName = componentName + nodeType;
+                const data = {};
+                const symbolTable = callExpr.getSymbolTable();
+                const foundType = symbolTable.getSymbolType(fullName, {
+                    flags: SymbolTypeFlag.typetime,
+                    data: data,
+                    tableProvider: () => callExpr?.getSymbolTable(),
+                    fullName: fullName
+                });
+                if (foundType) {
+                    return foundType;
+                }
+            }
+        }
+    }
 }
 
 /**


Test patch:
diff --git a/src/files/tests/optionalChaning.spec.ts b/src/files/tests/optionalChaning.spec.ts
index 0b4006bb..d1ad0b0f 100644
--- a/src/files/tests/optionalChaning.spec.ts
+++ b/src/files/tests/optionalChaning.spec.ts
@@ -80,12 +80,12 @@ describe('optional chaining', () => {
                 print arr?.value
                 print obj?.[0]
                 print obj?.getName()?.first?.second
-                print createObject("roByteArray")?.value
+                print createObject("roByteArray")?.Count
                 print createObject("roByteArray")?["0"]
-                print createObject("roList")?.value
+                print createObject("roList")?.Count
                 print createObject("roList")?["0"]
                 print createObject("roXmlList")?["0"]
-                print createObject("roDateTime")?.value
+                print createObject("roDateTime")?.GetYear
                 print createObject("roDateTime")?.GetTimeZoneOffset
                 print createObject("roSGNode", "Node")?[0]
                 print obj?.first?.second