diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 60f6a5aa6..ea58514a6 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -4583,6 +4583,20 @@ describe('BrsFile', () => { `); }); }); + + describe('typed functions in type expressions', () => { + it('transpiles to function', () => { + testTranspile(` + function test(func as function(name as string, num as integer) as integer) as integer + return func("hello", 123) + end function + `, ` + function test(func as Function) as integer + return func("hello", 123) + end function + `); + }); + }); }); it('allows up to 63 function params', () => { diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index 9b9fef941..6195be1ab 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -1785,6 +1785,108 @@ describe('parser', () => { expectZeroDiagnostics(diagnostics); }); }); + + describe('typed functions as types', () => { + it('disallowed in brightscript mode', () => { + let { diagnostics } = parse(` + function test(func as function()) + return func() + end function + `, ParseMode.BrightScript); + expectDiagnosticsIncludes(diagnostics, [ + DiagnosticMessages.unexpectedToken(')') + ]); + }); + + it('can be passed as param types', () => { + let { diagnostics } = parse(` + function test(func as function()) + return func() + end function + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('can have a return type', () => { + let { diagnostics } = parse(` + function test(func as sub() as integer) as integer + return func() + end function + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('can use sub or function', () => { + let { diagnostics } = parse(` + function test(func as sub() as integer) as integer + return func() + end function + + function test2(func as function() as integer) as integer + return func() + end function + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('can have primitive parameters', () => { + let { diagnostics } = parse(` + function test(func as function(name as string, num as integer) as integer) as integer + return func("hello", 123) + end function + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('can have complex parameters', () => { + let { diagnostics } = parse(` + interface IFace + name as string + end interface + + function test(func as function(thing as IFace) as integer) as integer + return func({name: "hello"}) + end function + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('can have compound parameters', () => { + let { diagnostics } = parse(` + interface IFace + name as string + end interface + + function test(func as function(arg1 as string or integer, arg2 as IFace) as integer) as integer + return func("hello", {name: "hello"}) + end function + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('can be used as return types', () => { + let { diagnostics } = parse(` + function test() as function() as integer + return function() as integer + return 123 + end function + end function + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('can have a union as return type', () => { + let { diagnostics } = parse(` + type foo = function() as integer or string + function test() as foo + return function() as integer + return 123 + end function + end function + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + }); }); function parse(text: string, mode?: ParseMode) { diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 852ce650b..3616fd1ff 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -2813,7 +2813,9 @@ export class Parser { while (lookForCompounds) { lookForCompounds = false; - if (this.checkAny(...DeclarableTypes)) { + const isTypedFunction = this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkNext(TokenKind.LeftParen); + + if (this.checkAny(...DeclarableTypes) && !isTypedFunction) { // Token is a built in type typeToken = this.advance(); } else if (this.options.mode === ParseMode.BrighterScript) { @@ -2824,6 +2826,9 @@ export class Parser { } else if (this.check(TokenKind.LeftParen)) { // could be an inline interface typeToken = this.groupedTypeExpression(); + } else if (isTypedFunction) { + //typed function type + typeToken = this.typedFunctionType(); } else { // see if we can get a namespaced identifer const qualifiedType = this.getNamespacedVariableNameExpression(ignoreDiagnostics); @@ -2941,6 +2946,28 @@ export class Parser { return createToken(TokenKind.Dynamic, null, util.createBoundingRange(leftParen, typeToken, rightParen)); } + private typedFunctionType() { + const funcOrSub = this.advance(); + const leftParen = this.advance(); + + let params = [] as FunctionParameterExpression[]; + if (!this.check(TokenKind.RightParen)) { + do { + params.push(this.functionParameter()); + } while (this.match(TokenKind.Comma)); + } + const rightParen = this.advance(); + let asToken: Token; + let returnType: Token; + if ((this.check(TokenKind.As))) { + // this is a function type with a return type, e.g. `function(string) as void` + asToken = this.advance(); + returnType = this.typeToken(); + } + + return createToken(TokenKind.Function, null, util.createBoundingRange(funcOrSub, leftParen, rightParen, asToken, returnType)); + } + private primary(): Expression { switch (true) { case this.matchAny(