diff --git a/src/Program.ts b/src/Program.ts index e28c28124..c822ac83b 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -52,12 +52,15 @@ import { ProgramValidatorDiagnosticsTag } from './bscPlugin/validation/ProgramVa import type { ProvidedSymbolInfo, BrsFile } from './files/BrsFile'; import type { XmlFile } from './files/XmlFile'; import { SymbolTable } from './SymbolTable'; -import { ReferenceType, TypesCreated } from './types'; +import type { BscType } from './types/BscType'; +import { ReferenceType } from './types/ReferenceType'; +import { TypesCreated } from './types/helpers'; import type { Statement } from './parser/AstNode'; import { CallExpressionInfo } from './bscPlugin/CallExpressionInfo'; import { SignatureHelpUtil } from './bscPlugin/SignatureHelpUtil'; import { Sequencer } from './common/Sequencer'; import { Deferred } from './deferred'; +import { roFunctionType } from './types/roFunctionType'; const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`; const bslibAliasedRokuModulesPkgPath = s`source/roku_modules/bslib/bslib.brs`; @@ -216,20 +219,28 @@ export class Program { } for (const ifaceData of Object.values(interfaces) as BRSInterfaceData[]) { - const nodeType = new InterfaceType(ifaceData.name); - nodeType.addBuiltInInterfaces(); - nodeType.isBuiltIn = true; - this.globalScope.symbolTable.addSymbol(ifaceData.name, { ...builtInSymbolData, description: ifaceData.description }, nodeType, SymbolTypeFlag.typetime); + const ifaceType = new InterfaceType(ifaceData.name); + ifaceType.addBuiltInInterfaces(); + ifaceType.isBuiltIn = true; + this.globalScope.symbolTable.addSymbol(ifaceData.name, { ...builtInSymbolData, description: ifaceData.description }, ifaceType, SymbolTypeFlag.typetime); } for (const componentData of Object.values(components) as BRSComponentData[]) { - const nodeType = new InterfaceType(componentData.name); - nodeType.addBuiltInInterfaces(); - nodeType.isBuiltIn = true; - if (componentData.name !== 'roSGNode') { + let roComponentType: BscType; + const lowerComponentName = componentData.name.toLowerCase(); + + if (lowerComponentName === 'rosgnode') { // we will add `roSGNode` as shorthand for `roSGNodeNode`, since all roSgNode components are SceneGraph nodes - this.globalScope.symbolTable.addSymbol(componentData.name, { ...builtInSymbolData, description: componentData.description }, nodeType, SymbolTypeFlag.typetime); + continue; + } + if (lowerComponentName === 'rofunction') { + roComponentType = new roFunctionType(); + } else { + roComponentType = new InterfaceType(componentData.name); } + roComponentType.addBuiltInInterfaces(); + roComponentType.isBuiltIn = true; + this.globalScope.symbolTable.addSymbol(componentData.name, { ...builtInSymbolData, description: componentData.description }, roComponentType, SymbolTypeFlag.typetime); } for (const nodeData of Object.values(nodes) as SGNodeData[]) { @@ -237,10 +248,10 @@ export class Program { } for (const eventData of Object.values(events) as BRSEventData[]) { - const nodeType = new InterfaceType(eventData.name); - nodeType.addBuiltInInterfaces(); - nodeType.isBuiltIn = true; - this.globalScope.symbolTable.addSymbol(eventData.name, { ...builtInSymbolData, description: eventData.description }, nodeType, SymbolTypeFlag.typetime); + const eventType = new InterfaceType(eventData.name); + eventType.addBuiltInInterfaces(); + eventType.isBuiltIn = true; + this.globalScope.symbolTable.addSymbol(eventData.name, { ...builtInSymbolData, description: eventData.description }, eventType, SymbolTypeFlag.typetime); } } diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 4423968ae..b3720a474 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -339,7 +339,7 @@ export function isFunctionType(value: any): value is FunctionType { return value?.kind === BscTypeKind.FunctionType; } export function isRoFunctionType(value: any): value is InterfaceType { - return isBuiltInType(value, 'roFunction'); + return value?.kind === BscTypeKind.RoFunctionType || isBuiltInType(value, 'roFunction'); } export function isFunctionTypeLike(value: any): value is FunctionType | InterfaceType { return isFunctionType(value) || isRoFunctionType(value); @@ -465,7 +465,7 @@ export function isInheritableType(target): target is InheritableType { } export function isCallableType(target): target is BaseFunctionType { - return isFunctionTypeLike(target) || isTypedFunctionType(target) || (isDynamicType(target) && !isAnyReferenceType(target)); + return isFunctionTypeLike(target) || isTypedFunctionType(target) || isObjectType(target) || (isDynamicType(target) && !isAnyReferenceType(target)); } export function isAnyReferenceType(target): target is AnyReferenceType { diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index 303a23004..9e8e86d19 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -16,6 +16,7 @@ import { tempDir, rootDir } from '../../testHelpers.spec'; import { isReturnStatement } from '../../astUtils/reflection'; import { ScopeValidator } from './ScopeValidator'; import type { ReturnStatement } from '../../parser/Statement'; +import { Logger } from '@rokucommunity/logger'; describe('ScopeValidator', () => { @@ -2892,6 +2893,75 @@ describe('ScopeValidator', () => { program.validate(); expectZeroDiagnostics(program); }); + + it('allows returning a function call', () => { + const spy = sinon.spy(Logger.prototype, 'error'); + program.setFile('source/main.bs', ` + function abc(func as function) as dynamic + return func() + end function + `); + program.validate(); + expectZeroDiagnostics(program); + expect( + spy.getCalls().map(x => (x.args?.[0] as string)?.toString()).filter(x => x?.includes('Error when calling plugin')) + ).to.eql([]); + }); + + it('allows returning a roFunction call', () => { + const spy = sinon.spy(Logger.prototype, 'error'); + program.setFile('source/main.bs', ` + function abc(func as roFunction) as dynamic + return func() + end function + `); + program.validate(); + expectZeroDiagnostics(program); + expect( + spy.getCalls().map(x => (x.args?.[0] as string)?.toString()).filter(x => x?.includes('Error when calling plugin')) + ).to.eql([]); + }); + + it('allows returning a call on an object type', () => { + const spy = sinon.spy(Logger.prototype, 'error'); + program.setFile('source/main.bs', ` + function abc(func as object) as dynamic + return func() + end function + `); + program.validate(); + expectZeroDiagnostics(program); + expect( + spy.getCalls().map(x => (x.args?.[0] as string)?.toString()).filter(x => x?.includes('Error when calling plugin')) + ).to.eql([]); + }); + + it('allows calling func returned from other func', () => { + const spy = sinon.spy(Logger.prototype, 'error'); + program.setFile('source/calc.bs', ` + sub otherFuncFirst() + ' forces getOperation to be referenceType called from ReferenceType + end sub + + function calc(a as dynamic, b as dynamic, op as string) as dynamic + op = getOperation(op) + return op(1, 2) + end function + + function getOperation(name as string) as object + return { + "sum": function(a as dynamic, b as dynamic) as dynamic + return a + b + end function + }[name] + end function + `); + program.validate(); + expectZeroDiagnostics(program); + expect( + spy.getCalls().map(x => (x.args?.[0] as string)?.toString()).filter(x => x?.includes('Error when calling plugin')) + ).to.eql([]); + }); }); describe('returnTypeCoercionMismatch', () => { @@ -4618,6 +4688,16 @@ describe('ScopeValidator', () => { DiagnosticMessages.notCallable('"string"').message ]); }); + + it('allows calling an object type', () => { + program.setFile('source/calc.bs', ` + function someFunc(otherFunc as object) as dynamic + return otherFunc() + end function + `); + program.validate(); + expectZeroDiagnostics(program); + }); }); describe('callFunc', () => { diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 7941f1e83..689f76b37 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -200,6 +200,7 @@ export class CallExpression extends Expression { if (isNewExpression(this.parent)) { return calleeType; } + const specialCaseReturnType = util.getSpecialCaseCallExpressionReturnType(this, options); if (specialCaseReturnType) { return specialCaseReturnType; diff --git a/src/types/BscTypeKind.ts b/src/types/BscTypeKind.ts index f262a03c4..cb511541c 100644 --- a/src/types/BscTypeKind.ts +++ b/src/types/BscTypeKind.ts @@ -18,6 +18,7 @@ export enum BscTypeKind { NamespaceType = 'NamespaceType', ObjectType = 'ObjectType', ReferenceType = 'ReferenceType', + RoFunctionType = 'RoFunctionType', StringType = 'StringType', UninitializedType = 'UninitializedType', UnionType = 'UnionType', diff --git a/src/types/ObjectType.ts b/src/types/ObjectType.ts index df4ff0eaf..51c5bae16 100644 --- a/src/types/ObjectType.ts +++ b/src/types/ObjectType.ts @@ -36,6 +36,10 @@ export class ObjectType extends BscType { isEqual(otherType: BscType) { return isObjectType(otherType) && this.checkCompatibilityBasedOnMembers(otherType, SymbolTypeFlag.runtime); } + + get returnType() { + return DynamicType.instance; + } } diff --git a/src/types/ReferenceType.ts b/src/types/ReferenceType.ts index 097e45ed2..6ad03d9f2 100644 --- a/src/types/ReferenceType.ts +++ b/src/types/ReferenceType.ts @@ -362,6 +362,12 @@ export class TypePropertyReferenceType extends BscType { return outerType; } + if (propName === 'getTarget') { + return () => { + return this.getTarget(); + }; + } + if (propName === 'isResolvable') { return () => { return !!(isAnyReferenceType(this.outerType) ? (this.outerType as any).getTarget() : this.outerType?.isResolvable()); @@ -394,7 +400,7 @@ export class TypePropertyReferenceType extends BscType { return () => false; } } - let inner = (isAnyReferenceType(this.outerType) ? (this.outerType as ReferenceType).getTarget() : this.outerType)?.[this.propertyName]; + let inner = this.getTarget(); if (!inner) { inner = DynamicType.instance; @@ -420,12 +426,19 @@ export class TypePropertyReferenceType extends BscType { }); } - getTarget: () => BscType; + getTarget(): BscType { + let actualOuterType = this.outerType; + if (isAnyReferenceType(this.outerType)) { + if ((this.outerType as ReferenceType).isResolvable()) { + actualOuterType = (this.outerType as ReferenceType)?.getTarget(); + } + } + return actualOuterType?.[this.propertyName]; + } tableProvider: SymbolTableProvider; } - /** * Use this class for when there is a binary operator and either the left hand side and/or the right hand side * are ReferenceTypes diff --git a/src/types/roFunctionType.spec.ts b/src/types/roFunctionType.spec.ts new file mode 100644 index 000000000..e502de9a4 --- /dev/null +++ b/src/types/roFunctionType.spec.ts @@ -0,0 +1,20 @@ +import { expect } from '../chai-config.spec'; + +import { DynamicType } from './DynamicType'; +import { FunctionType } from './FunctionType'; +import { IntegerType } from './IntegerType'; +import { ObjectType } from './ObjectType'; +import { roFunctionType } from './roFunctionType'; +import { TypedFunctionType } from './TypedFunctionType'; + +describe('roFunctionType', () => { + it('is equivalent to other function types', () => { + const roFunc = new roFunctionType(); + + expect(roFunc.isTypeCompatible(new ObjectType())).to.be.true; + expect(roFunc.isTypeCompatible(new DynamicType())).to.be.true; + expect(roFunc.isTypeCompatible(new FunctionType())).to.be.true; + expect(roFunc.isTypeCompatible(new roFunctionType())).to.be.true; + expect(roFunc.isTypeCompatible(new TypedFunctionType(IntegerType.instance))).to.be.true; + }); +}); diff --git a/src/types/roFunctionType.ts b/src/types/roFunctionType.ts new file mode 100644 index 000000000..1dc81c28d --- /dev/null +++ b/src/types/roFunctionType.ts @@ -0,0 +1,39 @@ +import { isCallableType, isDynamicType, isFunctionTypeLike, isObjectType } from '../astUtils/reflection'; +import type { TypeCompatibilityData } from '../interfaces'; +import { BaseFunctionType } from './BaseFunctionType'; +import type { BscType } from './BscType'; +import { BscTypeKind } from './BscTypeKind'; +import { isUnionTypeCompatible } from './helpers'; + +export class roFunctionType extends BaseFunctionType { + public readonly kind = BscTypeKind.RoFunctionType; + + public isTypeCompatible(targetType: BscType, data?: TypeCompatibilityData) { + if ( + isDynamicType(targetType) || + isCallableType(targetType) || + isFunctionTypeLike(targetType) || + isObjectType(targetType) || + isUnionTypeCompatible(this, targetType, data) + ) { + return true; + } + return false; + } + + public toString() { + return 'roFunction'; + } + + public toTypeString(): string { + return 'dynamic'; + } + + + isEqual(targetType: BscType) { + if (isFunctionTypeLike(targetType)) { + return true; + } + return false; + } +}