diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts
index f0d8c6c3f..863a482a8 100644
--- a/src/Scope.spec.ts
+++ b/src/Scope.spec.ts
@@ -2222,6 +2222,65 @@ describe('Scope', () => {
expectZeroDiagnostics(program);
});
+ it('allows interfaces to extend components', () => {
+ program.setFile('components/MyNode.xml', trim`
+
+
+
+
+
+
+
+ `);
+
+ program.setFile(s`components/MyNode.bs`, `
+ `);
+
+ program.setFile(`source/main.bs`, `
+ interface MyIFace extends roSGNodeMyNode
+ end interface
+
+ sub foo(iface as MyIFace)
+ value = iface.myProp
+ print value
+ end sub
+ `);
+ program.validate();
+ expectZeroDiagnostics(program);
+ });
+
+ it('allows interfaces to have callfunc members from parent components', () => {
+ program.setFile('components/MyNode.xml', trim`
+
+
+
+
+
+
+
+
+ `);
+
+ program.setFile(s`components/MyNode.bs`, `
+ function someCallfunc() as integer
+ return m.top.myProp
+ end function
+ `);
+
+ program.setFile(`source/main.bs`, `
+ interface MyIFace extends roSGNodeMyNode
+ end interface
+
+
+ sub foo(iface as MyIFace)
+ value = iface@.someCallfunc()
+ print value
+ end sub
+ `);
+ program.validate();
+ expectZeroDiagnostics(program);
+ });
+
});
@@ -3717,6 +3776,46 @@ describe('Scope', () => {
expect(thingType.name).to.eq('WidgetInternal');
sourceScope.unlinkSymbolTable();
});
+
+ it('should correctly get return value of interfaces with callfunc members', () => {
+ program.setFile('components/MyNode.xml', trim`
+
+
+
+
+
+
+
+
+ `);
+
+ program.setFile(s`components/MyNode.bs`, `
+ function someCallfunc() as integer
+ return m.top.myProp
+ end function
+ `);
+
+ let utilFile = program.setFile(`source/util.bs`, `
+ interface MyIFace extends roSGNodeMyNode
+ end interface
+
+ sub foo(iface as MyIFace)
+ value = iface@.someCallfunc()
+ print value
+ end sub
+ `);
+ program.validate();
+ expectZeroDiagnostics(program);
+ const processFnScope = utilFile.getFunctionScopeAtPosition(util.createPosition(5, 31));
+ const symbolTable = processFnScope.symbolTable;
+ const opts = { flags: SymbolTypeFlag.runtime };
+ const sourceScope = program.getScopeByName('source');
+ sourceScope.linkSymbolTable();
+ const thingType = symbolTable.getSymbolType('value', opts) as IntegerType;
+
+ expectTypeToBe(thingType, IntegerType);
+ sourceScope.unlinkSymbolTable();
+ });
});
diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts
index b3720a474..9cb5af7e6 100644
--- a/src/astUtils/reflection.ts
+++ b/src/astUtils/reflection.ts
@@ -29,6 +29,7 @@ import type { UnionType } from '../types/UnionType';
import type { UninitializedType } from '../types/UninitializedType';
import type { ArrayType } from '../types/ArrayType';
import type { InheritableType } from '../types/InheritableType';
+import type { CallFuncableType } from '../types/CallFuncableType';
import { BscTypeKind } from '../types/BscTypeKind';
import type { NamespaceType } from '../types/NamespaceType';
import type { BaseFunctionType } from '../types/BaseFunctionType';
@@ -39,6 +40,7 @@ import { TokenKind } from '../lexer/TokenKind';
import type { Program } from '../Program';
import type { Project } from '../lsp/Project';
+
// File reflection
export function isBrsFile(file: BscFile | undefined): file is BrsFile {
return file?.constructor.name === 'BrsFile';
@@ -461,7 +463,11 @@ export function isAssociativeArrayType(value: any): value is AssociativeArrayTyp
return value?.kind === BscTypeKind.AssociativeArrayType;
}
export function isInheritableType(target): target is InheritableType {
- return isClassType(target) || isInterfaceType(target) || isComponentType(target);
+ return isClassType(target) || isCallFuncableType(target);
+}
+
+export function isCallFuncableType(target): target is CallFuncableType {
+ return isInterfaceType(target) || isComponentType(target);
}
export function isCallableType(target): target is BaseFunctionType {
diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts
index c19e3345c..6efc7bb27 100644
--- a/src/bscPlugin/validation/ScopeValidator.ts
+++ b/src/bscPlugin/validation/ScopeValidator.ts
@@ -1,5 +1,5 @@
import { DiagnosticTag, type Range } from 'vscode-languageserver';
-import { isAliasStatement, isAssignmentStatement, isAssociativeArrayType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassStatement, isClassType, isComponentType, isDottedGetExpression, isDynamicType, isEnumMemberType, isEnumType, isFunctionExpression, isFunctionParameterExpression, isLiteralExpression, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isObjectType, isPrimitiveType, isReferenceType, isReturnStatement, isStringTypeLike, isTypedFunctionType, isUnionType, isVariableExpression, isVoidType, isXmlScope } from '../../astUtils/reflection';
+import { isAliasStatement, isAssignmentStatement, isAssociativeArrayType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallFuncableType, isCallableType, isCallfuncExpression, isClassStatement, isClassType, isComponentType, isDottedGetExpression, isDynamicType, isEnumMemberType, isEnumType, isFunctionExpression, isFunctionParameterExpression, isLiteralExpression, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isObjectType, isPrimitiveType, isReferenceType, isReturnStatement, isStringTypeLike, isTypedFunctionType, isUnionType, isVariableExpression, isVoidType, isXmlScope } from '../../astUtils/reflection';
import type { DiagnosticInfo } from '../../DiagnosticMessages';
import { DiagnosticMessages } from '../../DiagnosticMessages';
import type { BrsFile } from '../../files/BrsFile';
@@ -475,7 +475,7 @@ export class ScopeValidator {
}
const callerType = call.callee.obj?.getType({ flags: SymbolTypeFlag.runtime });
- if (!isComponentType(callerType)) {
+ if (!isCallFuncableType(callerType)) {
return;
}
const firstArgToken = call?.args[0]?.tokens.value;
@@ -541,7 +541,7 @@ export class ScopeValidator {
if (!funcType?.isResolvable() || !isCallableType(funcType)) {
const funcName = util.getAllDottedGetPartsAsString(callee, ParseMode.BrighterScript, isCallfuncExpression(callee) ? '@.' : '.');
if (isUnionType(funcType)) {
- if (!util.isUnionOfFunctions(funcType)) {
+ if (!util.isUnionOfFunctions(funcType) && !isCallfuncExpression(callee)) {
// union of func and non func. not callable
this.addMultiScopeDiagnostic({
...DiagnosticMessages.notCallable(funcName),
diff --git a/src/types/BscType.ts b/src/types/BscType.ts
index 0938b526b..47ec9e567 100644
--- a/src/types/BscType.ts
+++ b/src/types/BscType.ts
@@ -52,15 +52,11 @@ export abstract class BscType {
throw new Error('Method not implemented.');
}
- getCallFuncMemberType(name: string, options: GetSymbolTypeOptions) {
+ getCallFuncTable(): SymbolTable {
throw new Error('Method not implemented.');
}
- getCallFuncMemberTable() {
- throw new Error('Method not implemented.');
- }
-
- getCallFuncType(name: string, options: GetSymbolTypeOptions) {
+ getCallFuncType(name: string, options: GetSymbolTypeOptions): BscType {
return undefined;
}
diff --git a/src/types/CallFuncableType.ts b/src/types/CallFuncableType.ts
new file mode 100644
index 000000000..0cc25926d
--- /dev/null
+++ b/src/types/CallFuncableType.ts
@@ -0,0 +1,107 @@
+import { SymbolTypeFlag } from '../SymbolTypeFlag';
+import type { BscSymbol, GetSymbolTypeOptions, SymbolTableProvider } from '../SymbolTable';
+import { SymbolTable } from '../SymbolTable';
+import type { BscType } from './BscType';
+import { InheritableType } from './InheritableType';
+import util from '../util';
+import { ReferenceType } from './ReferenceType';
+import type { ExtraSymbolData, TypeCompatibilityData } from '../interfaces';
+import type { BaseFunctionType } from './BaseFunctionType';
+import { isAnyReferenceType, isCallFuncableType, isPrimitiveType, isReferenceType, isTypedFunctionType } from '../astUtils/reflection';
+import { addAssociatedTypesTableAsSiblingToMemberTable } from './helpers';
+
+/**
+ * A Type to represent types that support Callfunc syntax for members, eg. `someType@.someCallfunc()`
+ */
+export abstract class CallFuncableType extends InheritableType {
+ constructor(name: string, parentType?: CallFuncableType | ReferenceType) {
+ super(name, parentType);
+ this.callFuncMemberTable = new SymbolTable(`${this.name}: CallFunc`, () => (this.parentType as CallFuncableType)?.callFuncMemberTable);
+ this.callFuncAssociatedTypesTable = new SymbolTable(`${this.name}: CallFuncAssociatedTypes`, () => (this.parentType as CallFuncableType)?.callFuncAssociatedTypesTable);
+ }
+
+ public readonly callFuncMemberTable: SymbolTable;
+ public readonly callFuncAssociatedTypesTable: SymbolTable;
+
+
+ isEqual(targetType: BscType, data: TypeCompatibilityData = {}): boolean {
+ if (this === targetType) {
+ return true;
+ }
+ if (isReferenceType(targetType)) {
+ return super.isEqual(targetType, data);
+ }
+
+ if (!isCallFuncableType(targetType)) {
+ return false;
+ }
+ return super.isEqual(targetType, data) &&
+ this.checkCompatibilityBasedOnMembers(targetType, SymbolTypeFlag.runtime, data, this.callFuncMemberTable, targetType.callFuncMemberTable) &&
+ targetType.checkCompatibilityBasedOnMembers(this, SymbolTypeFlag.runtime, data, targetType.callFuncMemberTable, this.callFuncMemberTable);
+ }
+
+
+ /**
+ * Adds a function to the call func member table
+ * Also adds any associated custom types to its own table, so they can be used through a callfunc
+ */
+ addCallFuncMember(name: string, data: ExtraSymbolData, funcType: BaseFunctionType, flags: SymbolTypeFlag, associatedTypesTableProvider?: SymbolTableProvider) {
+ const originalTypesToCheck = new Set();
+ if (isTypedFunctionType(funcType)) {
+ const paramTypes = (funcType.params ?? []).map(p => p.type);
+ for (const paramType of paramTypes) {
+ originalTypesToCheck.add(paramType);
+ }
+ }
+ if (funcType.returnType) {
+ originalTypesToCheck.add(funcType.returnType);
+ }
+ const additionalTypesToCheck = new Set();
+
+ for (const type of originalTypesToCheck) {
+ if (!type.isBuiltIn) {
+ util.getCustomTypesInSymbolTree(additionalTypesToCheck, type, (subSymbol: BscSymbol) => {
+ return !originalTypesToCheck.has(subSymbol.type);
+ });
+ }
+ }
+
+ for (const type of [...originalTypesToCheck.values(), ...additionalTypesToCheck.values()]) {
+ if (!isPrimitiveType(type) && type.isResolvable()) {
+ // This type is a reference type, but was able to be resolved here
+ // add it to the table of associated types, so it can be used through a callfunc
+ const extraData = {};
+ if (associatedTypesTableProvider) {
+ associatedTypesTableProvider().getSymbolType(type.toString(), { flags: SymbolTypeFlag.typetime, data: extraData });
+ }
+ let targetType = isAnyReferenceType(type) ? type.getTarget?.() : type;
+
+ this.callFuncAssociatedTypesTable.addSymbol(type.toString(), { ...extraData, isFromCallFunc: true }, targetType, SymbolTypeFlag.typetime);
+ }
+ }
+
+ // add this function to be available through callfunc
+ this.callFuncMemberTable.addSymbol(name, data, funcType, flags);
+ }
+
+ getCallFuncTable() {
+ return this.callFuncMemberTable;
+ }
+
+ getCallFuncType(name: string, options: GetSymbolTypeOptions) {
+ const callFuncType = this.callFuncMemberTable.getSymbolType(name, options);
+
+ if (isTypedFunctionType(callFuncType)) {
+ const typesToCheck = [...callFuncType.params.map(p => p.type), callFuncType.returnType];
+
+ for (const type of typesToCheck) {
+ addAssociatedTypesTableAsSiblingToMemberTable(type, this.callFuncAssociatedTypesTable, SymbolTypeFlag.runtime);
+ }
+ } else {
+ // return a reference type for potentially finding this in the future
+ return new ReferenceType(name, name, options.flags, () => this.callFuncMemberTable);
+ }
+
+ return callFuncType;
+ }
+}
diff --git a/src/types/ComponentType.ts b/src/types/ComponentType.ts
index 7bbe601c8..e2c46c5f9 100644
--- a/src/types/ComponentType.ts
+++ b/src/types/ComponentType.ts
@@ -1,22 +1,18 @@
-import type { BscSymbol, GetSymbolTypeOptions, SymbolTableProvider } from '../SymbolTable';
import { SymbolTypeFlag } from '../SymbolTypeFlag';
import { SymbolTable } from '../SymbolTable';
-import { isAnyReferenceType, isComponentType, isDynamicType, isInvalidType, isObjectType, isPrimitiveType, isReferenceType, isTypedFunctionType } from '../astUtils/reflection';
-import type { ExtraSymbolData, TypeCompatibilityData } from '../interfaces';
-import type { BaseFunctionType } from './BaseFunctionType';
+import { isComponentType, isDynamicType, isInvalidType, isObjectType, isReferenceType } from '../astUtils/reflection';
+import type { TypeCompatibilityData } from '../interfaces';
import type { BscType } from './BscType';
import { BscTypeKind } from './BscTypeKind';
import { BuiltInInterfaceAdder } from './BuiltInInterfaceAdder';
-import { InheritableType } from './InheritableType';
-import { addAssociatedTypesTableAsSiblingToMemberTable, isUnionTypeCompatible } from './helpers';
+import { isUnionTypeCompatible } from './helpers';
import util from '../util';
+import { CallFuncableType } from './CallFuncableType';
-export class ComponentType extends InheritableType {
+export class ComponentType extends CallFuncableType {
constructor(public name: string, superComponent?: ComponentType) {
super(name, superComponent);
- this.callFuncMemberTable = new SymbolTable(`${this.name}: CallFunc`, () => this.parentComponent?.callFuncMemberTable);
- this.callFuncAssociatedTypesTable = new SymbolTable(`${this.name}: CallFuncAssociatedTypes`, () => this.parentComponent?.callFuncAssociatedTypesTable);
}
public readonly kind = BscTypeKind.ComponentType;
@@ -121,69 +117,5 @@ export class ComponentType extends InheritableType {
}
this.hasAddedBuiltInFields = true;
}
-
- public readonly callFuncMemberTable: SymbolTable;
- public readonly callFuncAssociatedTypesTable: SymbolTable;
-
- /**
- * Adds a function to the call func member table
- * Also adds any associated custom types to its own table, so they can be used through a callfunc
- */
- addCallFuncMember(name: string, data: ExtraSymbolData, funcType: BaseFunctionType, flags: SymbolTypeFlag, associatedTypesTableProvider?: SymbolTableProvider) {
- const originalTypesToCheck = new Set();
- if (isTypedFunctionType(funcType)) {
- const paramTypes = (funcType.params ?? []).map(p => p.type);
- for (const paramType of paramTypes) {
- originalTypesToCheck.add(paramType);
- }
- }
- if (funcType.returnType) {
- originalTypesToCheck.add(funcType.returnType);
- }
- const additionalTypesToCheck = new Set();
-
- for (const type of originalTypesToCheck) {
- if (!type.isBuiltIn) {
- util.getCustomTypesInSymbolTree(additionalTypesToCheck, type, (subSymbol: BscSymbol) => {
- return !originalTypesToCheck.has(subSymbol.type);
- });
- }
- }
-
- for (const type of [...originalTypesToCheck.values(), ...additionalTypesToCheck.values()]) {
- if (!isPrimitiveType(type) && type.isResolvable()) {
- // This type is a reference type, but was able to be resolved here
- // add it to the table of associated types, so it can be used through a callfunc
- const extraData = {};
- if (associatedTypesTableProvider) {
- associatedTypesTableProvider().getSymbolType(type.toString(), { flags: SymbolTypeFlag.typetime, data: extraData });
- }
- let targetType = isAnyReferenceType(type) ? type.getTarget?.() : type;
-
- this.callFuncAssociatedTypesTable.addSymbol(type.toString(), { ...extraData, isFromCallFunc: true }, targetType, SymbolTypeFlag.typetime);
- }
- }
-
- // add this function to be available through callfunc
- this.callFuncMemberTable.addSymbol(name, data, funcType, flags);
- }
-
- getCallFuncTable() {
- return this.callFuncMemberTable;
- }
-
- getCallFuncType(name: string, options: GetSymbolTypeOptions) {
- const callFuncType = this.callFuncMemberTable.getSymbolType(name, options);
-
- if (isTypedFunctionType(callFuncType)) {
- const typesToCheck = [...callFuncType.params.map(p => p.type), callFuncType.returnType];
-
- for (const type of typesToCheck) {
- addAssociatedTypesTableAsSiblingToMemberTable(type, this.callFuncAssociatedTypesTable, SymbolTypeFlag.runtime);
- }
- }
-
- return callFuncType;
- }
}
diff --git a/src/types/InterfaceType.ts b/src/types/InterfaceType.ts
index f58564561..11f395f11 100644
--- a/src/types/InterfaceType.ts
+++ b/src/types/InterfaceType.ts
@@ -1,13 +1,13 @@
import type { TypeCompatibilityData } from '../interfaces';
import { SymbolTypeFlag } from '../SymbolTypeFlag';
-import { isDynamicType, isInterfaceType, isInvalidType, isObjectType } from '../astUtils/reflection';
+import { isCallFuncableType, isDynamicType, isInterfaceType, isInvalidType, isObjectType } from '../astUtils/reflection';
import type { BscType } from './BscType';
import { BscTypeKind } from './BscTypeKind';
-import { InheritableType } from './InheritableType';
import { isUnionTypeCompatible } from './helpers';
import type { ReferenceType } from './ReferenceType';
+import { CallFuncableType } from './CallFuncableType';
-export class InterfaceType extends InheritableType {
+export class InterfaceType extends CallFuncableType {
public constructor(
public name: string,
public readonly superInterface?: InterfaceType | ReferenceType
@@ -24,9 +24,11 @@ export class InterfaceType extends InheritableType {
isUnionTypeCompatible(this, targetType, data)) {
return true;
}
- if (isInterfaceType(targetType)) {
- return this.checkCompatibilityBasedOnMembers(targetType, SymbolTypeFlag.runtime, data);
+ if (isCallFuncableType(targetType)) {
+ return this.checkCompatibilityBasedOnMembers(targetType, SymbolTypeFlag.runtime, data) &&
+ this.checkCompatibilityBasedOnMembers(targetType, SymbolTypeFlag.runtime, data, this.callFuncMemberTable, targetType.callFuncMemberTable);
}
+
const ancestorTypes = this.getAncestorTypeList();
if (ancestorTypes?.find(ancestorType => ancestorType.isEqual(targetType))) {
return true;