Skip to content

Commit

Permalink
Adds callFunc as member method to Custom Components (#929)
Browse files Browse the repository at this point in the history
* Added Callfunc as ComponentType member

* Tightened up TypedFUnctionType compatibility code and added tests for .callFunc invocations

* fixed build issue

* Added callfunc as an override directly in scrape-roku-docs script

* fixed build errors

* Rescraped Roku docs
  • Loading branch information
markwpearce authored Oct 10, 2023
1 parent 28d6d6a commit 725c3c6
Show file tree
Hide file tree
Showing 14 changed files with 362 additions and 46 deletions.
2 changes: 1 addition & 1 deletion scripts/.cache.json

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions scripts/scrape-roku-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,26 @@ class Runner {
private mergeOverrides() {
this.result = deepmerge(this.result, {
nodes: {
node: {
// Taken from: https://developer.roku.com/en-ca/docs/developer-program/core-concepts/handling-application-events.md#functional-fields
methods: [
{
description: `callFunc() is a synchronized interface on roSGNode. It will always execute in the component's owning ScriptEngine and thread (by rendezvous if necessary), and it will always use the m and m.top of the owning component. Any context from the caller can be passed via one or more method parameters, which may be of any type (previously, callFunc() only supported a single associative array parameter).\n\nTo call the function, use the \`callFunc\` field with the required method signature. A return value, if any, can be an object that is similarly arbitrary. The method being called must determine how to interpret the parameters included in the \`callFunc\` field.`,
name: 'callFunc',
params: [
{
default: null,
description: 'The function name to call.',
isRequired: true,
name: 'functionName',
type: 'String'
}
],
isVariadic: true,
returnType: 'Dynamic'
}
]
}
},
components: {},
events: {},
Expand Down Expand Up @@ -1205,6 +1225,7 @@ interface SceneGraphNode {
interfaces: Reference[];
events: Reference[];
fields: SceneGraphNodeField[];
methods: Func[];
}

interface SceneGraphNodeField {
Expand Down Expand Up @@ -1239,6 +1260,7 @@ interface Signature {
params: Param[];
returnType: string;
returnDescription: string;
isVariadic?: boolean;
}
interface ElementFilter {
id?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/Program.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2278,7 +2278,7 @@ describe('Program', () => {
expectTypeToBe(table.getSymbolType('ifDeviceInfo', opts).getMemberType('GetRandomUUID', rtOpts), TypedFunctionType);

expectTypeToBe(table.getSymbolType('ifSGNodeField', opts), InterfaceType);
expectTypeToBe(table.getSymbolType('ifSGNodeField', opts).getMemberType('observeField', rtOpts), TypedFunctionType);
expectTypeToBe(table.getSymbolType('ifSGNodeField', opts).getMemberType('addFields', rtOpts), TypedFunctionType);
});

it('adds brightscript events', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class Program {
nodeType.addBuiltInInterfaces();
this.globalScope.symbolTable.addSymbol(nodeName, { description: nodeData.description }, nodeType, SymbolTypeFlag.typetime);
} else {
nodeType = this.globalScope.symbolTable.getSymbolType(nodeData.name, { flags: SymbolTypeFlag.typetime }) as ComponentType;
nodeType = this.globalScope.symbolTable.getSymbolType(nodeName, { flags: SymbolTypeFlag.typetime }) as ComponentType;
}

return nodeType;
Expand Down
28 changes: 26 additions & 2 deletions src/XmlScope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Position, Range } from 'vscode-languageserver';
import { DiagnosticMessages } from './DiagnosticMessages';
import type { XmlFile } from './files/XmlFile';
import { Program } from './Program';
import { expectDiagnostics, expectTypeToBe, trim } from './testHelpers.spec';
import { expectDiagnostics, expectTypeToBe, expectZeroDiagnostics, trim } from './testHelpers.spec';
import { standardizePath as s, util } from './util';
let rootDir = s`${process.cwd()}/rootDir`;
import { createSandbox } from 'sinon';
Expand Down Expand Up @@ -219,7 +219,7 @@ describe('XmlScope', () => {
program.setFile('components/Widget.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="Widget" extends="Group">
<script uri="Widget.brs"/>
<script uri="Widget.brs"/>
<interface>
<function name="someFunc" />
</interface>
Expand All @@ -239,5 +239,29 @@ describe('XmlScope', () => {
expectTypeToBe(widgetType.getCallFuncType('someFunc', { flags: SymbolTypeFlag.runtime }), TypedFunctionType);
});

it('allows .callFunc() on components', () => {
program.setFile('components/Widget.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="Widget" extends="Group">
<script uri="Widget.brs"/>
<interface>
<function name="someFunc" />
</interface>
</component>
`);
program.setFile('components/Widget.brs', `
sub someFunc(input as object)
print input
end sub
`);
program.setFile('source/util.brs', `
sub useCallFunc(input as roSGNodeWidget)
input.callFunc("someFunc", {hello: "world"})
end sub
`);
program.validate();
expectZeroDiagnostics(program);
});

});
});
3 changes: 2 additions & 1 deletion src/XmlScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { isXmlFile } from './astUtils/reflection';
import { SGFieldTypes } from './parser/SGTypes';
import type { SGElement } from './parser/SGTypes';
import { SymbolTypeFlag } from './SymbolTable';
import { DynamicType } from './types';
import type { BaseFunctionType } from './types/BaseFunctionType';
import { ComponentType } from './types/ComponentType';
import { DynamicType } from './types/DynamicType';

export class XmlScope extends Scope {
constructor(
Expand Down Expand Up @@ -53,6 +53,7 @@ export class XmlScope extends Scope {
? this.symbolTable.getSymbolType(util.getSgNodeTypeName(componentElement?.extends), { flags: SymbolTypeFlag.typetime, fullName: componentElement?.extends, tableProvider: () => this.symbolTable })
: undefined;
const result = new ComponentType(componentElement.name, parentComponentType as ComponentType);
result.addBuiltInInterfaces();
const iface = componentElement.interfaceElement;
if (!iface) {
return result;
Expand Down
55 changes: 54 additions & 1 deletion src/bscPlugin/validation/ScopeValidator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as sinonImport from 'sinon';
import { DiagnosticMessages } from '../../DiagnosticMessages';
import { Program } from '../../Program';
import { expectDiagnostics, expectTypeToBe, expectZeroDiagnostics } from '../../testHelpers.spec';
import { expectDiagnostics, expectTypeToBe, expectZeroDiagnostics, trim } from '../../testHelpers.spec';
import { expect } from 'chai';
import type { TypeCompatibilityData } from '../../interfaces';
import { IntegerType } from '../../types/IntegerType';
Expand Down Expand Up @@ -140,6 +140,59 @@ describe('ScopeValidator', () => {
//should have no errors
expectZeroDiagnostics(program);
});

it('checks for at least the number of non-optional args on variadic (callFunc) functions', () => {
program.setFile('components/Widget.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="Widget" extends="Group">
<script uri="Widget.brs"/>
<interface>
<function name="someFunc" />
</interface>
</component>
`);
program.setFile('components/Widget.brs', `
sub someFunc(input as object)
print input
end sub
`);
program.setFile('source/util.brs', `
sub useCallFunc(input as roSGNodeWidget)
input.callFunc()
end sub
`);
program.validate();
//should have an error
expectDiagnostics(program, [
DiagnosticMessages.mismatchArgumentCount('1-32', 0)
]);
});

it('any number number of args on variadic (callFunc) functions', () => {
program.setFile('components/Widget.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="Widget" extends="Group">
<script uri="Widget.brs"/>
<interface>
<function name="someFunc" />
</interface>
</component>
`);
program.setFile('components/Widget.brs', `
sub someFunc(input as object)
print input
end sub
`);
program.setFile('source/util.brs', `
sub useCallFunc(input as roSGNodeWidget)
input.callFunc("someFunc", 1, 2, 3, {})
end sub
`);
program.validate();
//TODO: do a better job of handling callFunc() invocations!
//should have an error
expectZeroDiagnostics(program);
});
});

describe('argumentTypeMismatch', () => {
Expand Down
7 changes: 6 additions & 1 deletion src/bscPlugin/validation/ScopeValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import type { BRSComponentData } from '../../roku-types';
import type { Token } from '../../lexer/Token';
import type { Scope } from '../../Scope';
import { type AstNode, type Expression } from '../../parser/AstNode';
import type { VariableExpression, DottedGetExpression, CallExpression, BinaryExpression, UnaryExpression } from '../../parser/Expression';
import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression } from '../../parser/Expression';
import { CallExpression } from '../../parser/Expression';
import { ParseMode } from '../../parser/Parser';
import { TokenKind } from '../../lexer/TokenKind';
import { WalkMode, createVisitor } from '../../astUtils/visitors';
Expand Down Expand Up @@ -438,6 +439,10 @@ export class ScopeValidator {
minParams++;
}
}
if (funcType.isVariadic) {
// function accepts variable number of arguments
maxParams = CallExpression.MaximumArguments;
}
let expCallArgCount = expression.args.length;
if (expCallArgCount > maxParams || expCallArgCount < minParams) {
let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`;
Expand Down
Loading

0 comments on commit 725c3c6

Please sign in to comment.