From 0d9745e4ba4c05dc658609714ce2b11a7484d2d7 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 5 Dec 2025 14:32:30 -0400 Subject: [PATCH 01/17] wip --- src/CrossScopeValidator.ts | 4 +- src/astUtils/reflection.ts | 8 + src/types/BscTypeKind.ts | 5 +- src/types/BuiltInInterfaceAdder.ts | 2 + src/types/IntersectionType.ts | 266 +++++++++++++++++++++++++++++ src/types/UnionType.ts | 8 +- src/types/helpers.ts | 11 +- src/util.ts | 43 ++++- 8 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 src/types/IntersectionType.ts diff --git a/src/CrossScopeValidator.ts b/src/CrossScopeValidator.ts index 517fe30c8..00138643e 100644 --- a/src/CrossScopeValidator.ts +++ b/src/CrossScopeValidator.ts @@ -11,7 +11,7 @@ import type { ReferenceType } from './types/ReferenceType'; import { getAllRequiredSymbolNames } from './types/ReferenceType'; import type { TypeChainEntry, TypeChainProcessResult } from './interfaces'; import { BscTypeKind } from './types/BscTypeKind'; -import { getAllTypesFromUnionType } from './types/helpers'; +import { getAllTypesFromComplexType } from './types/helpers'; import type { BscType } from './types/BscType'; import type { BscFile } from './files/BscFile'; import type { ClassStatement, ConstStatement, EnumMemberStatement, EnumStatement, InterfaceStatement, NamespaceStatement } from './parser/Statement'; @@ -222,7 +222,7 @@ export class CrossScopeValidator { } if (isUnionType(symbol.typeChain[0].type) && symbol.typeChain[0].data.isInstance) { - const allUnifiedTypes = getAllTypesFromUnionType(symbol.typeChain[0].type); + const allUnifiedTypes = getAllTypesFromComplexType(symbol.typeChain[0].type); for (const unifiedType of allUnifiedTypes) { unnamespacedNameLowers.push(joinTypeChainForKey(symbol.typeChain, unifiedType)); } diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 9cb5af7e6..c21bf58a1 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -39,6 +39,7 @@ import type { AssociativeArrayType } from '../types/AssociativeArrayType'; import { TokenKind } from '../lexer/TokenKind'; import type { Program } from '../Program'; import type { Project } from '../lsp/Project'; +import type { IntersectionType } from '../types/IntersectionType'; // File reflection @@ -453,6 +454,9 @@ export function isNamespaceType(value: any): value is NamespaceType { export function isUnionType(value: any): value is UnionType { return value?.kind === BscTypeKind.UnionType; } +export function isIntersectionType(value: any): value is IntersectionType { + return value?.kind === BscTypeKind.IntersectionType; +} export function isUninitializedType(value: any): value is UninitializedType { return value?.kind === BscTypeKind.UninitializedType; } @@ -507,6 +511,10 @@ export function isNativeType(value: any): value is IntegerType | LongIntegerType } +export function isComplexType(value: any): value is UnionType | IntersectionType { + return isUnionType(value) || isIntersectionType(value); +} + // Literal reflection export function isLiteralInvalid(value: any): value is LiteralExpression & { type: InvalidType } { diff --git a/src/types/BscTypeKind.ts b/src/types/BscTypeKind.ts index cb511541c..26ddb32f3 100644 --- a/src/types/BscTypeKind.ts +++ b/src/types/BscTypeKind.ts @@ -1,3 +1,5 @@ +import { IntersectionType } from "./IntersectionType"; + export enum BscTypeKind { ArrayType = 'ArrayType', AssociativeArrayType = 'AssociativeArrayType', @@ -22,5 +24,6 @@ export enum BscTypeKind { StringType = 'StringType', UninitializedType = 'UninitializedType', UnionType = 'UnionType', - VoidType = 'VoidType' + VoidType = 'VoidType', + IntersectionType = 'IntersectionType' } diff --git a/src/types/BuiltInInterfaceAdder.ts b/src/types/BuiltInInterfaceAdder.ts index ac37bc98b..41e12796c 100644 --- a/src/types/BuiltInInterfaceAdder.ts +++ b/src/types/BuiltInInterfaceAdder.ts @@ -10,6 +10,7 @@ import type { ComponentType } from './ComponentType'; import { util } from '../util'; import type { UnionType } from './UnionType'; import type { ExtraSymbolData } from '../interfaces'; +import type { IntersectionType } from './IntersectionType'; export interface BuiltInInterfaceOverride { @@ -29,6 +30,7 @@ export class BuiltInInterfaceAdder { static readonly primitiveTypeInstanceCache = new Cache(); static typedFunctionFactory: (type: BscType) => TypedFunctionType; + static intersectionTypeFactory: (types: BscType[]) => IntersectionType; static unionTypeFactory: (types: BscType[]) => UnionType; static getLookupTable: () => SymbolTable; diff --git a/src/types/IntersectionType.ts b/src/types/IntersectionType.ts new file mode 100644 index 000000000..7718d20d5 --- /dev/null +++ b/src/types/IntersectionType.ts @@ -0,0 +1,266 @@ +import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; +import { isDynamicType, isIntersectionType, isObjectType, isTypedFunctionType, isUnionType } from '../astUtils/reflection'; +import { BscType } from './BscType'; +import { ReferenceType } from './ReferenceType'; +import { addAssociatedTypesTableAsSiblingToMemberTable, findTypeUnion, findTypeUnionDeepCheck, getAllTypesFromComplexType, getUniqueType, isEnumTypeCompatible, reduceTypesToMostGeneric } from './helpers'; +import { BscTypeKind } from './BscTypeKind'; +import type { TypeCacheEntry } from '../SymbolTable'; +import { SymbolTable } from '../SymbolTable'; +import { SymbolTypeFlag } from '../SymbolTypeFlag'; +import { BuiltInInterfaceAdder } from './BuiltInInterfaceAdder'; +import { util } from '../util'; +import { unionTypeFactory } from './UnionType'; + +export function intersectionTypeFactory(types: BscType[]) { + return new IntersectionType(types); +} + +export class IntersectionType extends BscType { + constructor( + public types: BscType[] + ) { + super(joinTypesString(types)); + this.callFuncAssociatedTypesTable = new SymbolTable(`Intersection: CallFuncAssociatedTypes`); + } + + public readonly kind = BscTypeKind.IntersectionType; + + public readonly callFuncAssociatedTypesTable: SymbolTable; + + public addType(type: BscType) { + this.types.push(type); + } + + isResolvable(): boolean { + for (const type of this.types) { + if (!type.isResolvable()) { + return false; + } + } + return true; + } + + private getMemberTypeFromInnerTypes(name: string, options: GetTypeOptions): BscType { + const typeFromMembers = this.types.map((innerType) => innerType?.getMemberType(name, options)).filter(t => t !== undefined); + + if (typeFromMembers.length === 0) { + return undefined; + } else if (typeFromMembers.length === 1) { + return typeFromMembers[0]; + } + return new IntersectionType(typeFromMembers); + } + + private getCallFuncFromInnerTypes(name: string, options: GetTypeOptions): BscType { + const typeFromMembers = this.types.map((innerType) => innerType?.getCallFuncType(name, options)).filter(t => t !== undefined); + + if (typeFromMembers.length === 0) { + return undefined; + } else if (typeFromMembers.length === 1) { + return typeFromMembers[0]; + } + return new IntersectionType(typeFromMembers); + } + + getMemberType(name: string, options: GetTypeOptions) { + const innerTypesMemberType = this.getMemberTypeFromInnerTypes(name, options); + if (!innerTypesMemberType) { + // We don't have any members of any inner types that match + // so instead, create reference type that will + return new ReferenceType(name, name, options.flags, () => { + return { + name: `IntersectionType MemberTable: '${this.__identifier}'`, + getSymbolType: (innerName: string, innerOptions: GetTypeOptions) => { + const referenceTypeInnerMemberTypes = this.getMemberTypeFromInnerTypes(name, options); + if (!referenceTypeInnerMemberTypes) { + return undefined; + } + return referenceTypeInnerMemberTypes; + }, + setCachedType: (innerName: string, innerCacheEntry: TypeCacheEntry, innerOptions: GetTypeOptions) => { + // TODO: is this even cachable? This is a NO-OP for now, and it shouldn't hurt anything + }, + addSibling: (symbolTable: SymbolTable) => { + // TODO: I don't know what this means in this context? + } + }; + }); + } + return innerTypesMemberType; + } + + getCallFuncType(name: string, options: GetTypeOptions) { + const resultCallFuncType = this.getCallFuncFromInnerTypes(name, options); + if (!resultCallFuncType) { + // We don't have any members of any inner types that match + // so instead, create reference type that will + return new ReferenceType(name, name, options.flags, () => { + return { + name: `IntersectionType CallFunc MemberTable: '${this.__identifier}'`, + getSymbolType: (innerName: string, innerOptions: GetTypeOptions) => { + const referenceTypeInnerMemberType = this.getCallFuncFromInnerTypes(name, options); + if (!referenceTypeInnerMemberType) { + return undefined; + } + return referenceTypeInnerMemberType; + }, + setCachedType: (innerName: string, innerCacheEntry: TypeCacheEntry, innerOptions: GetTypeOptions) => { + // TODO: is this even cachable? This is a NO-OP for now, and it shouldn't hurt anything + }, + addSibling: (symbolTable: SymbolTable) => { + // TODO: I don't know what this means in this context? + } + }; + }); + } + + if (isTypedFunctionType(resultCallFuncType)) { + const typesToCheck = [...resultCallFuncType.params.map(p => p.type), resultCallFuncType.returnType]; + + for (const type of typesToCheck) { + addAssociatedTypesTableAsSiblingToMemberTable(type, this.callFuncAssociatedTypesTable, SymbolTypeFlag.runtime); + } + } + return resultCallFuncType; + } + + get returnType() { + return util.getReturnTypeOfIntersectionOfFunctions(this); + } + + + isTypeCompatible(targetType: BscType, data?: TypeCompatibilityData): boolean { + if (isDynamicType(targetType) || isObjectType(targetType) || this === targetType) { + return true; + } + if (isEnumTypeCompatible(this, targetType, data)) { + return true; + } + if (isUnionType(targetType)) { + // check if this set of inner types is a SUPERSET of targetTypes's inner types + for (const targetInnerType of targetType.types) { + if (!this.isTypeCompatible(targetInnerType, data)) { + return false; + } + } + return true; + } + for (const innerType of this.types) { + const foundCompatibleInnerType = innerType.isTypeCompatible(targetType, data); + if (foundCompatibleInnerType) { + return true; + } + } + + return false; + } + toString(): string { + return joinTypesString(this.types); + } + + /** + * Used for transpilation + */ + toTypeString(): string { + const uniqueTypeStrings = new Set(getAllTypesFromComplexType(this).map(t => t.toTypeString())); + + if (uniqueTypeStrings.size === 1) { + return uniqueTypeStrings.values().next().value; + } + return 'dynamic'; + } + + checkAllMemberTypes(predicate: (BscType) => boolean) { + return this.types.reduce((acc, type) => { + return acc && predicate(type); + }, true); + } + + isEqual(targetType: BscType): boolean { + if (!isIntersectionType(targetType)) { + return false; + } + if (this === targetType) { + return true; + } + return this.isTypeCompatible(targetType) && targetType.isTypeCompatible(this); + } + + getMemberTable(): SymbolTable { + const intersectionTable = new SymbolTable(this.__identifier + ' IntersectionTable'); + + for (const type of this.types) { + type.addBuiltInInterfaces(); + for (const symbol of type.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime)) { + const foundType = this.getMemberTypeFromInnerTypes(symbol.name, { flags: SymbolTypeFlag.runtime }); + const allResolvableTypes = foundType.reduce((acc, curType) => { + return acc && curType?.isResolvable(); + }, true); + + if (!allResolvableTypes) { + continue; + } + const uniqueType = getUniqueType(findTypeUnion(foundType), unionTypeFactory); + intersectionTable.addSymbol(symbol.name, {}, uniqueType, SymbolTypeFlag.runtime); + } + } + const firstType = this.types[0]; + if (!firstType) { + return intersectionTable; + } + firstType.addBuiltInInterfaces(); + for (const symbol of firstType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime)) { + const foundType = this.getMemberTypeFromInnerTypes(symbol.name, { flags: SymbolTypeFlag.runtime }); + const allResolvableTypes = foundType.reduce((acc, curType) => { + return acc && curType?.isResolvable(); + }, true); + + if (!allResolvableTypes) { + continue; + } + const uniqueType = getUniqueType(findTypeUnion(foundType), unionTypeFactory); + unionTable.addSymbol(symbol.name, {}, uniqueType, SymbolTypeFlag.runtime); + } + return unionTable; + } +} + + +function joinTypesString(types: BscType[]) { + return [...new Set(types.map(t => t.toString()))].join(' and '); +} + +BuiltInInterfaceAdder.intersectionTypeFactory = (types: BscType[]) => { + return new IntersectionType(types); +}; + + + +interface iface1 { + name: string; + age: number; + address: string; +} + +interface ifaceWrapper1 { + y: iface1; +} + +interface iface2 { + name: string; + shoeSize: number; +} + +interface ifaceWrapper2 { + y: iface2; +} + +type combined = ifaceWrapper1 & ifaceWrapper2; + + +function foo(param: combined) { + param.y.name; // valid + param.y.age; // valid + param.y.shoeSize; // valid + param.y.address; // valid +} \ No newline at end of file diff --git a/src/types/UnionType.ts b/src/types/UnionType.ts index 2ba2068f5..bcc65e822 100644 --- a/src/types/UnionType.ts +++ b/src/types/UnionType.ts @@ -2,7 +2,7 @@ import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; import { isDynamicType, isObjectType, isTypedFunctionType, isUnionType } from '../astUtils/reflection'; import { BscType } from './BscType'; import { ReferenceType } from './ReferenceType'; -import { addAssociatedTypesTableAsSiblingToMemberTable, findTypeUnion, findTypeUnionDeepCheck, getAllTypesFromUnionType, getUniqueType, isEnumTypeCompatible } from './helpers'; +import { addAssociatedTypesTableAsSiblingToMemberTable, findTypeUnion, findTypeUnionDeepCheck, getAllTypesFromComplexType, getUniqueType, isEnumTypeCompatible } from './helpers'; import { BscTypeKind } from './BscTypeKind'; import type { TypeCacheEntry } from '../SymbolTable'; import { SymbolTable } from '../SymbolTable'; @@ -57,7 +57,7 @@ export class UnionType extends BscType { name: `UnionType MemberTable: '${this.__identifier}'`, getSymbolType: (innerName: string, innerOptions: GetTypeOptions) => { const referenceTypeInnerMemberTypes = this.getMemberTypeFromInnerTypes(name, options); - if (!innerTypesMemberTypes || innerTypesMemberTypes.includes(undefined)) { + if (!referenceTypeInnerMemberTypes || referenceTypeInnerMemberTypes.includes(undefined)) { return undefined; } return getUniqueType(findTypeUnion(referenceTypeInnerMemberTypes), unionTypeFactory); @@ -84,7 +84,7 @@ export class UnionType extends BscType { name: `UnionType CallFunc MemberTable: '${this.__identifier}'`, getSymbolType: (innerName: string, innerOptions: GetTypeOptions) => { const referenceTypeInnerMemberTypes = this.getCallFuncFromInnerTypes(name, options); - if (!innerTypesMemberTypes || innerTypesMemberTypes.includes(undefined)) { + if (!referenceTypeInnerMemberTypes || referenceTypeInnerMemberTypes.includes(undefined)) { return undefined; } return getUniqueType(findTypeUnionDeepCheck(referenceTypeInnerMemberTypes), unionTypeFactory); @@ -149,7 +149,7 @@ export class UnionType extends BscType { * Used for transpilation */ toTypeString(): string { - const uniqueTypeStrings = new Set(getAllTypesFromUnionType(this).map(t => t.toTypeString())); + const uniqueTypeStrings = new Set(getAllTypesFromComplexType(this).map(t => t.toTypeString())); if (uniqueTypeStrings.size === 1) { return uniqueTypeStrings.values().next().value; diff --git a/src/types/helpers.ts b/src/types/helpers.ts index 152669cfb..d1ffa31da 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -1,9 +1,10 @@ import type { TypeCompatibilityData } from '../interfaces'; -import { isAnyReferenceType, isArrayDefaultTypeReferenceType, isDynamicType, isEnumMemberType, isEnumType, isInheritableType, isInterfaceType, isReferenceType, isTypePropertyReferenceType, isUnionType, isVoidType } from '../astUtils/reflection'; +import { isAnyReferenceType, isArrayDefaultTypeReferenceType, isComplexType, isDynamicType, isEnumMemberType, isEnumType, isInheritableType, isInterfaceType, isReferenceType, isTypePropertyReferenceType, isUnionType, isVoidType } from '../astUtils/reflection'; import type { BscType } from './BscType'; import type { UnionType } from './UnionType'; import type { SymbolTable } from '../SymbolTable'; import type { SymbolTypeFlag } from '../SymbolTypeFlag'; +import type { IntersectionType } from './IntersectionType'; export function findTypeIntersection(typesArr1: BscType[], typesArr2: BscType[]) { if (!typesArr1 || !typesArr2) { @@ -202,12 +203,12 @@ export function isNativeInterfaceCompatibleNumber(thisType: BscType, otherType: return false; } -export function getAllTypesFromUnionType(union: UnionType): BscType[] { +export function getAllTypesFromComplexType(complex: UnionType | IntersectionType): BscType[] { const results = []; - for (const type of union.types) { - if (isUnionType(type)) { - results.push(...getAllTypesFromUnionType(type)); + for (const type of complex.types) { + if (isComplexType(type)) { + results.push(...getAllTypesFromComplexType(type)); } else { results.push(type); } diff --git a/src/util.ts b/src/util.ts index 632ca2b47..e129283a7 100644 --- a/src/util.ts +++ b/src/util.ts @@ -25,7 +25,7 @@ import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionP import { LogLevel, createLogger } from './logging'; import { isToken, type Identifier, type Token } from './lexer/Token'; import { TokenKind } from './lexer/TokenKind'; -import { isAnyReferenceType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isComponentType, isDottedGetExpression, isDoubleTypeLike, isDynamicType, isEnumMemberType, isExpression, isFloatTypeLike, isIndexedGetExpression, isIntegerTypeLike, isInvalidTypeLike, isLiteralString, isLongIntegerTypeLike, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isObjectType, isPrimitiveType, isReferenceType, isStatement, isStringTypeLike, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection'; +import { isAnyReferenceType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isComponentType, isDottedGetExpression, isDoubleTypeLike, isDynamicType, isEnumMemberType, isExpression, isFloatTypeLike, isIndexedGetExpression, isIntegerTypeLike, isIntersectionType, isInvalidTypeLike, isLiteralString, isLongIntegerTypeLike, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isObjectType, isPrimitiveType, isReferenceType, isStatement, isStringTypeLike, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection'; import { WalkMode } from './astUtils/visitors'; import { SourceNode } from 'source-map'; import * as requireRelative from 'require-relative'; @@ -51,6 +51,7 @@ import type { NamespaceType } from './types/NamespaceType'; import { getUniqueType } from './types/helpers'; import { InvalidType } from './types/InvalidType'; import { TypedFunctionType } from './types'; +import { IntersectionType } from './types/IntersectionType'; export class Util { public clearConsole() { @@ -2529,6 +2530,14 @@ export class Util { return false; } + public isIntersectionOfFunctions(type: BscType, allowReferenceTypes = false): type is IntersectionType { + if (isIntersectionType(type)) { + const callablesInUnion = type.types.filter(t => isCallableType(t) || (allowReferenceTypes && isReferenceType(t))); + return callablesInUnion.length === type.types.length && callablesInUnion.length > 0; + } + return false; + } + public getFunctionTypeFromUnion(type: BscType): BscType { if (this.isUnionOfFunctions(type)) { const typedFuncsInUnion = type.types.filter(isTypedFunctionType); @@ -2548,6 +2557,25 @@ export class Util { return undefined; } + public getFunctionTypeFromIntersection(type: BscType): BscType { + if (this.isIntersectionOfFunctions(type)) { + const typedFuncsInUnion = type.types.filter(isTypedFunctionType); + if (typedFuncsInUnion.length < type.types.length) { + // has non-typedFuncs in union + return FunctionType.instance; + } + const exampleFunc = typedFuncsInUnion[0]; + const cumulativeFunction = new TypedFunctionType(getUniqueType(typedFuncsInUnion.map(f => f.returnType), (types) => new IntersectionType(types))) + .setName(exampleFunc.name) + .setSub(exampleFunc.isSub); + for (const param of exampleFunc.params) { + cumulativeFunction.addParameter(param.name, param.type, param.isOptional); + } + return cumulativeFunction; + } + return undefined; + } + public getReturnTypeOfUnionOfFunctions(type: UnionType): BscType { if (this.isUnionOfFunctions(type, true)) { const typedFuncsInUnion = type.types.filter(t => isTypedFunctionType(t) || isReferenceType(t)) as TypedFunctionType[]; @@ -2561,6 +2589,19 @@ export class Util { return InvalidType.instance; } + public getReturnTypeOfIntersectionOfFunctions(type: IntersectionType): BscType { + if (this.isIntersectionOfFunctions(type, true)) { + const typedFuncsInUnion = type.types.filter(t => isTypedFunctionType(t) || isReferenceType(t)) as TypedFunctionType[]; + if (typedFuncsInUnion.length < type.types.length) { + // is non-typedFuncs in union + return DynamicType.instance; + } + const funcReturns = typedFuncsInUnion.map(f => f.returnType); + return new IntersectionType(funcReturns);//getUniqueType(funcReturns, (types) => new UnionType(types)); + } + return InvalidType.instance; + } + public symbolComesFromSameNode(symbolName: string, definingNode: AstNode, symbolTable: SymbolTable) { let nsData: ExtraSymbolData = {}; let foundType = symbolTable?.getSymbolType(symbolName, { flags: SymbolTypeFlag.runtime, data: nsData }); From 8fde59b611a285e86f4317988cb964626965bce5 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 15 Dec 2025 11:36:13 -0400 Subject: [PATCH 02/17] Added IntersectionType tests --- src/types/BscTypeKind.ts | 4 +- src/types/IntersectionType.spec.ts | 130 +++++++++++++++++++++++++++++ src/types/IntersectionType.ts | 97 +++++++++------------ src/types/helpers.ts | 79 ++++++++++++++++++ 4 files changed, 250 insertions(+), 60 deletions(-) create mode 100644 src/types/IntersectionType.spec.ts diff --git a/src/types/BscTypeKind.ts b/src/types/BscTypeKind.ts index 8d81ab8fb..0fbb40347 100644 --- a/src/types/BscTypeKind.ts +++ b/src/types/BscTypeKind.ts @@ -13,6 +13,7 @@ export enum BscTypeKind { TypedFunctionType = 'TypedFunctionType', IntegerType = 'IntegerType', InterfaceType = 'InterfaceType', + IntersectionType = 'IntersectionType', InvalidType = 'InvalidType', LongIntegerType = 'LongIntegerType', NamespaceType = 'NamespaceType', @@ -20,9 +21,8 @@ export enum BscTypeKind { ReferenceType = 'ReferenceType', RoFunctionType = 'RoFunctionType', StringType = 'StringType', + TypeStatementType = 'TypeStatementType', UninitializedType = 'UninitializedType', UnionType = 'UnionType', - IntersectionType = 'IntersectionType', - TypeStatementType = 'TypeStatementType', VoidType = 'VoidType' } diff --git a/src/types/IntersectionType.spec.ts b/src/types/IntersectionType.spec.ts new file mode 100644 index 000000000..0dcc89f18 --- /dev/null +++ b/src/types/IntersectionType.spec.ts @@ -0,0 +1,130 @@ +import { expect } from 'chai'; +import { StringType } from './StringType'; +import { IntegerType } from './IntegerType'; +import { IntersectionType } from './IntersectionType'; +import { FloatType } from './FloatType'; +import { InterfaceType } from './InterfaceType'; +import { SymbolTypeFlag } from '../SymbolTypeFlag'; +import { BooleanType } from './BooleanType'; +import { expectTypeToBe } from '../testHelpers.spec'; +import { isReferenceType } from '../astUtils/reflection'; +import { SymbolTable } from '../SymbolTable'; +import { ReferenceType } from './ReferenceType'; + + +describe('IntersectionType', () => { + + it('has all the members of all types', () => { + const iFace = new InterfaceType('Iface'); + iFace.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + iFace.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + const iFace2 = new InterfaceType('Iface2'); + iFace2.addMember('age', null, FloatType.instance, SymbolTypeFlag.runtime); + const addressInterfaceType = new InterfaceType('address'); + iFace2.addMember('address', null, addressInterfaceType, SymbolTypeFlag.runtime); + iFace2.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + + const myIntersection = new IntersectionType([iFace, iFace2]); + const ageType = myIntersection.getMemberType('age', { flags: SymbolTypeFlag.runtime }); + const addressType = myIntersection.getMemberType('address', { flags: SymbolTypeFlag.runtime }); + const nameType = myIntersection.getMemberType('name', { flags: SymbolTypeFlag.runtime }); + + expectTypeToBe(nameType, StringType); + + expectTypeToBe(addressType, IntersectionType); + expect((addressType as IntersectionType).types.length).to.eq(2); + expect((addressType as IntersectionType).types).to.include(addressInterfaceType); + expect((addressType as IntersectionType).types.filter(isReferenceType).length).to.equal(1); // no address in iface1, so it is reference + + expectTypeToBe(ageType, IntersectionType); + expect((ageType as IntersectionType).types.length).to.eq(2); + expect((ageType as IntersectionType).types).to.include(FloatType.instance); + expect((ageType as IntersectionType).types).to.include(IntegerType.instance); + }); + + it('can assign to a more general Intersection', () => { + const myInter = new IntersectionType([StringType.instance, FloatType.instance]); + const otheInter = new IntersectionType([FloatType.instance, StringType.instance, BooleanType.instance]); + + expect(myInter.isTypeCompatible(otheInter)).to.be.true; + expect(otheInter.isTypeCompatible(myInter)).to.be.false; + }); + + + it('will get a string representation in order given', () => { + const iFace1 = new InterfaceType('SomeIface'); + iFace1.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + iFace1.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + iFace1.addMember('height', null, FloatType.instance, SymbolTypeFlag.runtime); + const myInter = new IntersectionType([FloatType.instance, StringType.instance, BooleanType.instance, iFace1]); + + expect(myInter.toString()).to.eq('float and string and boolean and SomeIface'); + }); + + it('isResolvable if any inner type is resolvable', () => { + const refTable = new SymbolTable('test'); + const refType = new ReferenceType('SomeType', 'SomeType', SymbolTypeFlag.typetime, () => refTable); + const myInter1 = new IntersectionType([refType, StringType.instance]); + expect(myInter1.isResolvable()).to.be.true; + + const myInter2 = new IntersectionType([refType]); + expect(myInter2.isResolvable()).to.be.false; + }); + + + describe('getMemberType', () => { + it('will find the intersection of inner types', () => { + const iFace1 = new InterfaceType('iFace1'); + iFace1.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + iFace1.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + iFace1.addMember('height', null, FloatType.instance, SymbolTypeFlag.runtime); + + const iFace2 = new InterfaceType('iFace2'); + iFace2.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + iFace2.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + iFace2.addMember('height', null, StringType.instance, SymbolTypeFlag.runtime); + + const myInter = new IntersectionType([iFace1, iFace2]); + + const options = { flags: SymbolTypeFlag.runtime }; + const ageType = myInter.getMemberType('age', options); + expectTypeToBe(ageType, IntegerType); + const nameType = myInter.getMemberType('name', options); + expectTypeToBe(nameType, StringType); + const heightType = myInter.getMemberType('height', options); + expectTypeToBe(heightType, IntersectionType); + const heightTypes = (heightType as IntersectionType).types; + expect(heightTypes).to.include(FloatType.instance); + expect(heightTypes).to.include(StringType.instance); + }); + + + it('will return reference types if any inner type does not include the member', () => { + const iFace1 = new InterfaceType('iFace1'); + iFace1.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + iFace1.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + iFace1.addMember('height', null, FloatType.instance, SymbolTypeFlag.runtime); + + const iFace2 = new InterfaceType('iFace2'); + iFace2.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + iFace2.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + + const myInter = new IntersectionType([iFace1, iFace2]); + const options = { flags: SymbolTypeFlag.runtime }; + const heightType1 = myInter.getMemberType('height', options); + expectTypeToBe(heightType1, IntersectionType); + const heightTypes1 = (heightType1 as IntersectionType).types; + expect(heightTypes1.length).to.eq(2); + expectTypeToBe(heightTypes1[0], FloatType); + // height does not exist in iFace2 + expect(isReferenceType(heightTypes1[1])).to.be.true; + expect(heightTypes1[1].isResolvable()).to.be.false; + + iFace2.addMember('height', null, FloatType.instance, SymbolTypeFlag.runtime); + const heightType2 = myInter.getMemberType('height', options); + expectTypeToBe(heightType2, FloatType); + }); + }); + +}); + diff --git a/src/types/IntersectionType.ts b/src/types/IntersectionType.ts index 7718d20d5..358acc8b7 100644 --- a/src/types/IntersectionType.ts +++ b/src/types/IntersectionType.ts @@ -1,15 +1,14 @@ import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; -import { isDynamicType, isIntersectionType, isObjectType, isTypedFunctionType, isUnionType } from '../astUtils/reflection'; +import { isDynamicType, isIntersectionType, isObjectType, isTypedFunctionType } from '../astUtils/reflection'; import { BscType } from './BscType'; import { ReferenceType } from './ReferenceType'; -import { addAssociatedTypesTableAsSiblingToMemberTable, findTypeUnion, findTypeUnionDeepCheck, getAllTypesFromComplexType, getUniqueType, isEnumTypeCompatible, reduceTypesToMostGeneric } from './helpers'; +import { addAssociatedTypesTableAsSiblingToMemberTable, getAllTypesFromComplexType, isEnumTypeCompatible, reduceTypesForIntersectionType } from './helpers'; import { BscTypeKind } from './BscTypeKind'; import type { TypeCacheEntry } from '../SymbolTable'; import { SymbolTable } from '../SymbolTable'; import { SymbolTypeFlag } from '../SymbolTypeFlag'; import { BuiltInInterfaceAdder } from './BuiltInInterfaceAdder'; import { util } from '../util'; -import { unionTypeFactory } from './UnionType'; export function intersectionTypeFactory(types: BscType[]) { return new IntersectionType(types); @@ -33,26 +32,30 @@ export class IntersectionType extends BscType { isResolvable(): boolean { for (const type of this.types) { - if (!type.isResolvable()) { - return false; + // resolvable if any inner type is resolvable + if (type.isResolvable()) { + return true; } } - return true; + return false; } private getMemberTypeFromInnerTypes(name: string, options: GetTypeOptions): BscType { - const typeFromMembers = this.types.map((innerType) => innerType?.getMemberType(name, options)).filter(t => t !== undefined); + const typeFromMembers = this.types.map((innerType) => { + return innerType?.getMemberType(name, options); + }); + const filteredTypes = reduceTypesForIntersectionType(typeFromMembers.filter(t => t !== undefined)); - if (typeFromMembers.length === 0) { + if (filteredTypes.length === 0) { return undefined; - } else if (typeFromMembers.length === 1) { - return typeFromMembers[0]; + } else if (filteredTypes.length === 1) { + return filteredTypes[0]; } - return new IntersectionType(typeFromMembers); + return new IntersectionType(filteredTypes); } private getCallFuncFromInnerTypes(name: string, options: GetTypeOptions): BscType { - const typeFromMembers = this.types.map((innerType) => innerType?.getCallFuncType(name, options)).filter(t => t !== undefined); + const typeFromMembers = reduceTypesForIntersectionType(this.types.map((innerType) => innerType?.getCallFuncType(name, options)).filter(t => t !== undefined)); if (typeFromMembers.length === 0) { return undefined; @@ -136,10 +139,17 @@ export class IntersectionType extends BscType { if (isEnumTypeCompatible(this, targetType, data)) { return true; } - if (isUnionType(targetType)) { - // check if this set of inner types is a SUPERSET of targetTypes's inner types - for (const targetInnerType of targetType.types) { - if (!this.isTypeCompatible(targetInnerType, data)) { + if (isIntersectionType(targetType)) { + // check if this all the types of this type are in the target (eg, target is a super set of this types) + for (const memberType of this.types) { + let foundCompatibleInnerType = false; + for (const targetInnerType of targetType.types) { + if (memberType.isTypeCompatible(targetInnerType, data)) { + foundCompatibleInnerType = true; + continue; + } + } + if (!foundCompatibleInnerType) { return false; } } @@ -193,6 +203,8 @@ export class IntersectionType extends BscType { type.addBuiltInInterfaces(); for (const symbol of type.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime)) { const foundType = this.getMemberTypeFromInnerTypes(symbol.name, { flags: SymbolTypeFlag.runtime }); + /* + const allResolvableTypes = foundType.reduce((acc, curType) => { return acc && curType?.isResolvable(); }, true); @@ -200,8 +212,8 @@ export class IntersectionType extends BscType { if (!allResolvableTypes) { continue; } - const uniqueType = getUniqueType(findTypeUnion(foundType), unionTypeFactory); - intersectionTable.addSymbol(symbol.name, {}, uniqueType, SymbolTypeFlag.runtime); + const uniqueType = getUniqueType(findTypeUnion(foundType), intersectionTypeFactory);*/ + intersectionTable.addSymbol(symbol.name, {}, foundType, SymbolTypeFlag.runtime); } } const firstType = this.types[0]; @@ -211,17 +223,17 @@ export class IntersectionType extends BscType { firstType.addBuiltInInterfaces(); for (const symbol of firstType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime)) { const foundType = this.getMemberTypeFromInnerTypes(symbol.name, { flags: SymbolTypeFlag.runtime }); - const allResolvableTypes = foundType.reduce((acc, curType) => { - return acc && curType?.isResolvable(); - }, true); + /* const allResolvableTypes = foundType.reduce((acc, curType) => { + return acc && curType?.isResolvable(); + }, true); - if (!allResolvableTypes) { - continue; - } - const uniqueType = getUniqueType(findTypeUnion(foundType), unionTypeFactory); - unionTable.addSymbol(symbol.name, {}, uniqueType, SymbolTypeFlag.runtime); + if (!allResolvableTypes) { + continue; + } + const uniqueType = getUniqueType(findTypeUnion(foundType), unionTypeFactory);*/ + intersectionTable.addSymbol(symbol.name, {}, foundType, SymbolTypeFlag.runtime); } - return unionTable; + return intersectionTable; } } @@ -233,34 +245,3 @@ function joinTypesString(types: BscType[]) { BuiltInInterfaceAdder.intersectionTypeFactory = (types: BscType[]) => { return new IntersectionType(types); }; - - - -interface iface1 { - name: string; - age: number; - address: string; -} - -interface ifaceWrapper1 { - y: iface1; -} - -interface iface2 { - name: string; - shoeSize: number; -} - -interface ifaceWrapper2 { - y: iface2; -} - -type combined = ifaceWrapper1 & ifaceWrapper2; - - -function foo(param: combined) { - param.y.name; // valid - param.y.age; // valid - param.y.shoeSize; // valid - param.y.address; // valid -} \ No newline at end of file diff --git a/src/types/helpers.ts b/src/types/helpers.ts index d1ffa31da..37b5279b7 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -131,6 +131,85 @@ export function reduceTypesToMostGeneric(types: BscType[], allowNameEquality = t } +/** + * Reduces a list of types based on equality or inheritance + * If all types are the same - just that type is returned + * If one of the types is Dynamic, then Dynamic.instance is returned + * If any types inherit another type, the more Specific type is returned, eg. the one with the most members + * @param types array of types + * @returns an array of the most specific types + */ +export function reduceTypesForIntersectionType(types: BscType[], allowNameEquality = true): BscType[] { + if (!types || types?.length === 0) { + return undefined; + } + + if (types.length === 1) { + // only one type + return [types[0]]; + } + + types = types.map(t => { + if (isReferenceType(t) && t.isResolvable()) { + return (t as any).getTarget() ?? t; + } + return t; + }); + + // Get a list of unique types, based on the `isEqual()` method + const uniqueTypes = getUniqueTypesFromArray(types, allowNameEquality).map(t => { + // map to object with `shouldIgnore` flag + return { type: t, shouldIgnore: false }; + }); + + if (uniqueTypes.length === 1) { + // only one type after filtering + return [uniqueTypes[0].type]; + } + const existingDynamicType = uniqueTypes.find(t => !isAnyReferenceType(t.type) && isDynamicType(t.type)); + if (existingDynamicType) { + // If it includes dynamic, then the result is dynamic + return [existingDynamicType.type]; + } + const specificTypes = []; + //check assignability: + for (let i = 0; i < uniqueTypes.length; i++) { + const currentType = uniqueTypes[i].type; + if (i === uniqueTypes.length - 1) { + if (!uniqueTypes[i].shouldIgnore) { + //this type was not convertible to anything else... it is as general as possible + specificTypes.push(currentType); + } + break; + } + for (let j = i + 1; j < uniqueTypes.length; j++) { + if (uniqueTypes[j].shouldIgnore) { + continue; + } + const checkType = uniqueTypes[j].type; + + if (currentType.isResolvable() && currentType.isEqual(uniqueTypes[j].type, { allowNameEquality: allowNameEquality })) { + uniqueTypes[j].shouldIgnore = true; + } else if (isInheritableType(currentType) && isInheritableType(checkType)) { + if (checkType.isTypeDescendent(currentType)) { + //the type we're checking is more general than the current type... it can be ignored + uniqueTypes[j].shouldIgnore = true; + } + if (currentType.isTypeDescendent(checkType)) { + // the currentType is an ancestor to some other type - it won't be in the final set + break; + } + } + if (j === uniqueTypes.length - 1) { + //this type was not convertible to anything else... it is as general as possible + specificTypes.push(currentType); + } + } + } + return specificTypes; +} + + /** * Gets a Unique type from a list of types * @param types array of types From 48ac3f0c7d6d823280b8196518b19ba87b61e184 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 15 Dec 2025 16:29:44 -0400 Subject: [PATCH 03/17] Added support for grouped type expressions --- src/Scope.spec.ts | 40 +++++++++++++++ src/astUtils/reflection.ts | 2 +- src/parser/Parser.spec.ts | 95 +++++++++++++++++++++++++++++++++++ src/parser/Parser.ts | 14 ++++-- src/types/IntersectionType.ts | 25 +++++---- src/types/UnionType.ts | 28 ++++++++--- src/types/helper.spec.ts | 9 ++++ src/types/helpers.ts | 13 ++++- 8 files changed, 204 insertions(+), 22 deletions(-) diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts index 39345e1ad..7879eebac 100644 --- a/src/Scope.spec.ts +++ b/src/Scope.spec.ts @@ -2837,6 +2837,46 @@ describe('Scope', () => { expect(resultType.types.map(t => t.toString())).includes(IntegerType.instance.toString()); }); + it('should handle union types grouped', () => { + const mainFile = program.setFile('source/main.bs', ` + sub nestedUnion(thing as (Person or Pet) or (Vehicle or Duck) + id = thing.id + print id + end sub + + sub takesIntOrString(x as (integer or string)) + print x + end sub + + class Person + id as integer + end class + + class Pet + id as integer + end class + + class Vehicle + id as string + end class + + class Duck + id as string + end class + `); + program.validate(); + expectZeroDiagnostics(program); + const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const sourceScope = program.getScopeByName('source'); + expect(sourceScope).to.exist; + sourceScope.linkSymbolTable(); + expect(mainFnScope).to.exist; + const mainSymbolTable = mainFnScope.symbolTable; + const idType = mainSymbolTable.getSymbolType('id', { flags: SymbolTypeFlag.runtime }) as UnionType; + expectTypeToBe(idType, UnionType); + expect(idType.types).includes(StringType.instance); + expect(idType.types).includes(IntegerType.instance); + }); }); describe('type casts', () => { diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index da68f1520..94edbe37a 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -479,7 +479,7 @@ export function isTypeStatementType(value: any): value is TypeStatementType { } export function isInheritableType(target): target is InheritableType { - return isClassType(target) || isCallFuncableType(target) || isComplexTypeOf(target, isInheritableType); + return isClassType(target) || isCallFuncableType(target); } export function isCallFuncableType(target): target is CallFuncableType { diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index 29b84f7ab..df7d9c9f8 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -1514,6 +1514,66 @@ describe('parser', () => { }); }); + describe('grouped type expressions', () => { + it('is not allowed in brightscript mode', () => { + let parser = parse(` + sub main(param as (string or integer)) + print param + end sub + `, ParseMode.BrightScript); + expectDiagnosticsIncludes(parser.diagnostics, [DiagnosticMessages.expectedStatement()]); + }); + + it('allows group type expressions in parameters', () => { + let { diagnostics } = parse(` + sub main(param as (string or integer)) + print param + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('allows group type expressions in type casts', () => { + let { diagnostics } = parse(` + sub main(val) + printThing(val as (string or integer)) + end sub + sub printThing(thing as (string or integer)) + print thing + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('allows union of grouped type expressions', () => { + let { diagnostics } = parse(` + sub main(param as (string or integer) or (float or dynamic)) + print param + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('allows nested grouped type expressions', () => { + let { diagnostics } = parse(` + sub main(param as ((string or integer) or (float or dynamic))) + print param + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + + it('allows complicated grouped type expression', () => { + let { diagnostics } = parse(` + sub main(param as (({name as string} and {age as integer}) or (string and SomeInterface) or Klass and roAssociativeArray) ) + print param + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + }); + describe('union types', () => { it('is not allowed in brightscript mode', () => { @@ -1547,6 +1607,41 @@ describe('parser', () => { }); }); + + describe('intersection types', () => { + + it('is not allowed in brightscript mode', () => { + let parser = parse(` + sub main(param as string and integer) + print param + end sub + `, ParseMode.BrightScript); + expectDiagnosticsIncludes(parser.diagnostics, [DiagnosticMessages.expectedStatement()]); + }); + + it('allows intersection types in parameters', () => { + let { diagnostics } = parse(` + sub main(param as string and integer) + print param + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('allows intersection types in type casts', () => { + let { diagnostics } = parse(` + sub main(val) + printThing(val as string and integer) + end sub + sub printThing(thing as string and integer) + print thing + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + }); + + describe('typed arrays', () => { it('is not allowed in brightscript mode', () => { diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 19856ef11..df676169e 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -415,7 +415,7 @@ export class Parser { } //consume the statement separator this.consumeStatementSeparators(); - } else if (!this.checkAny(TokenKind.Identifier, TokenKind.LeftCurlyBrace, ...DeclarableTypes, ...AllowedTypeIdentifiers)) { + } else if (!this.checkAny(TokenKind.Identifier, TokenKind.LeftCurlyBrace, TokenKind.LeftParen, ...DeclarableTypes, ...AllowedTypeIdentifiers)) { if (!ignoreDiagnostics) { this.diagnostics.push({ ...DiagnosticMessages.expectedIdentifier(asToken.text), @@ -3039,7 +3039,7 @@ export class Parser { const changedTokens: { token: Token; oldKind: TokenKind }[] = []; try { let expr: Expression = this.getTypeExpressionPart(changedTokens); - while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or)) { + while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or, TokenKind.And)) { // If we're in Brighterscript mode, allow union types with "or" between types // TODO: Handle Union types in parens? eg. "(string or integer)" let operator = this.previous(); @@ -3071,7 +3071,7 @@ export class Parser { * @returns an expression that was successfully parsed */ private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) { - let expr: VariableExpression | DottedGetExpression | TypedArrayExpression | InlineInterfaceExpression; + let expr: VariableExpression | DottedGetExpression | TypedArrayExpression | InlineInterfaceExpression | GroupingExpression; if (this.checkAny(...DeclarableTypes)) { // if this is just a type, just use directly @@ -3085,6 +3085,14 @@ export class Parser { if (this.match(TokenKind.LeftCurlyBrace)) { expr = this.inlineInterface(); + } else if (this.match(TokenKind.LeftParen)) { + let left = this.previous(); + let typeExpr = this.typeExpression(); + let right = this.consume( + DiagnosticMessages.unmatchedLeftToken(left.text, 'type expression'), + TokenKind.RightParen + ); + expr = new GroupingExpression({ leftParen: left, rightParen: right, expression: typeExpr }); } else { if (this.checkAny(...AllowedTypeIdentifiers)) { // Since the next token is allowed as a type identifier, change the kind diff --git a/src/types/IntersectionType.ts b/src/types/IntersectionType.ts index 358acc8b7..a8f296466 100644 --- a/src/types/IntersectionType.ts +++ b/src/types/IntersectionType.ts @@ -2,7 +2,7 @@ import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; import { isDynamicType, isIntersectionType, isObjectType, isTypedFunctionType } from '../astUtils/reflection'; import { BscType } from './BscType'; import { ReferenceType } from './ReferenceType'; -import { addAssociatedTypesTableAsSiblingToMemberTable, getAllTypesFromComplexType, isEnumTypeCompatible, reduceTypesForIntersectionType } from './helpers'; +import { addAssociatedTypesTableAsSiblingToMemberTable, getAllTypesFromComplexType, isEnumTypeCompatible, joinTypesString, reduceTypesForIntersectionType } from './helpers'; import { BscTypeKind } from './BscTypeKind'; import type { TypeCacheEntry } from '../SymbolTable'; import { SymbolTable } from '../SymbolTable'; @@ -18,7 +18,7 @@ export class IntersectionType extends BscType { constructor( public types: BscType[] ) { - super(joinTypesString(types)); + super(joinTypesString(types, 'and', BscTypeKind.IntersectionType)); this.callFuncAssociatedTypesTable = new SymbolTable(`Intersection: CallFuncAssociatedTypes`); } @@ -165,7 +165,7 @@ export class IntersectionType extends BscType { return false; } toString(): string { - return joinTypesString(this.types); + return joinTypesString(this.types, 'and', BscTypeKind.IntersectionType); } /** @@ -193,7 +193,19 @@ export class IntersectionType extends BscType { if (this === targetType) { return true; } - return this.isTypeCompatible(targetType) && targetType.isTypeCompatible(this); + for (const type of this.types) { + let foundMatch = false; + for (const targetTypeInner of targetType.types) { + if (type.isEqual(targetTypeInner)) { + foundMatch = true; + break; + } + } + if (!foundMatch) { + return false; + } + } + return true; } getMemberTable(): SymbolTable { @@ -237,11 +249,6 @@ export class IntersectionType extends BscType { } } - -function joinTypesString(types: BscType[]) { - return [...new Set(types.map(t => t.toString()))].join(' and '); -} - BuiltInInterfaceAdder.intersectionTypeFactory = (types: BscType[]) => { return new IntersectionType(types); }; diff --git a/src/types/UnionType.ts b/src/types/UnionType.ts index bcc65e822..91e620bbb 100644 --- a/src/types/UnionType.ts +++ b/src/types/UnionType.ts @@ -2,7 +2,7 @@ import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; import { isDynamicType, isObjectType, isTypedFunctionType, isUnionType } from '../astUtils/reflection'; import { BscType } from './BscType'; import { ReferenceType } from './ReferenceType'; -import { addAssociatedTypesTableAsSiblingToMemberTable, findTypeUnion, findTypeUnionDeepCheck, getAllTypesFromComplexType, getUniqueType, isEnumTypeCompatible } from './helpers'; +import { addAssociatedTypesTableAsSiblingToMemberTable, findTypeUnion, findTypeUnionDeepCheck, getAllTypesFromComplexType, getUniqueType, isEnumTypeCompatible, joinTypesString } from './helpers'; import { BscTypeKind } from './BscTypeKind'; import type { TypeCacheEntry } from '../SymbolTable'; import { SymbolTable } from '../SymbolTable'; @@ -18,7 +18,7 @@ export class UnionType extends BscType { constructor( public types: BscType[] ) { - super(joinTypesString(types)); + super(joinTypesString(types, 'or', BscTypeKind.UnionType)); this.callFuncAssociatedTypesTable = new SymbolTable(`Union: CallFuncAssociatedTypes`); } @@ -142,7 +142,7 @@ export class UnionType extends BscType { return false; } toString(): string { - return joinTypesString(this.types); + return joinTypesString(this.types, 'or', BscTypeKind.UnionType); } /** @@ -170,7 +170,23 @@ export class UnionType extends BscType { if (this === targetType) { return true; } - return this.isTypeCompatible(targetType) && targetType.isTypeCompatible(this); + + if (this.types.length !== targetType.types.length) { + return false; + } + for (const type of this.types) { + let foundMatch = false; + for (const targetTypeInner of targetType.types) { + if (type.isEqual(targetTypeInner)) { + foundMatch = true; + break; + } + } + if (!foundMatch) { + return false; + } + } + return true; } getMemberTable(): SymbolTable { @@ -197,10 +213,6 @@ export class UnionType extends BscType { } -function joinTypesString(types: BscType[]) { - return [...new Set(types.map(t => t.toString()))].join(' or '); -} - BuiltInInterfaceAdder.unionTypeFactory = (types: BscType[]) => { return new UnionType(types); }; diff --git a/src/types/helper.spec.ts b/src/types/helper.spec.ts index e0d250eec..69c4d40e8 100644 --- a/src/types/helper.spec.ts +++ b/src/types/helper.spec.ts @@ -196,4 +196,13 @@ describe('reduceTypesToMostGeneric', () => { const genericTypes = reduceTypesToMostGeneric([retRefType1, retRefType2]); expect(genericTypes.length).to.eq(2); }); + + it('should handle unions of unions', () => { + const union1 = new UnionType([IntegerType.instance, StringType.instance]); + const union2 = new UnionType([StringType.instance, FloatType.instance]); + const genericTypes = reduceTypesToMostGeneric([union1, union2]); + expect(genericTypes.length).to.eq(2); + expect(genericTypes).to.include(union1); + expect(genericTypes).to.include(union2); + }); }); diff --git a/src/types/helpers.ts b/src/types/helpers.ts index 37b5279b7..e00f0ffd5 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -1,10 +1,11 @@ import type { TypeCompatibilityData } from '../interfaces'; -import { isAnyReferenceType, isArrayDefaultTypeReferenceType, isComplexType, isDynamicType, isEnumMemberType, isEnumType, isInheritableType, isInterfaceType, isReferenceType, isTypePropertyReferenceType, isUnionType, isVoidType } from '../astUtils/reflection'; +import { isAnyReferenceType, isArrayDefaultTypeReferenceType, isComplexType, isDynamicType, isEnumMemberType, isEnumType, isInheritableType, isInterfaceType, isIntersectionType, isReferenceType, isTypePropertyReferenceType, isUnionType, isVoidType } from '../astUtils/reflection'; import type { BscType } from './BscType'; import type { UnionType } from './UnionType'; import type { SymbolTable } from '../SymbolTable'; import type { SymbolTypeFlag } from '../SymbolTypeFlag'; import type { IntersectionType } from './IntersectionType'; +import type { BscTypeKind } from './BscTypeKind'; export function findTypeIntersection(typesArr1: BscType[], typesArr2: BscType[]) { if (!typesArr1 || !typesArr2) { @@ -314,3 +315,13 @@ export function addAssociatedTypesTableAsSiblingToMemberTable(type: BscType, ass * A map of all types created in the program during its lifetime. This applies across all programs, validate runs, etc. Mostly useful for a single run to track types created. */ export const TypesCreated: Record = {}; + +export function joinTypesString(types: BscType[], separator: string, thisTypeKind: BscTypeKind): string { + return [...new Set(types.map(t => { + const typeString = t.toString(); + if ((isUnionType(t) || isIntersectionType(t)) && t.kind !== thisTypeKind) { + return `(${typeString})`; + } + return t.toString(); + }))].join(` ${separator} `); +} From 723b945960517547d65f0af22586ef79032725d9 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Wed, 17 Dec 2025 09:49:10 -0400 Subject: [PATCH 04/17] Adding tests for member types of intersection types in scope contexts --- src/Scope.spec.ts | 164 +++++++++++++++++++++++++++++ src/interfaces.ts | 4 + src/parser/Expression.ts | 3 + src/types/AssociativeArrayType.ts | 6 +- src/types/InheritableType.ts | 2 +- src/types/IntersectionType.spec.ts | 75 ++++++++++--- src/types/IntersectionType.ts | 8 +- src/types/helpers.ts | 10 +- src/util.ts | 2 +- 9 files changed, 250 insertions(+), 24 deletions(-) diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts index 7bf32a8a7..d2a82c744 100644 --- a/src/Scope.spec.ts +++ b/src/Scope.spec.ts @@ -31,6 +31,7 @@ import { ObjectType } from './types'; import undent from 'undent'; import * as fsExtra from 'fs-extra'; import { InlineInterfaceType } from './types/InlineInterfaceType'; +import { IntersectionType } from './types/IntersectionType'; describe('Scope', () => { let sinon = sinonImport.createSandbox(); @@ -2879,6 +2880,169 @@ describe('Scope', () => { }); }); + describe('intersection types', () => { + + it('should create intersection types', () => { + const mainFile = program.setFile('source/main.bs', ` + sub printName(thing as Person and Pet) + name = thing.name + print name + legs = thing.legs + print legs + isAdult = thing.isAdult + print isAdult + end sub + + class Person + name as string + isAdult as boolean + end class + + class Pet + name as string + legs as integer + end class + `); + program.validate(); + expectZeroDiagnostics(program); + const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const sourceScope = program.getScopeByName('source'); + expect(sourceScope).to.exist; + sourceScope.linkSymbolTable(); + expect(mainFnScope).to.exist; + const mainSymbolTable = mainFnScope.symbolTable; + const thingType = mainSymbolTable.getSymbolType('thing', { flags: SymbolTypeFlag.runtime }) as IntersectionType; + expectTypeToBe(thingType, IntersectionType); + expect(thingType.types).to.have.lengthOf(2); + expect(thingType.types).to.satisfy((types) => { + return types.some(t => t.toString() === 'Person') && + types.some(t => t.toString() === 'Pet'); + }); + const nameType = mainSymbolTable.getSymbolType('name', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(nameType, StringType); + const legsType = mainSymbolTable.getSymbolType('legs', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(legsType, IntegerType); + const isAdultType = mainSymbolTable.getSymbolType('isAdult', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(isAdultType, BooleanType); + }); + + + it('should allow intersection of types in namespaces', () => { + const mainFile = program.setFile('source/main.bs', ` + sub printData(thing as NamespaceA.TypeA and NamespaceB.TypeB) + dataA = thing.dataA + print dataA + dataB = thing.dataB + print dataB + end sub + + namespace NamespaceA + class TypeA + dataA as string + end class + end namespace + + namespace NamespaceB + class TypeB + dataB as integer + end class + end namespace + `); + program.validate(); + expectZeroDiagnostics(program); + const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const sourceScope = program.getScopeByName('source'); + expect(sourceScope).to.exist; + sourceScope.linkSymbolTable(); + expect(mainFnScope).to.exist; + const mainSymbolTable = mainFnScope.symbolTable; + const thingType = mainSymbolTable.getSymbolType('thing', { flags: SymbolTypeFlag.runtime }) as IntersectionType; + expectTypeToBe(thingType, IntersectionType); + expect(thingType.types).to.have.lengthOf(2); + expect(thingType.types).to.satisfy((types) => { + return types.some(t => t.toString() === 'NamespaceA.TypeA') && + types.some(t => t.toString() === 'NamespaceB.TypeB'); + }); + const dataAType = mainSymbolTable.getSymbolType('dataA', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(dataAType, StringType); + const dataBType = mainSymbolTable.getSymbolType('dataB', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(dataBType, IntegerType); + }); + + it('should allow intersections with types from another file', () => { + const mainFile = program.setFile('source/main.bs', ` + sub printInfo(thing as Person and Pet) + name = thing.name + print name + legs = thing.legs + print legs + end sub + `); + program.setFile('source/types.bs', ` + class Person + name as string + end class + + class Pet + legs as integer + end class + `); + program.validate(); + expectZeroDiagnostics(program); + const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const sourceScope = program.getScopeByName('source'); + expect(sourceScope).to.exist; + sourceScope.linkSymbolTable(); + expect(mainFnScope).to.exist; + const mainSymbolTable = mainFnScope.symbolTable; + const thingType = mainSymbolTable.getSymbolType('thing', { flags: SymbolTypeFlag.runtime }) as IntersectionType; + expectTypeToBe(thingType, IntersectionType); + expect(thingType.types).to.have.lengthOf(2); + expect(thingType.types).to.satisfy((types) => { + return types.some(t => t.toString() === 'Person') && + types.some(t => t.toString() === 'Pet'); + }); + const nameType = mainSymbolTable.getSymbolType('name', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(nameType, StringType); + const legsType = mainSymbolTable.getSymbolType('legs', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(legsType, IntegerType); + }); + + it('allows a type intersection with a built in type', () => { + const mainFile = program.setFile('source/main.bs', ` + sub printStringInfo(data as MyKlass and roAssociativeArray) + x = data.customData + print x + y = data.count() + print y + end sub + + class MyKlass + customData as string + end class + `); + program.validate(); + expectZeroDiagnostics(program); + const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const sourceScope = program.getScopeByName('source'); + expect(sourceScope).to.exist; + sourceScope.linkSymbolTable(); + expect(mainFnScope).to.exist; + const mainSymbolTable = mainFnScope.symbolTable; + const dataType = mainSymbolTable.getSymbolType('data', { flags: SymbolTypeFlag.runtime }) as IntersectionType; + expectTypeToBe(dataType, IntersectionType); + expect(dataType.types).to.have.lengthOf(2); + expect(dataType.types).to.satisfy((types) => { + return types.some(t => t.toString() === 'MyKlass') && + types.some(t => t.toString() === 'roAssociativeArray'); + }); + const customDataType = mainSymbolTable.getSymbolType('x', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(customDataType, StringType); + const countType = mainSymbolTable.getSymbolType('y', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(countType, IntegerType); + }); + }); + describe('type casts', () => { it('should use type casts to determine the types of symbols', () => { const mainFile = program.setFile('source/main.bs', ` diff --git a/src/interfaces.ts b/src/interfaces.ts index aa74220b7..97a278791 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1042,6 +1042,10 @@ export interface GetTypeOptions { */ statementIndex?: number | 'end'; ignoreParentTables?: boolean; + /** + * If this is true, AA's do not return dynamic if no member is found + */ + ignoreAADefaultDynamicMembers?: boolean; } export class TypeChainEntry { diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index d6792c9b0..85b1c93ef 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -35,6 +35,7 @@ import { FunctionType } from '../types/FunctionType'; import type { BaseFunctionType } from '../types/BaseFunctionType'; import { brsDocParser } from './BrightScriptDocParser'; import { InlineInterfaceType } from '../types/InlineInterfaceType'; +import { IntersectionType } from '../types/IntersectionType'; export type ExpressionVisitor = (expression: Expression, parent: Expression) => void; @@ -88,6 +89,8 @@ export class BinaryExpression extends Expression { switch (operatorKind) { case TokenKind.Or: return new UnionType([this.left.getType(options), this.right.getType(options)]); + case TokenKind.And: + return new IntersectionType([this.left.getType(options), this.right.getType(options)]); //TODO: Intersection Types?, eg. case TokenKind.And: } } else if (options.flags & SymbolTypeFlag.runtime) { diff --git a/src/types/AssociativeArrayType.ts b/src/types/AssociativeArrayType.ts index 3b61ea900..eb8edf66f 100644 --- a/src/types/AssociativeArrayType.ts +++ b/src/types/AssociativeArrayType.ts @@ -41,7 +41,11 @@ export class AssociativeArrayType extends BscType { getMemberType(name: string, options: GetTypeOptions) { // if a member has specifically been added, cool. otherwise, assume dynamic - return super.getMemberType(name, options) ?? DynamicType.instance; + const memberType = super.getMemberType(name, options); + if (!memberType && !options.ignoreAADefaultDynamicMembers) { + return DynamicType.instance; + } + return memberType; } isEqual(otherType: BscType) { diff --git a/src/types/InheritableType.ts b/src/types/InheritableType.ts index b9e53fd0f..3300d00c6 100644 --- a/src/types/InheritableType.ts +++ b/src/types/InheritableType.ts @@ -20,7 +20,7 @@ export abstract class InheritableType extends BscType { let hasRoAssociativeArrayAsAncestor = this.name.toLowerCase() === 'roassociativearray' || this.getAncestorTypeList()?.find(ancestorType => ancestorType.name.toLowerCase() === 'roassociativearray'); if (hasRoAssociativeArrayAsAncestor) { - return super.getMemberType(memberName, options) ?? DynamicType.instance; + return super.getMemberType(memberName, options) ?? (!options?.ignoreAADefaultDynamicMembers ? DynamicType.instance : undefined); } const resultType = super.getMemberType(memberName, { ...options, fullName: memberName, tableProvider: () => this.memberTable }); diff --git a/src/types/IntersectionType.spec.ts b/src/types/IntersectionType.spec.ts index 0dcc89f18..c3c5f6a98 100644 --- a/src/types/IntersectionType.spec.ts +++ b/src/types/IntersectionType.spec.ts @@ -7,7 +7,6 @@ import { InterfaceType } from './InterfaceType'; import { SymbolTypeFlag } from '../SymbolTypeFlag'; import { BooleanType } from './BooleanType'; import { expectTypeToBe } from '../testHelpers.spec'; -import { isReferenceType } from '../astUtils/reflection'; import { SymbolTable } from '../SymbolTable'; import { ReferenceType } from './ReferenceType'; @@ -31,10 +30,8 @@ describe('IntersectionType', () => { expectTypeToBe(nameType, StringType); - expectTypeToBe(addressType, IntersectionType); - expect((addressType as IntersectionType).types.length).to.eq(2); - expect((addressType as IntersectionType).types).to.include(addressInterfaceType); - expect((addressType as IntersectionType).types.filter(isReferenceType).length).to.equal(1); // no address in iface1, so it is reference + expectTypeToBe(addressType, InterfaceType); + expect(addressType).to.equal(addressInterfaceType); expectTypeToBe(ageType, IntersectionType); expect((ageType as IntersectionType).types.length).to.eq(2); @@ -71,6 +68,53 @@ describe('IntersectionType', () => { expect(myInter2.isResolvable()).to.be.false; }); + it('inner type order does not affect equality', () => { + const inter1 = new IntersectionType([StringType.instance, IntegerType.instance, FloatType.instance]); + const inter2 = new IntersectionType([FloatType.instance, StringType.instance, IntegerType.instance]); + expect(inter1.isEqual(inter2)).to.be.true; + }); + + it('more specific intersections are compatible with less specific ones', () => { + const inter1 = new IntersectionType([StringType.instance, BooleanType.instance, FloatType.instance]); + const inter2 = new IntersectionType([FloatType.instance, BooleanType.instance]); + expect(inter1.isTypeCompatible(inter2)).to.be.false; + expect(inter2.isTypeCompatible(inter1)).to.be.true; + }); + + + it('more specific member interface is compatible with less specific ones', () => { + const iFace1 = new InterfaceType('iFace1'); + iFace1.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + iFace1.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + + const iFace2 = new InterfaceType('iFace2'); + iFace2.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + iFace2.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + iFace2.addMember('height', null, FloatType.instance, SymbolTypeFlag.runtime); + + const inter1 = new IntersectionType([iFace1, StringType.instance]); + const inter2 = new IntersectionType([iFace2, StringType.instance]); + expect(inter2.isTypeCompatible(inter1)).to.be.false; + expect(inter1.isTypeCompatible(inter2)).to.be.true; + }); + + + it('interface is compatible with intersection with common members and vice versa', () => { + const iFace1 = new InterfaceType('iFace1'); + iFace1.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + iFace1.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + + const iFace2 = new InterfaceType('iFace2'); + iFace2.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + + const iFace3 = new InterfaceType('iFace3'); + iFace3.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + + const inter = new IntersectionType([iFace2, iFace3]); + expect(iFace1.isTypeCompatible(inter)).to.be.true; + expect(inter.isTypeCompatible(iFace1)).to.be.true; + }); + describe('getMemberType', () => { it('will find the intersection of inner types', () => { @@ -99,7 +143,7 @@ describe('IntersectionType', () => { }); - it('will return reference types if any inner type does not include the member', () => { + it('will not return reference types if at least one inner type includes the member', () => { const iFace1 = new InterfaceType('iFace1'); iFace1.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); iFace1.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); @@ -112,17 +156,16 @@ describe('IntersectionType', () => { const myInter = new IntersectionType([iFace1, iFace2]); const options = { flags: SymbolTypeFlag.runtime }; const heightType1 = myInter.getMemberType('height', options); - expectTypeToBe(heightType1, IntersectionType); - const heightTypes1 = (heightType1 as IntersectionType).types; - expect(heightTypes1.length).to.eq(2); - expectTypeToBe(heightTypes1[0], FloatType); - // height does not exist in iFace2 - expect(isReferenceType(heightTypes1[1])).to.be.true; - expect(heightTypes1[1].isResolvable()).to.be.false; - - iFace2.addMember('height', null, FloatType.instance, SymbolTypeFlag.runtime); + expectTypeToBe(heightType1, FloatType); + + // adding new member to iFace2, result will be intersection + iFace2.addMember('height', null, BooleanType.instance, SymbolTypeFlag.runtime); const heightType2 = myInter.getMemberType('height', options); - expectTypeToBe(heightType2, FloatType); + expectTypeToBe(heightType2, IntersectionType); + const heightTypes2 = (heightType2 as IntersectionType).types; + expect(heightTypes2.length).to.eq(2); + expectTypeToBe(heightTypes2[0], FloatType); + expectTypeToBe(heightTypes2[1], BooleanType); }); }); diff --git a/src/types/IntersectionType.ts b/src/types/IntersectionType.ts index a8f296466..abef2b76b 100644 --- a/src/types/IntersectionType.ts +++ b/src/types/IntersectionType.ts @@ -1,5 +1,5 @@ import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; -import { isDynamicType, isIntersectionType, isObjectType, isTypedFunctionType } from '../astUtils/reflection'; +import { isAssociativeArrayType, isDynamicType, isIntersectionType, isObjectType, isTypedFunctionType } from '../astUtils/reflection'; import { BscType } from './BscType'; import { ReferenceType } from './ReferenceType'; import { addAssociatedTypesTableAsSiblingToMemberTable, getAllTypesFromComplexType, isEnumTypeCompatible, joinTypesString, reduceTypesForIntersectionType } from './helpers'; @@ -9,6 +9,7 @@ import { SymbolTable } from '../SymbolTable'; import { SymbolTypeFlag } from '../SymbolTypeFlag'; import { BuiltInInterfaceAdder } from './BuiltInInterfaceAdder'; import { util } from '../util'; +import { DynamicType } from './DynamicType'; export function intersectionTypeFactory(types: BscType[]) { return new IntersectionType(types); @@ -42,11 +43,14 @@ export class IntersectionType extends BscType { private getMemberTypeFromInnerTypes(name: string, options: GetTypeOptions): BscType { const typeFromMembers = this.types.map((innerType) => { - return innerType?.getMemberType(name, options); + return innerType?.getMemberType(name, { ...options, ignoreAADefaultDynamicMembers: true }); }); const filteredTypes = reduceTypesForIntersectionType(typeFromMembers.filter(t => t !== undefined)); if (filteredTypes.length === 0) { + if (this.types.some(isAssociativeArrayType)) { + return DynamicType.instance; + } return undefined; } else if (filteredTypes.length === 1) { return filteredTypes[0]; diff --git a/src/types/helpers.ts b/src/types/helpers.ts index e00f0ffd5..e4507f314 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -151,11 +151,15 @@ export function reduceTypesForIntersectionType(types: BscType[], allowNameEquali } types = types.map(t => { - if (isReferenceType(t) && t.isResolvable()) { - return (t as any).getTarget() ?? t; + if (isReferenceType(t)) { + if (t.isResolvable()) { + return (t as any).getTarget() ?? t; + + } + return undefined; } return t; - }); + }).filter(t => t); // Get a list of unique types, based on the `isEqual()` method const uniqueTypes = getUniqueTypesFromArray(types, allowNameEquality).map(t => { diff --git a/src/util.ts b/src/util.ts index e812972d7..c4d0ad2ec 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2279,7 +2279,7 @@ export class Util { } if (isBinaryExpression(expression?.parent)) { let currentExpr: AstNode = expression.parent; - while (isBinaryExpression(currentExpr) && currentExpr.tokens.operator.kind === TokenKind.Or) { + while (isBinaryExpression(currentExpr) && (currentExpr.tokens.operator.kind === TokenKind.Or || currentExpr.tokens.operator.kind === TokenKind.And)) { currentExpr = currentExpr.parent; } return isTypeExpression(currentExpr) || isTypedArrayExpression(currentExpr); From e202859e9e80310d95dca416d0ef03557f862e24 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Thu, 1 Jan 2026 10:43:17 -0400 Subject: [PATCH 05/17] Adds code to handle order of operations when building types --- src/parser/Parser.spec.ts | 37 ++++++++++++++++++++++++++++++++++ src/parser/Parser.ts | 42 +++++++++++++++++++++++++++++---------- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index df7d9c9f8..3a3221077 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -1639,6 +1639,43 @@ describe('parser', () => { `, ParseMode.BrighterScript); expectZeroDiagnostics(diagnostics); }); + + it('follows order of operations with "or" first lexically', () => { + let { ast, diagnostics } = parse(` + sub main(param as string or integer and float) + print param + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + const func = (ast.statements[0] as FunctionStatement).func; + const paramExpr = func.parameters[0]; + let binExpr = paramExpr.typeExpression.expression as BinaryExpression; + //first level should be 'or' + expect(binExpr.tokens.operator.kind).to.equal(TokenKind.Or); + //right side should be 'and' + expect(isBinaryExpression(binExpr.right)).to.be.true; + const rightAndExpr = binExpr.right as BinaryExpression; + expect(rightAndExpr.tokens.operator.kind).to.equal(TokenKind.And); + }); + + + it('follows order of operations with "and" first lexically', () => { + let { ast, diagnostics } = parse(` + sub main(param as string and integer or float) + print param + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + const func = (ast.statements[0] as FunctionStatement).func; + const paramExpr = func.parameters[0]; + let binExpr = paramExpr.typeExpression.expression as BinaryExpression; + //first level should be 'or' + expect(binExpr.tokens.operator.kind).to.equal(TokenKind.Or); + //left side should be 'and' + expect(isBinaryExpression(binExpr.left)).to.be.true; + const leftAndExpr = binExpr.left as BinaryExpression; + expect(leftAndExpr.tokens.operator.kind).to.equal(TokenKind.And); + }); }); diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index df676169e..2dde48849 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -3038,20 +3038,42 @@ export class Parser { private typeExpression(): TypeExpression { const changedTokens: { token: Token; oldKind: TokenKind }[] = []; try { + // handle types with 'and'/'or' operators + const expressionsWithOperator: { expression: Expression; operator?: Token }[] = []; + + // find all expressions and operators let expr: Expression = this.getTypeExpressionPart(changedTokens); while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or, TokenKind.And)) { - // If we're in Brighterscript mode, allow union types with "or" between types - // TODO: Handle Union types in parens? eg. "(string or integer)" let operator = this.previous(); - let right = this.getTypeExpressionPart(changedTokens); - if (right) { - expr = new BinaryExpression({ left: expr, operator: operator, right: right }); - } else { - break; + expressionsWithOperator.push({ expression: expr, operator: operator }); + expr = this.getTypeExpressionPart(changedTokens); + } + // add last expression + expressionsWithOperator.push({ expression: expr }); + + // handle expressions with order of operations - first "and", then "or" + const combineExpressions = (opToken: TokenKind) => { + let exprWithOp = expressionsWithOperator[0]; + let index = 0; + while (exprWithOp?.operator) { + if (exprWithOp.operator.kind === opToken) { + const nextExpr = expressionsWithOperator[index + 1]; + const combinedExpr = new BinaryExpression({ left: exprWithOp.expression, operator: exprWithOp.operator, right: nextExpr.expression }); + // replace the two expressions with the combined one + expressionsWithOperator.splice(index, 2, { expression: combinedExpr, operator: nextExpr.operator }); + exprWithOp = expressionsWithOperator[index]; + } else { + index++; + exprWithOp = expressionsWithOperator[index]; + } } - } - if (expr) { - return new TypeExpression({ expression: expr }); + }; + + combineExpressions(TokenKind.And); + combineExpressions(TokenKind.Or); + + if (expressionsWithOperator[0]?.expression) { + return new TypeExpression({ expression: expressionsWithOperator[0].expression }); } } catch (error) { From 02ba2bb57c66bc026481de876a9252375d4f8233 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Thu, 1 Jan 2026 19:17:05 -0400 Subject: [PATCH 06/17] Adds code to handle order of operations when building types --- src/Scope.spec.ts | 24 +++++++++++++++++++++-- src/parser/Parser.spec.ts | 37 +++++++++++++++++++++++++++++++++-- src/parser/Parser.ts | 2 ++ src/types/CallFuncableType.ts | 6 +++--- src/util.ts | 20 ++++++++++--------- 5 files changed, 73 insertions(+), 16 deletions(-) diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts index d2a82c744..bac41220f 100644 --- a/src/Scope.spec.ts +++ b/src/Scope.spec.ts @@ -7,7 +7,7 @@ import { Program } from './Program'; import PluginInterface from './PluginInterface'; import { expectDiagnostics, expectDiagnosticsIncludes, expectTypeToBe, expectZeroDiagnostics, trim } from './testHelpers.spec'; import type { BrsFile } from './files/BrsFile'; -import type { AssignmentStatement, ForEachStatement, IfStatement, NamespaceStatement, PrintStatement } from './parser/Statement'; +import type { AssignmentStatement, ForEachStatement, IfStatement, NamespaceStatement, PrintStatement, TypeStatement } from './parser/Statement'; import type { CompilerPlugin, OnScopeValidateEvent } from './interfaces'; import { SymbolTypeFlag } from './SymbolTypeFlag'; import { EnumMemberType, EnumType } from './types/EnumType'; @@ -20,7 +20,7 @@ import { FloatType } from './types/FloatType'; import { NamespaceType } from './types/NamespaceType'; import { DoubleType } from './types/DoubleType'; import { UnionType } from './types/UnionType'; -import { isBlock, isCallExpression, isForEachStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isNamespaceStatement, isPrintStatement } from './astUtils/reflection'; +import { isBlock, isCallExpression, isForEachStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isNamespaceStatement, isPrintStatement, isTypeStatement } from './astUtils/reflection'; import { ArrayType } from './types/ArrayType'; import { AssociativeArrayType } from './types/AssociativeArrayType'; import { InterfaceType } from './types/InterfaceType'; @@ -3041,6 +3041,26 @@ describe('Scope', () => { const countType = mainSymbolTable.getSymbolType('y', { flags: SymbolTypeFlag.runtime }); expectTypeToBe(countType, IntegerType); }); + + it('allows grouped expressions in type statement', () => { + const mainFile = program.setFile('source/main.bs', ` + type guy = ({name as string, age as integer} or {id as integer, age as integer}) and {foo as boolean} + + + sub foo(person as guy) + if person.foo + print person.age + 123 + end if + end sub + `); + const ast = mainFile.ast; + program.validate(); + expectZeroDiagnostics(program); + expect(isTypeStatement(ast.statements[0])).to.be.true; + const stmt = ast.statements[0] as TypeStatement; + expect(stmt.tokens.type.text).to.eq('type'); + expect(stmt.value).to.exist; + }); }); describe('type casts', () => { diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index 3a3221077..9aa4ca1a8 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -1,14 +1,14 @@ import { expect, assert } from '../chai-config.spec'; import { Lexer } from '../lexer/Lexer'; import { ReservedWords, TokenKind } from '../lexer/TokenKind'; -import type { AAMemberExpression, BinaryExpression, InlineInterfaceExpression, LiteralExpression, TypecastExpression, UnaryExpression } from './Expression'; +import type { AAMemberExpression, BinaryExpression, GroupingExpression, InlineInterfaceExpression, LiteralExpression, TypecastExpression, TypeExpression, UnaryExpression } from './Expression'; import { TernaryExpression, NewExpression, IndexedGetExpression, DottedGetExpression, XmlAttributeGetExpression, CallfuncExpression, AnnotationExpression, CallExpression, FunctionExpression, VariableExpression } from './Expression'; import { Parser, ParseMode } from './Parser'; import type { AliasStatement, AssignmentStatement, Block, ClassStatement, ConditionalCompileConstStatement, ConditionalCompileErrorStatement, ConditionalCompileStatement, ExitStatement, ForStatement, IfStatement, InterfaceStatement, ReturnStatement, TypecastStatement, TypeStatement } from './Statement'; import { PrintStatement, FunctionStatement, NamespaceStatement, ImportStatement } from './Statement'; import { Range } from 'vscode-languageserver'; import { DiagnosticMessages } from '../DiagnosticMessages'; -import { isAliasStatement, isAssignmentStatement, isBinaryExpression, isBlock, isBody, isCallExpression, isCallfuncExpression, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isDottedGetExpression, isExitStatement, isExpression, isExpressionStatement, isFunctionStatement, isGroupingExpression, isIfStatement, isIndexedGetExpression, isInlineInterfaceExpression, isInterfaceStatement, isLiteralExpression, isNamespaceStatement, isPrintStatement, isTypecastExpression, isTypecastStatement, isTypeStatement, isUnaryExpression, isVariableExpression } from '../astUtils/reflection'; +import { isAliasStatement, isAssignmentStatement, isBinaryExpression, isBlock, isBody, isCallExpression, isCallfuncExpression, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isDottedGetExpression, isExitStatement, isExpression, isExpressionStatement, isFunctionStatement, isGroupingExpression, isIfStatement, isIndexedGetExpression, isInlineInterfaceExpression, isInterfaceStatement, isLiteralExpression, isNamespaceStatement, isPrintStatement, isTypecastExpression, isTypecastStatement, isTypeExpression, isTypeStatement, isUnaryExpression, isVariableExpression } from '../astUtils/reflection'; import { expectDiagnostics, expectDiagnosticsIncludes, expectTypeToBe, expectZeroDiagnostics, rootDir } from '../testHelpers.spec'; import { createVisitor, WalkMode } from '../astUtils/visitors'; import type { Expression, Statement } from './AstNode'; @@ -1676,6 +1676,27 @@ describe('parser', () => { const leftAndExpr = binExpr.left as BinaryExpression; expect(leftAndExpr.tokens.operator.kind).to.equal(TokenKind.And); }); + + it('allows grouped expression to override order of operations', () => { + let { ast, diagnostics } = parse(` + sub main(param as string and (integer or float)) + print param + end sub + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + const func = (ast.statements[0] as FunctionStatement).func; + const paramExpr = func.parameters[0]; + let binExpr = paramExpr.typeExpression.expression as BinaryExpression; + //first level should be 'and' + expect(binExpr.tokens.operator.kind).to.equal(TokenKind.And); + //right side should be 'or' + expect(isGroupingExpression(binExpr.right)).to.be.true; + const groupedExpr = binExpr.right as GroupingExpression; + expect(isGroupingExpression(groupedExpr)).to.be.true; + expect(isTypeExpression(groupedExpr.expression)).to.be.true; + const rightOrExpr = (groupedExpr.expression as TypeExpression).expression as BinaryExpression; + expect(rightOrExpr.tokens.operator.kind).to.equal(TokenKind.Or); + }); }); @@ -2641,6 +2662,18 @@ describe('parser', () => { expect(stmt.tokens.type.text).to.eq('type'); expect(stmt.value).to.exist; }); + + it('allows grouped expressions in type statement', () => { + let { ast, diagnostics } = parse(` + type guy = ({name as string} or {age as integer}) and {foo as boolean, age as integer} + + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + expect(isTypeStatement(ast.statements[0])).to.be.true; + const stmt = ast.statements[0] as TypeStatement; + expect(stmt.tokens.type.text).to.eq('type'); + expect(stmt.value).to.exist; + }); }); describe('jump statements', () => { diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 2dde48849..0926f3279 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -3047,6 +3047,7 @@ export class Parser { let operator = this.previous(); expressionsWithOperator.push({ expression: expr, operator: operator }); expr = this.getTypeExpressionPart(changedTokens); + console.log('got type part', this.current); } // add last expression expressionsWithOperator.push({ expression: expr }); @@ -3056,6 +3057,7 @@ export class Parser { let exprWithOp = expressionsWithOperator[0]; let index = 0; while (exprWithOp?.operator) { + console.log(`checking operator ${TokenKind[exprWithOp.operator.kind]} against ${TokenKind[opToken]}`, index); if (exprWithOp.operator.kind === opToken) { const nextExpr = expressionsWithOperator[index + 1]; const combinedExpr = new BinaryExpression({ left: exprWithOp.expression, operator: exprWithOp.operator, right: nextExpr.expression }); diff --git a/src/types/CallFuncableType.ts b/src/types/CallFuncableType.ts index 0cc25926d..efc205a98 100644 --- a/src/types/CallFuncableType.ts +++ b/src/types/CallFuncableType.ts @@ -1,5 +1,5 @@ import { SymbolTypeFlag } from '../SymbolTypeFlag'; -import type { BscSymbol, GetSymbolTypeOptions, SymbolTableProvider } from '../SymbolTable'; +import type { GetSymbolTypeOptions, SymbolTableProvider } from '../SymbolTable'; import { SymbolTable } from '../SymbolTable'; import type { BscType } from './BscType'; import { InheritableType } from './InheritableType'; @@ -60,8 +60,8 @@ export abstract class CallFuncableType extends InheritableType { for (const type of originalTypesToCheck) { if (!type.isBuiltIn) { - util.getCustomTypesInSymbolTree(additionalTypesToCheck, type, (subSymbol: BscSymbol) => { - return !originalTypesToCheck.has(subSymbol.type); + util.getCustomTypesInSymbolTree(additionalTypesToCheck, type, (subSymbolType: BscType) => { + return !originalTypesToCheck.has(subSymbolType); }); } } diff --git a/src/util.ts b/src/util.ts index c4d0ad2ec..4794dcacb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -25,7 +25,7 @@ import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionP import { LogLevel, createLogger } from './logging'; import { isToken, type Identifier, type Token } from './lexer/Token'; import { TokenKind } from './lexer/TokenKind'; -import { isAnyReferenceType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isComponentType, isDottedGetExpression, isDoubleTypeLike, isDynamicType, isEnumMemberType, isExpression, isFloatTypeLike, isIndexedGetExpression, isIntegerTypeLike, isIntersectionType, isInvalidTypeLike, isLiteralString, isLongIntegerTypeLike, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberTypeLike, isObjectType, isPrimitiveType, isReferenceType, isStatement, isStringTypeLike, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection'; +import { isAnyReferenceType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isComplexType, isComponentType, isDottedGetExpression, isDoubleTypeLike, isDynamicType, isEnumMemberType, isExpression, isFloatTypeLike, isIndexedGetExpression, isIntegerTypeLike, isIntersectionType, isInvalidTypeLike, isLiteralString, isLongIntegerTypeLike, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberTypeLike, isObjectType, isPrimitiveType, isReferenceType, isStatement, isStringTypeLike, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection'; import { WalkMode } from './astUtils/visitors'; import { SourceNode } from 'source-map'; import * as requireRelative from 'require-relative'; @@ -34,7 +34,7 @@ import type { XmlFile } from './files/XmlFile'; import type { AstNode, Expression, Statement } from './parser/AstNode'; import { AstNodeKind } from './parser/AstNode'; import type { UnresolvedSymbol } from './AstValidationSegmenter'; -import type { BscSymbol, GetSymbolTypeOptions, SymbolTable } from './SymbolTable'; +import type { GetSymbolTypeOptions, SymbolTable } from './SymbolTable'; import { SymbolTypeFlag } from './SymbolTypeFlag'; import { createIdentifier, createToken } from './astUtils/creators'; import { MAX_RELATED_INFOS_COUNT } from './diagnosticUtils'; @@ -2327,17 +2327,19 @@ export class Util { return false; } - public getCustomTypesInSymbolTree(setToFill: Set, type: BscType, filter?: (t: BscSymbol) => boolean) { - const subSymbols = type.getMemberTable()?.getAllSymbols(SymbolTypeFlag.runtime) ?? []; - for (const subSymbol of subSymbols) { - if (!subSymbol.type?.isBuiltIn && !setToFill.has(subSymbol.type)) { - if (filter && !filter(subSymbol)) { + public getCustomTypesInSymbolTree(setToFill: Set, type: BscType, filter?: (t: BscType) => boolean) { + const subSymbolTypes = isComplexType(type) + ? type.types + : type.getMemberTable()?.getAllSymbols(SymbolTypeFlag.runtime).map(sym => sym.type) ?? []; + for (const subSymbolType of subSymbolTypes) { + if (!subSymbolType?.isBuiltIn && !setToFill.has(subSymbolType)) { + if (filter && !filter(subSymbolType)) { continue; } // if this is a custom type, and we haven't added it to the types to check to see if can add it to the additional types // add the type, and investigate any members - setToFill.add(subSymbol.type); - this.getCustomTypesInSymbolTree(setToFill, subSymbol.type, filter); + setToFill.add(subSymbolType); + this.getCustomTypesInSymbolTree(setToFill, subSymbolType, filter); } } From 29022b78b93d45a83a61880dd78f97aea065fe51 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 2 Jan 2026 09:22:18 -0400 Subject: [PATCH 07/17] removed log statments, started adding validation tests --- .../validation/ScopeValidator.spec.ts | 114 +++++++++++++++++- src/parser/Parser.ts | 2 - 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index 191a8e163..ccbfcdb8a 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -7,7 +7,7 @@ import type { TypeCompatibilityData } from '../../interfaces'; import { IntegerType } from '../../types/IntegerType'; import { StringType } from '../../types/StringType'; import type { BrsFile } from '../../files/BrsFile'; -import { FloatType, InterfaceType, TypedFunctionType, VoidType } from '../../types'; +import { FloatType, InterfaceType, TypedFunctionType, VoidType, BooleanType } from '../../types'; import { SymbolTypeFlag } from '../../SymbolTypeFlag'; import { AssociativeArrayType } from '../../types/AssociativeArrayType'; import undent from 'undent'; @@ -278,6 +278,25 @@ describe('ScopeValidator', () => { DiagnosticMessages.mismatchArgumentCount(1, 0).message ]); }); + + it.only('validates against functions defined in intersection types', () => { + program.setFile('source/main.bs', ` + interface IFirst + num as integer + end interface + interface ISecond + function doThing2(a as integer, b as string) as void + end interface + + sub main(thing as IFirst and ISecond) + thing.doThing2(thing.num) + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.mismatchArgumentCount(2, 1).message + ]); + }); }); describe('argumentTypeMismatch', () => { @@ -1916,6 +1935,99 @@ describe('ScopeValidator', () => { program.validate(); expectZeroDiagnostics(program); }); + + describe.only('intersection types', () => { + + it('validates against functions defined in intersection types', () => { + program.setFile('source/main.bs', ` + interface IFirst + num as integer + end interface + interface ISecond + function doThing2(a as integer, b as string) as void + end interface + + sub main(thing as IFirst and ISecond) + thing.doThing2(thing.num, false) ' b should be a string + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.argumentTypeMismatch('boolean', 'string').message + ]); + }); + + it('allows passing AAs that satisfy intersection types', () => { + program.setFile('source/main.bs', ` + interface IFirst + num as integer + end interface + interface ISecond + function doThing2(a as integer, b as string) as void + end interface + + sub main() + thing = { + num: 123, + doThing2: function(a as integer, b as string) as void + print a + print b + end function + } + usesThing(thing) + end sub + + sub usesThing(thing as IFirst and ISecond) + thing.doThing2(thing.num, "hello") + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('validates passing AAs that do not satisfy intersection types', () => { + program.setFile('source/main.bs', ` + interface IFirst + num as integer + end interface + interface ISecond + function doThing2(a as integer, b as string) as void + end interface + + sub main() + thing = { + num: false, + doThing2: function(a as integer, b as boolean) as void + print a + print b + end function + } + usesThing(thing) + end sub + + sub usesThing(thing as IFirst and ISecond) + end sub + `); + program.validate(); + const expectedDoThing2 = new TypedFunctionType(VoidType.instance); + expectedDoThing2.name = 'doThing2'; + expectedDoThing2.addParameter('a', IntegerType.instance, false); + expectedDoThing2.addParameter('b', StringType.instance, false); + + const actualDoThing2 = new TypedFunctionType(VoidType.instance); + actualDoThing2.addParameter('a', IntegerType.instance, false); + actualDoThing2.addParameter('b', BooleanType.instance, false); + expectDiagnostics(program, + [ + DiagnosticMessages.argumentTypeMismatch('roAssociativeArray', 'IFirst and ISecond', { + fieldMismatches: [ + { name: 'num', expectedType: IntegerType.instance, actualType: BooleanType.instance }, + { name: 'doThing2', expectedType: expectedDoThing2, actualType: actualDoThing2 } + ] + }).message + ]); + }); + }); }); describe('cannotFindName', () => { diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 0926f3279..2dde48849 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -3047,7 +3047,6 @@ export class Parser { let operator = this.previous(); expressionsWithOperator.push({ expression: expr, operator: operator }); expr = this.getTypeExpressionPart(changedTokens); - console.log('got type part', this.current); } // add last expression expressionsWithOperator.push({ expression: expr }); @@ -3057,7 +3056,6 @@ export class Parser { let exprWithOp = expressionsWithOperator[0]; let index = 0; while (exprWithOp?.operator) { - console.log(`checking operator ${TokenKind[exprWithOp.operator.kind]} against ${TokenKind[opToken]}`, index); if (exprWithOp.operator.kind === opToken) { const nextExpr = expressionsWithOperator[index + 1]; const combinedExpr = new BinaryExpression({ left: exprWithOp.expression, operator: exprWithOp.operator, right: nextExpr.expression }); From 6a7394695071dee7d4ba45900874a96f3f01206b Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 2 Jan 2026 09:22:44 -0400 Subject: [PATCH 08/17] remove .only --- src/bscPlugin/validation/ScopeValidator.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index ccbfcdb8a..22f49e426 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -279,7 +279,7 @@ describe('ScopeValidator', () => { ]); }); - it.only('validates against functions defined in intersection types', () => { + it('validates against functions defined in intersection types', () => { program.setFile('source/main.bs', ` interface IFirst num as integer @@ -1936,7 +1936,7 @@ describe('ScopeValidator', () => { expectZeroDiagnostics(program); }); - describe.only('intersection types', () => { + describe('intersection types', () => { it('validates against functions defined in intersection types', () => { program.setFile('source/main.bs', ` From e49662b753215a2f078c0873ef9852911b61f69d Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 5 Jan 2026 09:06:24 -0400 Subject: [PATCH 09/17] Better handling of errors - missing right paren on function declration and using union type in BRS mode --- src/Scope.spec.ts | 2 +- .../validation/ScopeValidator.spec.ts | 6 +-- src/parser/Parser.spec.ts | 43 ++++++++++++++++--- src/parser/Parser.ts | 11 ++++- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts index bac41220f..057c84510 100644 --- a/src/Scope.spec.ts +++ b/src/Scope.spec.ts @@ -2840,7 +2840,7 @@ describe('Scope', () => { it('should handle union types grouped', () => { const mainFile = program.setFile('source/main.bs', ` - sub nestedUnion(thing as (Person or Pet) or (Vehicle or Duck) + sub nestedUnion(thing as (Person or Pet) or (Vehicle or Duck)) id = thing.id print id end sub diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index 22f49e426..d9974e552 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -1985,7 +1985,7 @@ describe('ScopeValidator', () => { expectZeroDiagnostics(program); }); - it('validates passing AAs that do not satisfy intersection types', () => { + it('validates passing AAs that do not satisfy intersection types', () => { program.setFile('source/main.bs', ` interface IFirst num as integer @@ -3738,7 +3738,7 @@ describe('ScopeValidator', () => { inlineMember as {name as string} end interface - sub takesInline(someIface as Iface} + sub takesInline(someIface as Iface) someIface.inlineMember = {name: "test"} end sub `); @@ -3752,7 +3752,7 @@ describe('ScopeValidator', () => { inlineMember as {name as string} end interface - sub takesInline(someIface as Iface} + sub takesInline(someIface as Iface) someIface.inlineMember = {name: 123} end sub `); diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index 9aa4ca1a8..2e253827c 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -1521,7 +1521,7 @@ describe('parser', () => { print param end sub `, ParseMode.BrightScript); - expectDiagnosticsIncludes(parser.diagnostics, [DiagnosticMessages.expectedStatement()]); + expectDiagnosticsIncludes(parser.diagnostics, [DiagnosticMessages.bsFeatureNotSupportedInBrsFiles('custom types')]); }); it('allows group type expressions in parameters', () => { @@ -1582,7 +1582,7 @@ describe('parser', () => { print param end sub `, ParseMode.BrightScript); - expectDiagnosticsIncludes(parser.diagnostics, [DiagnosticMessages.expectedStatement()]); + expectDiagnosticsIncludes(parser.diagnostics, [DiagnosticMessages.bsFeatureNotSupportedInBrsFiles('custom types')]); }); it('allows union types in parameters', () => { @@ -1616,7 +1616,7 @@ describe('parser', () => { print param end sub `, ParseMode.BrightScript); - expectDiagnosticsIncludes(parser.diagnostics, [DiagnosticMessages.expectedStatement()]); + expectDiagnosticsIncludes(parser.diagnostics, [DiagnosticMessages.bsFeatureNotSupportedInBrsFiles('custom types')]); }); it('allows intersection types in parameters', () => { @@ -1697,6 +1697,36 @@ describe('parser', () => { const rightOrExpr = (groupedExpr.expression as TypeExpression).expression as BinaryExpression; expect(rightOrExpr.tokens.operator.kind).to.equal(TokenKind.Or); }); + + describe('invalid syntax', () => { + + it('flags union type with missing sides', () => { + let { diagnostics } = parse(` + sub main(param as Thing or ) + print param + end sub + `, ParseMode.BrighterScript); + expectDiagnosticsIncludes(diagnostics, DiagnosticMessages.expectedIdentifier('or').message); + }); + + it('flags missing type inside binary type', () => { + let { diagnostics } = parse(` + sub main(param as string or and float) + print param + end sub + `, ParseMode.BrighterScript); + expect(diagnostics[0]?.message).to.exist; + }); + + it('flags missing group paren', () => { + let { diagnostics } = parse(` + sub main(param as (string or float) + print param + end sub + `, ParseMode.BrighterScript); + expect(diagnostics[0]?.message).to.exist; + }); + }); }); @@ -1936,13 +1966,14 @@ describe('parser', () => { }); it('gets multiple lines of leading trivia', () => { - let { ast } = parse(` + let { ast, diagnostics } = parse(` ' Say hello to someone ' ' @param {string} name the person you want to say hello to. - sub sayHello(name as string = "world") + sub sayHello(name = "world" as string) end sub `); + expectZeroDiagnostics(diagnostics); const funcStatements = ast.statements.filter(isFunctionStatement); const helloTrivia = funcStatements[0].leadingTrivia; expect(helloTrivia.length).to.be.greaterThan(0); @@ -1969,7 +2000,7 @@ describe('parser', () => { @annotation ' hello comment 3 @otherAnnotation - sub sayHello(name as string = "world") + sub sayHello(name = "world" as string) end sub `, ParseMode.BrighterScript); const funcStatements = ast.statements.filter(isFunctionStatement); diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 2dde48849..f26c47d35 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -105,6 +105,7 @@ import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDotte import { createStringLiteral, createToken } from '../astUtils/creators'; import type { Expression, Statement } from './AstNode'; import type { BsDiagnostic, DeepWriteable } from '../interfaces'; +import { warn } from 'console'; const declarableTypesLower = DeclarableTypes.map(tokenKind => tokenKind.toLowerCase()); @@ -426,6 +427,9 @@ export class Parser { typeExpression = this.typeExpression(); } } + if (this.checkAny(TokenKind.And, TokenKind.Or)) { + this.warnIfNotBrighterScriptMode('custom types'); + } return [asToken, typeExpression]; } @@ -882,8 +886,10 @@ export class Parser { params.push(this.functionParameter()); } while (this.match(TokenKind.Comma)); } - let rightParen = this.advance(); - + let rightParen = this.consume( + DiagnosticMessages.unmatchedLeftToken(leftParen.text, 'function parameter list'), + TokenKind.RightParen + ); if (this.check(TokenKind.As)) { [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression(); } @@ -3102,6 +3108,7 @@ export class Parser { if (this.options.mode === ParseMode.BrightScript && !declarableTypesLower.includes(this.peek()?.text?.toLowerCase())) { // custom types arrays not allowed in Brightscript this.warnIfNotBrighterScriptMode('custom types'); + this.advance(); // skip custom type token return expr; } From 8f72d559783a5bd3d2cd6980fd195025e677fffc Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 5 Jan 2026 09:09:17 -0400 Subject: [PATCH 10/17] fix lint error --- src/parser/Parser.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index f26c47d35..67138b6d5 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -105,7 +105,6 @@ import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDotte import { createStringLiteral, createToken } from '../astUtils/creators'; import type { Expression, Statement } from './AstNode'; import type { BsDiagnostic, DeepWriteable } from '../interfaces'; -import { warn } from 'console'; const declarableTypesLower = DeclarableTypes.map(tokenKind => tokenKind.toLowerCase()); From e7a80744ab0b8a2319bfe377ccaf46d9f62af92f Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Tue, 6 Jan 2026 09:31:07 -0400 Subject: [PATCH 11/17] Adds more validation and compatibility tests for intersections: --- .../validation/ScopeValidator.spec.ts | 79 +++++++++++++++++++ src/types/IntersectionType.spec.ts | 26 ++++-- src/types/IntersectionType.ts | 15 ++-- 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index d9974e552..92ab7bb9a 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -3262,6 +3262,85 @@ describe('ScopeValidator', () => { program.validate(); expectZeroDiagnostics(program); }); + + describe('intersection types', () => { + + it('validates when type is intersection of primitive', () => { + program.setFile('source/main.bs', ` + function foo() as {id as string} and string + return {id: "test"} + end function + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.returnTypeMismatch('roAssociativeArray', '{id as string} and string').message + ]); + }); + + it('allows passing AAs that satisfy intersection types', () => { + program.setFile('source/main.bs', ` + interface IFirst + num as integer + end interface + interface ISecond + function doThing2(a as integer, b as string) as void + end interface + + function getThing() as IFirst and ISecond + thing = { + num: 123, + doThing2: function(a as integer, b as string) as void + print a + print b + end function + } + return thing + end function + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('validates passing AAs that do not satisfy intersection types', () => { + program.setFile('source/main.bs', ` + interface IFirst + num as integer + end interface + interface ISecond + function doThing2(a as integer, b as string) as void + end interface + + function getThing() as IFirst and ISecond + thing = { + num: false, + doThing2: function(a as integer, b as boolean) as void + print a + print b + end function + } + return thing + end function + `); + program.validate(); + const expectedDoThing2 = new TypedFunctionType(VoidType.instance); + expectedDoThing2.name = 'doThing2'; + expectedDoThing2.addParameter('a', IntegerType.instance, false); + expectedDoThing2.addParameter('b', StringType.instance, false); + + const actualDoThing2 = new TypedFunctionType(VoidType.instance); + actualDoThing2.addParameter('a', IntegerType.instance, false); + actualDoThing2.addParameter('b', BooleanType.instance, false); + expectDiagnostics(program, + [ + DiagnosticMessages.returnTypeMismatch('roAssociativeArray', 'IFirst and ISecond', { + fieldMismatches: [ + { name: 'num', expectedType: IntegerType.instance, actualType: BooleanType.instance }, + { name: 'doThing2', expectedType: expectedDoThing2, actualType: actualDoThing2 } + ] + }).message + ]); + }); + }); }); describe('returnTypeCoercionMismatch', () => { diff --git a/src/types/IntersectionType.spec.ts b/src/types/IntersectionType.spec.ts index c3c5f6a98..ccdd52a83 100644 --- a/src/types/IntersectionType.spec.ts +++ b/src/types/IntersectionType.spec.ts @@ -39,12 +39,12 @@ describe('IntersectionType', () => { expect((ageType as IntersectionType).types).to.include(IntegerType.instance); }); - it('can assign to a more general Intersection', () => { + it('can assign to a more specific Intersection', () => { const myInter = new IntersectionType([StringType.instance, FloatType.instance]); - const otheInter = new IntersectionType([FloatType.instance, StringType.instance, BooleanType.instance]); + const otherInter = new IntersectionType([FloatType.instance, StringType.instance, BooleanType.instance]); - expect(myInter.isTypeCompatible(otheInter)).to.be.true; - expect(otheInter.isTypeCompatible(myInter)).to.be.false; + expect(myInter.isTypeCompatible(otherInter)).to.be.true; + expect(otherInter.isTypeCompatible(myInter)).to.be.false; }); @@ -115,6 +115,23 @@ describe('IntersectionType', () => { expect(inter.isTypeCompatible(iFace1)).to.be.true; }); + it('is not compatible when it must satisfy conflicting member types', () => { + const iFace1 = new InterfaceType('iFace1'); + iFace1.addMember('age', null, IntegerType.instance, SymbolTypeFlag.runtime); + + const iFace2 = new InterfaceType('iFace2'); + iFace2.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + + const inter = new IntersectionType([iFace1, iFace2]); + + const iFace3 = new InterfaceType('iFace3'); + iFace3.addMember('age', null, BooleanType.instance, SymbolTypeFlag.runtime); + iFace3.addMember('name', null, StringType.instance, SymbolTypeFlag.runtime); + + expect(inter.isTypeCompatible(iFace3)).to.be.false; + expect(iFace3.isTypeCompatible(inter)).to.be.false; + }); + describe('getMemberType', () => { it('will find the intersection of inner types', () => { @@ -170,4 +187,3 @@ describe('IntersectionType', () => { }); }); - diff --git a/src/types/IntersectionType.ts b/src/types/IntersectionType.ts index abef2b76b..a495b8489 100644 --- a/src/types/IntersectionType.ts +++ b/src/types/IntersectionType.ts @@ -145,28 +145,29 @@ export class IntersectionType extends BscType { } if (isIntersectionType(targetType)) { // check if this all the types of this type are in the target (eg, target is a super set of this types) + let allMembersSatisfied = true; for (const memberType of this.types) { let foundCompatibleInnerType = false; for (const targetInnerType of targetType.types) { if (memberType.isTypeCompatible(targetInnerType, data)) { foundCompatibleInnerType = true; - continue; + break; } } if (!foundCompatibleInnerType) { - return false; + allMembersSatisfied = false; } } - return true; + return allMembersSatisfied; } + let foundCompatibleInnerType = true; for (const innerType of this.types) { - const foundCompatibleInnerType = innerType.isTypeCompatible(targetType, data); - if (foundCompatibleInnerType) { - return true; + if (!innerType.isTypeCompatible(targetType, data)) { + foundCompatibleInnerType = false; } } - return false; + return foundCompatibleInnerType; } toString(): string { return joinTypesString(this.types, 'and', BscTypeKind.IntersectionType); From 9305d487a4bf79aaa0410b309117a257f54ea7b4 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 9 Jan 2026 10:57:02 -0400 Subject: [PATCH 12/17] Adds new reference type for intersections with types with default dynamic members --- src/Scope.spec.ts | 78 +++++++++++++++++++ src/astUtils/reflection.ts | 11 ++- .../validation/ScopeValidator.spec.ts | 77 ++++++++++++++++++ src/interfaces.ts | 4 +- src/types/AssociativeArrayType.ts | 6 +- src/types/InheritableType.ts | 5 +- src/types/IntersectionType.ts | 74 +++++++++++------- src/types/ObjectType.ts | 5 +- src/types/ReferenceType.ts | 45 +++++++++++ src/types/helpers.ts | 10 ++- src/util.ts | 2 +- 11 files changed, 278 insertions(+), 39 deletions(-) diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts index 057c84510..79108bdc7 100644 --- a/src/Scope.spec.ts +++ b/src/Scope.spec.ts @@ -3061,6 +3061,84 @@ describe('Scope', () => { expect(stmt.tokens.type.text).to.eq('type'); expect(stmt.value).to.exist; }); + + it('unknown members of intersections with AA types return dynamic', () => { + const mainFile = program.setFile('source/main.bs', ` + sub printData(data as {customData as string} and roAssociativeArray) + x = data.someDynamicKey + y = data.customData + print x + print y + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const sourceScope = program.getScopeByName('source'); + expect(sourceScope).to.exist; + sourceScope.linkSymbolTable(); + expect(mainFnScope).to.exist; + const mainSymbolTable = mainFnScope.symbolTable; + const dataType = mainSymbolTable.getSymbolType('data', { flags: SymbolTypeFlag.runtime }) as IntersectionType; + expectTypeToBe(dataType, IntersectionType); + expect(dataType.types).to.have.lengthOf(2); + const xType = mainSymbolTable.getSymbolType('x', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(xType, DynamicType); + const yType = mainSymbolTable.getSymbolType('y', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(yType, StringType); + }); + + it('unknown members of intersections with object type return dynamic', () => { + const mainFile = program.setFile('source/main.bs', ` + sub printData(data as {customData as string} and object) + x = data.someDynamicKey + y = data.customData + print x + print y + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const sourceScope = program.getScopeByName('source'); + expect(sourceScope).to.exist; + sourceScope.linkSymbolTable(); + expect(mainFnScope).to.exist; + const mainSymbolTable = mainFnScope.symbolTable; + const dataType = mainSymbolTable.getSymbolType('data', { flags: SymbolTypeFlag.runtime }) as IntersectionType; + expectTypeToBe(dataType, IntersectionType); + expect(dataType.types).to.have.lengthOf(2); + const xType = mainSymbolTable.getSymbolType('x', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(xType, DynamicType); + const yType = mainSymbolTable.getSymbolType('y', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(yType, StringType); + }); + + it('order doesnt matter for intersections with object type and finding members', () => { + const mainFile = program.setFile('source/main.bs', ` + sub printData(data as object and {customData as string}) + x = data.someDynamicKey + y = data.customData + print x + print y + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const sourceScope = program.getScopeByName('source'); + expect(sourceScope).to.exist; + sourceScope.linkSymbolTable(); + expect(mainFnScope).to.exist; + const mainSymbolTable = mainFnScope.symbolTable; + const dataType = mainSymbolTable.getSymbolType('data', { flags: SymbolTypeFlag.runtime }) as IntersectionType; + expectTypeToBe(dataType, IntersectionType); + expect(dataType.types).to.have.lengthOf(2); + const xType = mainSymbolTable.getSymbolType('x', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(xType, DynamicType); + const yType = mainSymbolTable.getSymbolType('y', { flags: SymbolTypeFlag.runtime }); + expectTypeToBe(yType, StringType); + }); }); describe('type casts', () => { diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 94edbe37a..20648d791 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -23,7 +23,7 @@ import type { ObjectType } from '../types/ObjectType'; import type { AstNode, Expression, Statement } from '../parser/AstNode'; import type { AssetFile } from '../files/AssetFile'; import { AstNodeKind } from '../parser/AstNode'; -import type { TypePropertyReferenceType, ReferenceType, BinaryOperatorReferenceType, ArrayDefaultTypeReferenceType, AnyReferenceType, ParamTypeFromValueReferenceType } from '../types/ReferenceType'; +import type { TypePropertyReferenceType, ReferenceType, BinaryOperatorReferenceType, ArrayDefaultTypeReferenceType, AnyReferenceType, ParamTypeFromValueReferenceType, IntersectionWithDefaultDynamicReferenceType } from '../types/ReferenceType'; import type { EnumMemberType, EnumType } from '../types/EnumType'; import type { UnionType } from '../types/UnionType'; import type { UninitializedType } from '../types/UninitializedType'; @@ -456,6 +456,9 @@ export function isArrayDefaultTypeReferenceType(value: any): value is ArrayDefau export function isParamTypeFromValueReferenceType(value: any): value is ParamTypeFromValueReferenceType { return value?.__reflection?.name === 'ParamTypeFromValueReferenceType'; } +export function isIntersectionWithDefaultDynamicReferenceType(value: any): value is IntersectionWithDefaultDynamicReferenceType { + return value?.__reflection?.name === 'IntersectionWithDefaultDynamicReferenceType'; +} export function isNamespaceType(value: any): value is NamespaceType { return value?.kind === BscTypeKind.NamespaceType; } @@ -492,7 +495,7 @@ export function isCallableType(target): target is BaseFunctionType { export function isAnyReferenceType(target): target is AnyReferenceType { const name = target?.__reflection?.name; - return name === 'ReferenceType' || name === 'TypePropertyReferenceType' || name === 'BinaryOperatorReferenceType' || name === 'ArrayDefaultTypeReferenceType' || name === 'ParamTypeFromValueReferenceType'; + return name === 'ReferenceType' || name === 'TypePropertyReferenceType' || name === 'BinaryOperatorReferenceType' || name === 'ArrayDefaultTypeReferenceType' || name === 'ParamTypeFromValueReferenceType' || name === 'IntersectionWithDefaultDynamicReferenceType'; } export function isNumberType(value: any): value is IntegerType | LongIntegerType | FloatType | DoubleType | InterfaceType { @@ -523,6 +526,10 @@ export function isPrimitiveTypeLike(value: any = false): value is IntegerType | isTypeStatementTypeOf(value, isPrimitiveTypeLike); } +export function isAssociativeArrayTypeLike(value: any): value is AssociativeArrayType | InterfaceType { + return value?.kind === BscTypeKind.AssociativeArrayType || isBuiltInType(value, 'roAssociativeArray') || isComplexTypeOf(value, isAssociativeArrayTypeLike); +} + export function isBuiltInType(value: any, name: string): value is InterfaceType { return (isInterfaceType(value) && value.name.toLowerCase() === name.toLowerCase() && value.isBuiltIn) || (isTypeStatementType(value) && isBuiltInType(value.wrappedType, name)); diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index 92ab7bb9a..e21a988fc 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -2578,6 +2578,83 @@ describe('ScopeValidator', () => { ]); }); }); + + describe('intersection types', () => { + it('finds members from intersection types', () => { + program.setFile('source/main.bs', ` + interface IFirst + num as integer + end interface + interface ISecond + function doThing2(a as integer, b as string) as void + end interface + + sub main(thing as IFirst and ISecond) + print thing.num + thing.doThing2(thing.num, "hello") + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('finds random members of intersections with AA types', () => { + program.setFile('source/main.bs', ` + sub printData(data as {customData as string} and roAssociativeArray) + x = data.someDynamicKey + y = data.customData + print x + print y + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + + it('handles type statements that are intersections of classes and AA', () => { + program.setFile('source/main.bs', ` + sub printData(data as MyKlassAA) + x = data.customData + data.append({ + newKey: "newValue" + }) + print x + y = data.newKey + print y + end sub + + + type MyKlassAA = MyKlass and roAssociativeArray + + class MyKlass + customData as string + end class + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('validates missing members from intersection types', () => { + program.setFile('source/main.bs', ` + interface IFirst + num as integer + end interface + interface ISecond + function doThing2(a as integer, b as string) as void + end interface + + sub main(thing as IFirst and ISecond) + print thing.nonExistentMember + thing.doThing2(thing.num, "hello") + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.cannotFindName('nonExistentMember', '(IFirst and ISecond).nonExistentMember', '(IFirst and ISecond)') + ]); + }); + }); }); describe('itemCannotBeUsedAsVariable', () => { diff --git a/src/interfaces.ts b/src/interfaces.ts index 97a278791..ce9b4541c 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1043,9 +1043,9 @@ export interface GetTypeOptions { statementIndex?: number | 'end'; ignoreParentTables?: boolean; /** - * If this is true, AA's do not return dynamic if no member is found + * If this is true, AA's, objects, nodes, etc, do not return dynamic if no member is found */ - ignoreAADefaultDynamicMembers?: boolean; + ignoreDefaultDynamicMembers?: boolean; } export class TypeChainEntry { diff --git a/src/types/AssociativeArrayType.ts b/src/types/AssociativeArrayType.ts index eb8edf66f..25d826b96 100644 --- a/src/types/AssociativeArrayType.ts +++ b/src/types/AssociativeArrayType.ts @@ -1,6 +1,6 @@ import { SymbolTable } from '../SymbolTable'; import { SymbolTypeFlag } from '../SymbolTypeFlag'; -import { isAssociativeArrayType, isClassType, isDynamicType, isObjectType } from '../astUtils/reflection'; +import { isAssociativeArrayType, isAssociativeArrayTypeLike, isClassType, isDynamicType, isObjectType } from '../astUtils/reflection'; import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; import { BscType } from './BscType'; import { BscTypeKind } from './BscTypeKind'; @@ -21,7 +21,7 @@ export class AssociativeArrayType extends BscType { return true; } else if (isUnionTypeCompatible(this, targetType)) { return true; - } else if (isAssociativeArrayType(targetType)) { + } else if (isAssociativeArrayTypeLike(targetType)) { return true; } else if (this.checkCompatibilityBasedOnMembers(targetType, SymbolTypeFlag.runtime, data)) { return true; @@ -42,7 +42,7 @@ export class AssociativeArrayType extends BscType { getMemberType(name: string, options: GetTypeOptions) { // if a member has specifically been added, cool. otherwise, assume dynamic const memberType = super.getMemberType(name, options); - if (!memberType && !options.ignoreAADefaultDynamicMembers) { + if (!memberType && !options.ignoreDefaultDynamicMembers) { return DynamicType.instance; } return memberType; diff --git a/src/types/InheritableType.ts b/src/types/InheritableType.ts index 3300d00c6..6483694d4 100644 --- a/src/types/InheritableType.ts +++ b/src/types/InheritableType.ts @@ -20,12 +20,15 @@ export abstract class InheritableType extends BscType { let hasRoAssociativeArrayAsAncestor = this.name.toLowerCase() === 'roassociativearray' || this.getAncestorTypeList()?.find(ancestorType => ancestorType.name.toLowerCase() === 'roassociativearray'); if (hasRoAssociativeArrayAsAncestor) { - return super.getMemberType(memberName, options) ?? (!options?.ignoreAADefaultDynamicMembers ? DynamicType.instance : undefined); + return super.getMemberType(memberName, options) ?? (!options?.ignoreDefaultDynamicMembers ? DynamicType.instance : undefined); } const resultType = super.getMemberType(memberName, { ...options, fullName: memberName, tableProvider: () => this.memberTable }); if (this.changeUnknownMemberToDynamic && !resultType.isResolvable()) { + if (options.ignoreDefaultDynamicMembers) { + return resultType; + } return DynamicType.instance; } return resultType; diff --git a/src/types/IntersectionType.ts b/src/types/IntersectionType.ts index a495b8489..34eac1caa 100644 --- a/src/types/IntersectionType.ts +++ b/src/types/IntersectionType.ts @@ -1,8 +1,8 @@ import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; -import { isAssociativeArrayType, isDynamicType, isIntersectionType, isObjectType, isTypedFunctionType } from '../astUtils/reflection'; +import { isDynamicType, isIntersectionType, isObjectType, isTypedFunctionType } from '../astUtils/reflection'; import { BscType } from './BscType'; -import { ReferenceType } from './ReferenceType'; -import { addAssociatedTypesTableAsSiblingToMemberTable, getAllTypesFromComplexType, isEnumTypeCompatible, joinTypesString, reduceTypesForIntersectionType } from './helpers'; +import { IntersectionWithDefaultDynamicReferenceType, ReferenceType } from './ReferenceType'; +import { addAssociatedTypesTableAsSiblingToMemberTable, getAllTypesFromComplexType, isEnumTypeCompatible, isTypeWithPotentialDefaultDynamicMember, joinTypesString, reduceTypesForIntersectionType } from './helpers'; import { BscTypeKind } from './BscTypeKind'; import type { TypeCacheEntry } from '../SymbolTable'; import { SymbolTable } from '../SymbolTable'; @@ -43,14 +43,17 @@ export class IntersectionType extends BscType { private getMemberTypeFromInnerTypes(name: string, options: GetTypeOptions): BscType { const typeFromMembers = this.types.map((innerType) => { - return innerType?.getMemberType(name, { ...options, ignoreAADefaultDynamicMembers: true }); + return innerType?.getMemberType(name, { ...options, ignoreDefaultDynamicMembers: true }); }); - const filteredTypes = reduceTypesForIntersectionType(typeFromMembers.filter(t => t !== undefined)); + let filteredTypes = reduceTypesForIntersectionType(typeFromMembers.map(t => t).filter(t => t !== undefined)); + if (filteredTypes.length === 0 && this.types.some(isTypeWithPotentialDefaultDynamicMember)) { + const typesFromMembersWithDynamicAA = this.types.map((innerType) => { + return innerType?.getMemberType(name, options); + }); + filteredTypes = reduceTypesForIntersectionType(typesFromMembersWithDynamicAA.map(t => t).filter(t => t !== undefined)); + } if (filteredTypes.length === 0) { - if (this.types.some(isAssociativeArrayType)) { - return DynamicType.instance; - } return undefined; } else if (filteredTypes.length === 1) { return filteredTypes[0]; @@ -80,6 +83,9 @@ export class IntersectionType extends BscType { getSymbolType: (innerName: string, innerOptions: GetTypeOptions) => { const referenceTypeInnerMemberTypes = this.getMemberTypeFromInnerTypes(name, options); if (!referenceTypeInnerMemberTypes) { + if (this.hasMemberTypeWithDefaultDynamicMember && !innerOptions.ignoreDefaultDynamicMembers) { + return DynamicType.instance; + } return undefined; } return referenceTypeInnerMemberTypes; @@ -93,6 +99,12 @@ export class IntersectionType extends BscType { }; }); } + if (!innerTypesMemberType?.isResolvable()) { + const shouldCreateDynamicAAMember = this.hasMemberTypeWithDefaultDynamicMember && !options.ignoreDefaultDynamicMembers; + if (shouldCreateDynamicAAMember) { + return new IntersectionWithDefaultDynamicReferenceType(innerTypesMemberType); + } + } return innerTypesMemberType; } @@ -107,7 +119,9 @@ export class IntersectionType extends BscType { getSymbolType: (innerName: string, innerOptions: GetTypeOptions) => { const referenceTypeInnerMemberType = this.getCallFuncFromInnerTypes(name, options); if (!referenceTypeInnerMemberType) { - return undefined; + if (this.hasMemberTypeWithDefaultDynamicMember && !innerOptions.ignoreDefaultDynamicMembers) { + return DynamicType.instance; + } } return referenceTypeInnerMemberType; }, @@ -121,6 +135,13 @@ export class IntersectionType extends BscType { }); } + if (!resultCallFuncType?.isResolvable()) { + const shouldCreateDynamicAAMember = this.hasMemberTypeWithDefaultDynamicMember && !options.ignoreDefaultDynamicMembers; + if (shouldCreateDynamicAAMember) { + return new IntersectionWithDefaultDynamicReferenceType(resultCallFuncType); + } + } + if (isTypedFunctionType(resultCallFuncType)) { const typesToCheck = [...resultCallFuncType.params.map(p => p.type), resultCallFuncType.returnType]; @@ -220,16 +241,6 @@ export class IntersectionType extends BscType { type.addBuiltInInterfaces(); for (const symbol of type.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime)) { const foundType = this.getMemberTypeFromInnerTypes(symbol.name, { flags: SymbolTypeFlag.runtime }); - /* - - const allResolvableTypes = foundType.reduce((acc, curType) => { - return acc && curType?.isResolvable(); - }, true); - - if (!allResolvableTypes) { - continue; - } - const uniqueType = getUniqueType(findTypeUnion(foundType), intersectionTypeFactory);*/ intersectionTable.addSymbol(symbol.name, {}, foundType, SymbolTypeFlag.runtime); } } @@ -240,18 +251,27 @@ export class IntersectionType extends BscType { firstType.addBuiltInInterfaces(); for (const symbol of firstType.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime)) { const foundType = this.getMemberTypeFromInnerTypes(symbol.name, { flags: SymbolTypeFlag.runtime }); - /* const allResolvableTypes = foundType.reduce((acc, curType) => { - return acc && curType?.isResolvable(); - }, true); - - if (!allResolvableTypes) { - continue; - } - const uniqueType = getUniqueType(findTypeUnion(foundType), unionTypeFactory);*/ intersectionTable.addSymbol(symbol.name, {}, foundType, SymbolTypeFlag.runtime); } return intersectionTable; } + + + private _hasMemberTypeWithDefaultDynamicMember: boolean = undefined; + get hasMemberTypeWithDefaultDynamicMember(): boolean { + if (this._hasMemberTypeWithDefaultDynamicMember !== undefined) { + return this._hasMemberTypeWithDefaultDynamicMember; + } + this._hasMemberTypeWithDefaultDynamicMember = false; + + for (const type of this.types) { + if (isTypeWithPotentialDefaultDynamicMember(type)) { + this._hasMemberTypeWithDefaultDynamicMember = true; + break; + } + } + return this._hasMemberTypeWithDefaultDynamicMember; + } } BuiltInInterfaceAdder.intersectionTypeFactory = (types: BscType[]) => { diff --git a/src/types/ObjectType.ts b/src/types/ObjectType.ts index 51c5bae16..39a715de6 100644 --- a/src/types/ObjectType.ts +++ b/src/types/ObjectType.ts @@ -28,8 +28,9 @@ export class ObjectType extends BscType { } getMemberType(name: string, options: GetTypeOptions) { - // TODO: How should we handle accessing properties of an object? - // For example, we could add fields as properties to m.top, but there could be other members added programmatically + if (options.ignoreDefaultDynamicMembers) { + return undefined; + } return DynamicType.instance; } diff --git a/src/types/ReferenceType.ts b/src/types/ReferenceType.ts index 5eb38ef4e..ff484c498 100644 --- a/src/types/ReferenceType.ts +++ b/src/types/ReferenceType.ts @@ -644,6 +644,51 @@ export class ParamTypeFromValueReferenceType extends BscType { } } + +export class IntersectionWithDefaultDynamicReferenceType extends BscType { + constructor(public baseType: BscType) { + super('IntersectionWithDefaultDynamicReferenceType'); + // eslint-disable-next-line no-constructor-return + return new Proxy(this, { + get: (target, propName, receiver) => { + + if (propName === '__reflection') { + // Cheeky way to get `isIntersectionWithDefaultDynamicReferenceType` reflection to work + return { name: 'IntersectionWithDefaultDynamicReferenceType' }; + } + + if (propName === 'isResolvable') { + return () => { + return true; + }; + } + let innerType = this.getTarget(); + + if (!innerType) { + innerType = DynamicType.instance; + } + + if (innerType) { + const result = Reflect.get(innerType, propName, innerType); + return result; + } + } + }); + } + + getTarget(): BscType { + if (isAnyReferenceType(this.baseType)) { + if (this.baseType.isResolvable()) { + return (this.baseType as any)?.getTarget(); + } + } + return this.baseType; + } + + tableProvider: SymbolTableProvider; +} + + /** * Gives an array of all the symbol names that need to be resolved to make the given reference type be resolved */ diff --git a/src/types/helpers.ts b/src/types/helpers.ts index e4507f314..5375099f8 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -1,5 +1,5 @@ import type { TypeCompatibilityData } from '../interfaces'; -import { isAnyReferenceType, isArrayDefaultTypeReferenceType, isComplexType, isDynamicType, isEnumMemberType, isEnumType, isInheritableType, isInterfaceType, isIntersectionType, isReferenceType, isTypePropertyReferenceType, isUnionType, isVoidType } from '../astUtils/reflection'; +import { isAnyReferenceType, isArrayDefaultTypeReferenceType, isAssociativeArrayTypeLike, isComplexType, isDynamicType, isEnumMemberType, isEnumType, isInheritableType, isInterfaceType, isIntersectionType, isObjectType, isReferenceType, isTypePropertyReferenceType, isUnionType, isUnionTypeOf, isVoidType } from '../astUtils/reflection'; import type { BscType } from './BscType'; import type { UnionType } from './UnionType'; import type { SymbolTable } from '../SymbolTable'; @@ -329,3 +329,11 @@ export function joinTypesString(types: BscType[], separator: string, thisTypeKin return t.toString(); }))].join(` ${separator} `); } + + +export function isTypeWithPotentialDefaultDynamicMember(type: BscType): boolean { + return (isInheritableType(type) && type.changeUnknownMemberToDynamic) || + isAssociativeArrayTypeLike(type) || + isObjectType(type) || + isUnionTypeOf(type, isTypeWithPotentialDefaultDynamicMember); +} diff --git a/src/util.ts b/src/util.ts index 4794dcacb..3882b2f61 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2203,7 +2203,7 @@ export class Util { let typeString = chainItem.type?.toString(); let typeToFindStringFor = chainItem.type; while (typeToFindStringFor) { - if (isUnionType(chainItem.type)) { + if (isComplexType(chainItem.type)) { typeString = `(${typeToFindStringFor.toString()})`; break; } else if (isCallableType(typeToFindStringFor)) { From a7c0f7b34797ef85a39653147b5d49c42acf6a86 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 9 Jan 2026 10:58:09 -0400 Subject: [PATCH 13/17] Added comment --- src/types/ReferenceType.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/types/ReferenceType.ts b/src/types/ReferenceType.ts index ff484c498..e3159b15e 100644 --- a/src/types/ReferenceType.ts +++ b/src/types/ReferenceType.ts @@ -644,7 +644,10 @@ export class ParamTypeFromValueReferenceType extends BscType { } } - +/** + * Used when an IntersectionType has at least one member that may have default dynamic members. + * If the inner type is not resolvable, this type will resolve to DynamicType. + */ export class IntersectionWithDefaultDynamicReferenceType extends BscType { constructor(public baseType: BscType) { super('IntersectionWithDefaultDynamicReferenceType'); From b288f8aedcd7cddedaa42328298a8a827929251d Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 12 Jan 2026 16:57:44 -0400 Subject: [PATCH 14/17] Added docs --- docs/intersection-types.md | 86 ++++++++++++++++++++++++++++++++++++++ docs/readme.md | 60 ++++++++++++++++++++++---- 2 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 docs/intersection-types.md diff --git a/docs/intersection-types.md b/docs/intersection-types.md new file mode 100644 index 000000000..a708d6254 --- /dev/null +++ b/docs/intersection-types.md @@ -0,0 +1,86 @@ +# Intersection Types + +BrighterScript Intersection Types are a way to define a type that combines the members of multiple types. They are similar to Intersection Types found in other languages, such as [TypeScript](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types). + +## Syntax + +Intersection types can be declared with the following syntax: ` and `. For example, the parameter to the function below could be meets both the interfaces HasId and HasUrl: + +```BrighterScript +interface HasId + id as string +end interface + +interface HasUrl + url as string +end interface + +function getUrlWithQueryId(value as HasId and HasUrl) as string + return value.url + "?id=" + value.id +end function +``` + +Any number of inner types, including classes or interfaces, could be part of an intersection: + +```BrighterScript +interface HasId + id as string +end interface + +interface HasUrl + url as string +end interface + +interface HasSize + width as integer + height as integer +end interface + + +function getUrlWithQuerySize(response as HasId and HasUrl and HasSize) as string + return value.url + "?id=" + value.id + "&w=" + value.width.toStr().trim() + "&h=" + value.height.toStr().trim() +end function +``` + +## Members and Validation + +A diagnostic error will be raised when a member is accessed that is not a member of any of the types of a union. Note also that if a member is not the same type in each of the types in the union, it will itself be considered an intersection. + +```BrighterScript +sub testIntersection(value as {id as string} and {id as integer}) + ' This is an error - "value.id" is of type "string AND integer" + printInteger(value.id) +end sub + +sub printInteger(x as integer) + print x +end sub +``` + +## Transpilation + +Since Brightscript does not have intersection types natively, intersection types will be transpiled as `dynamic`. + +```BrighterScript + +interface HasRadius + radius as float +end interface + +interface Point + x as float + y as float +end interface + +function getCircleDetails(circle as HasRadius and Point) as string + return "Circle: radius=" + circle.radius.toStr() + ", center=" + circle.x.toStr() + "," + circle.y.toStr() +end function +``` + +transpiles to + +```BrightScript +function getCircleDetails(circle as dynamic) as string + return "Circle: radius=" + circle.radius.ToStr() + ", center=" + circle.x.toStr() + "," + circle.y.toStr() +end function +``` diff --git a/docs/readme.md b/docs/readme.md index 5975f3f86..3e25219d0 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,9 +1,11 @@ # BrighterScript + BrighterScript is a superset of Roku's BrightScript language. Its goal is to provide new functionality and enhanced syntax support to enhance the Roku channel developer experience. See the following pages for more information: ## [Annotations](annotations.md) + ```brighterscript 'mostly useful for plugins that change code based on annotations @logOnException() @@ -13,12 +15,14 @@ end ``` ## [Callfunc Operator](callfunc-operator.md) + ```brighterscript 'instead of `node.callfunc("someMethod", 1, 2, 3)`, you can do this: node@.someMethod(1, 2, 3) ``` ## [Classes](classes.md) + ```brighterscript class Movie public title as string @@ -33,6 +37,7 @@ end class ``` ## [Constants](constants.md) + ```brighterscript const API_URL = "https://api.acme.com/v1/" sub main() @@ -41,6 +46,7 @@ end sub ``` ## [Enums](enums.md) + ```brighterscript enum RemoteButton up = "up" @@ -51,6 +57,7 @@ end enum ``` ## [Exceptions](exceptions.md) + ```brighterscript try somethingDangerous() @@ -59,7 +66,40 @@ catch 'look, no exception variable! end try ``` +## [Imports](imports.md) + +```brighterscript +import "pkg:/source/util.bs" +sub main() + print util_toUpper("hello world") +end sub +``` + +## [Interfaces](interfaces.md) + +```brighterscript +interface IMyComponent + top as roSGNodeMyComponent + + isSelected as boolean + selectedIndex as integer + + data as {id as string, isEpisode as boolean} +end interface +``` + +## [Intersection Types](intersection-types.md) + +```brighterscript +type MyClassAA = MyClass and roAssociativeArray + +sub addData(klass as MyClass and roAssociativeArray, data as roAssociativeArray) + return klass.append(data) +end sub +``` + ## [Namespaces](namespaces.md) + ```brighterscript namespace util function toUpper(value as string) @@ -72,28 +112,24 @@ sub main() end sub ``` -## [Imports](imports.md) -```brighterscript -import "pkg:/source/util.bs" -sub main() - print util_toUpper("hello world") -end sub -``` - ## [Null-coalescing operator](null-coalescing-operator.md) + ```brighterscript userSettings = getSettingsFromRegistry() ?? {} ``` ## [Plugins](plugins.md) + Plugins can be used to manipulate code at any point during the program lifecycle. ## [Regular Expression Literals](regex-literals.md) + ```brighterscript print /hello world/ig ``` ## [Source Literals](source-literals.md) + ```brighterscript print SOURCE_FILE_PATH print SOURCE_LINE_NUM @@ -103,7 +139,9 @@ print SOURCE_LOCATION print PKG_PATH print PKG_LOCATION ``` + ## [Template Strings (Template Literals)](template-strings.md) + ```brighterscript name = `John Smith` @@ -114,16 +152,19 @@ second line text` ``` ## [Ternary (Conditional) Operator](ternary-operator.md) + ```brighterscript authStatus = user <> invalid ? "logged in" : "not logged in" ``` ## [Typecasts](typecasts.md) + ```BrighterScript nodeId = (node as roSgNode).id ``` ## [Typed Arrays](typed-arrays.md) + ```brighterscript function getY(translation as float[]) as float yValue = -1 @@ -135,6 +176,7 @@ end function ``` ## [Type Statements](type-statements.md) + ```brighterscript type number = integer or float or double @@ -144,6 +186,7 @@ end function ``` ## [Union Types](union-types.md) + ```brighterscript sub logData(data as string or number) print data.toStr() @@ -151,4 +194,5 @@ end sub ``` ## [Variable Shadowing](variable-shadowing.md) + Name resolution rules for various types of shadowing. From 7836adf46a038d699b7243732efdf91ffe7da781 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Tue, 13 Jan 2026 09:23:09 -0400 Subject: [PATCH 15/17] Added completion tests --- .../completions/CompletionsProcessor.spec.ts | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/bscPlugin/completions/CompletionsProcessor.spec.ts b/src/bscPlugin/completions/CompletionsProcessor.spec.ts index dd0ee7928..1904b2b00 100644 --- a/src/bscPlugin/completions/CompletionsProcessor.spec.ts +++ b/src/bscPlugin/completions/CompletionsProcessor.spec.ts @@ -2587,6 +2587,7 @@ describe('CompletionsProcessor', () => { }]); }); }); + describe('incomplete statements', () => { it('should complete after if', () => { @@ -2609,4 +2610,118 @@ describe('CompletionsProcessor', () => { expect(completions.length).to.eql(1); }); }); + + describe('compound types', () => { + it('includes only common members of union types', () => { + program.setFile('source/main.bs', ` + interface Person + name as string + age as integer + end interface + + interface Employee + name as string + employeeId as integer + end interface + + sub greet(p as Person or Employee) + print p. + end sub + `); + program.validate(); + // print p.| + let completions = program.getCompletions('source/main.bs', util.createPosition(12, 29)); + expect(completions.length).to.eql(1); + expectCompletionsIncludes(completions, [{ + label: 'name', + kind: CompletionItemKind.Field + }]); + }); + + it('includes all members of intersection types', () => { + program.setFile('source/main.bs', ` + interface Person + name as string + age as integer + end interface + + interface Employee + name as string + employeeId as integer + end interface + + sub greet(p as Person and Employee) + print p. + end sub + `); + program.validate(); + // print p.| + let completions = program.getCompletions('source/main.bs', util.createPosition(12, 29)); + expect(completions.length).to.eql(3); + expectCompletionsIncludes(completions, [{ + label: 'name', + kind: CompletionItemKind.Field + }, { + label: 'age', + kind: CompletionItemKind.Field + }, { + label: 'employeeId', + kind: CompletionItemKind.Field + }]); + }); + + it('includes AA members when it is an intersection with an AA', () => { + program.setFile('source/main.bs', ` + interface Person + name as string + age as integer + end interface + + sub greet(p as Person and roAssociativeArray) + print p. + end sub + `); + program.validate(); + // print p.| + let completions = program.getCompletions('source/main.bs', util.createPosition(7, 29)); + expect(completions.length).to.at.least(4); + expectCompletionsIncludes(completions, [{ + label: 'name', // from Person + kind: CompletionItemKind.Field + }, { + label: 'age', // from Person + kind: CompletionItemKind.Field + }, { + label: 'Append', // from roAssociativeArray + kind: CompletionItemKind.Method + }, { + label: 'Count', // from roAssociativeArray + kind: CompletionItemKind.Method + }]); + }); + + it('includes members from non-dynamic when it is an intersection with dynamic', () => { + program.setFile('source/main.bs', ` + interface Person + name as string + age as integer + end interface + + sub greet(p as Person and dynamic) + print p. + end sub + `); + program.validate(); + // print p.| + let completions = program.getCompletions('source/main.bs', util.createPosition(7, 29)); + expect(completions.length).to.at.least(2); + expectCompletionsIncludes(completions, [{ + label: 'name', // from Person + kind: CompletionItemKind.Field + }, { + label: 'age', // from Person + kind: CompletionItemKind.Field + }]); + }); + }); }); From bbd1becd0815725bc0b89f0563772bae3aed9b7b Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Tue, 13 Jan 2026 09:25:30 -0400 Subject: [PATCH 16/17] Renamed 'ComplexType' -> 'CompoundType' --- src/CrossScopeValidator.ts | 4 ++-- src/astUtils/reflection.ts | 26 +++++++++++++------------- src/types/IntersectionType.ts | 4 ++-- src/types/UnionType.ts | 4 ++-- src/types/helpers.ts | 8 ++++---- src/util.ts | 6 +++--- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/CrossScopeValidator.ts b/src/CrossScopeValidator.ts index 00138643e..7d5962075 100644 --- a/src/CrossScopeValidator.ts +++ b/src/CrossScopeValidator.ts @@ -11,7 +11,7 @@ import type { ReferenceType } from './types/ReferenceType'; import { getAllRequiredSymbolNames } from './types/ReferenceType'; import type { TypeChainEntry, TypeChainProcessResult } from './interfaces'; import { BscTypeKind } from './types/BscTypeKind'; -import { getAllTypesFromComplexType } from './types/helpers'; +import { getAllTypesFromCompoundType } from './types/helpers'; import type { BscType } from './types/BscType'; import type { BscFile } from './files/BscFile'; import type { ClassStatement, ConstStatement, EnumMemberStatement, EnumStatement, InterfaceStatement, NamespaceStatement } from './parser/Statement'; @@ -222,7 +222,7 @@ export class CrossScopeValidator { } if (isUnionType(symbol.typeChain[0].type) && symbol.typeChain[0].data.isInstance) { - const allUnifiedTypes = getAllTypesFromComplexType(symbol.typeChain[0].type); + const allUnifiedTypes = getAllTypesFromCompoundType(symbol.typeChain[0].type); for (const unifiedType of allUnifiedTypes) { unnamespacedNameLowers.push(joinTypeChainForKey(symbol.typeChain, unifiedType)); } diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 20648d791..2fe8af243 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -340,7 +340,7 @@ export function isRoStringType(value: any): value is InterfaceType { return isBuiltInType(value, 'roString'); } export function isStringTypeLike(value: any): value is StringType | InterfaceType { - return isStringType(value) || isRoStringType(value) || isComplexTypeOf(value, isStringTypeLike); + return isStringType(value) || isRoStringType(value) || isCompoundTypeOf(value, isStringTypeLike); } export function isTypedFunctionType(value: any): value is TypedFunctionType { @@ -354,7 +354,7 @@ export function isRoFunctionType(value: any): value is InterfaceType { return value?.kind === BscTypeKind.RoFunctionType || isBuiltInType(value, 'roFunction'); } export function isFunctionTypeLike(value: any): value is FunctionType | InterfaceType { - return isFunctionType(value) || isRoFunctionType(value) || isComplexTypeOf(value, isFunctionTypeLike); + return isFunctionType(value) || isRoFunctionType(value) || isCompoundTypeOf(value, isFunctionTypeLike); } export function isBooleanType(value: any): value is BooleanType { @@ -364,7 +364,7 @@ export function isRoBooleanType(value: any): value is InterfaceType { return isBuiltInType(value, 'roBoolean'); } export function isBooleanTypeLike(value: any): value is BooleanType | InterfaceType { - return isBooleanType(value) || isRoBooleanType(value) || isComplexTypeOf(value, isBooleanTypeLike); + return isBooleanType(value) || isRoBooleanType(value) || isCompoundTypeOf(value, isBooleanTypeLike); } export function isIntegerType(value: any): value is IntegerType { @@ -374,7 +374,7 @@ export function isRoIntType(value: any): value is LongIntegerType { return isBuiltInType(value, 'roInt'); } export function isIntegerTypeLike(value: any): value is IntegerType | InterfaceType { - return isIntegerType(value) || isRoIntType(value) || isComplexTypeOf(value, isIntegerTypeLike); + return isIntegerType(value) || isRoIntType(value) || isCompoundTypeOf(value, isIntegerTypeLike); } export function isLongIntegerType(value: any): value is LongIntegerType { @@ -384,7 +384,7 @@ export function isRoLongIntegerType(value: any): value is InterfaceType { return isBuiltInType(value, 'roLongInteger'); } export function isLongIntegerTypeLike(value: any): value is LongIntegerType | InterfaceType { - return isLongIntegerType(value) || isRoLongIntegerType(value) || isComplexTypeOf(value, isLongIntegerTypeLike); + return isLongIntegerType(value) || isRoLongIntegerType(value) || isCompoundTypeOf(value, isLongIntegerTypeLike); } export function isFloatType(value: any): value is FloatType { @@ -394,7 +394,7 @@ export function isRoFloatType(value: any): value is InterfaceType { return isBuiltInType(value, 'roFloat'); } export function isFloatTypeLike(value: any): value is FloatType | InterfaceType { - return isFloatType(value) || isRoFloatType(value) || isComplexTypeOf(value, isFloatTypeLike); + return isFloatType(value) || isRoFloatType(value) || isCompoundTypeOf(value, isFloatTypeLike); } export function isDoubleType(value: any): value is DoubleType { @@ -404,7 +404,7 @@ export function isRoDoubleType(value: any): value is InterfaceType { return isBuiltInType(value, 'roDouble'); } export function isDoubleTypeLike(value: any): value is DoubleType | InterfaceType { - return isDoubleType(value) || isRoDoubleType(value) || isComplexTypeOf(value, isDoubleTypeLike); + return isDoubleType(value) || isRoDoubleType(value) || isCompoundTypeOf(value, isDoubleTypeLike); } export function isInvalidType(value: any): value is InvalidType { @@ -414,7 +414,7 @@ export function isRoInvalidType(value: any): value is InterfaceType { return isBuiltInType(value, 'roInvalid'); } export function isInvalidTypeLike(value: any): value is InvalidType | InterfaceType { - return isInvalidType(value) || isRoInvalidType(value) || isComplexTypeOf(value, isInvalidTypeLike); + return isInvalidType(value) || isRoInvalidType(value) || isCompoundTypeOf(value, isInvalidTypeLike); } export function isVoidType(value: any): value is VoidType { @@ -486,7 +486,7 @@ export function isInheritableType(target): target is InheritableType { } export function isCallFuncableType(target): target is CallFuncableType { - return isInterfaceType(target) || isComponentType(target) || isComplexTypeOf(target, isCallFuncableType); + return isInterfaceType(target) || isComponentType(target) || isCompoundTypeOf(target, isCallFuncableType); } export function isCallableType(target): target is BaseFunctionType { @@ -510,7 +510,7 @@ export function isNumberTypeLike(value: any): value is IntegerType | LongInteger isLongIntegerTypeLike(value) || isFloatTypeLike(value) || isDoubleTypeLike(value) || - isComplexTypeOf(value, isNumberTypeLike); + isCompoundTypeOf(value, isNumberTypeLike); } export function isPrimitiveType(value: any = false): value is IntegerType | LongIntegerType | FloatType | DoubleType | StringType | BooleanType | InterfaceType { @@ -527,7 +527,7 @@ export function isPrimitiveTypeLike(value: any = false): value is IntegerType | } export function isAssociativeArrayTypeLike(value: any): value is AssociativeArrayType | InterfaceType { - return value?.kind === BscTypeKind.AssociativeArrayType || isBuiltInType(value, 'roAssociativeArray') || isComplexTypeOf(value, isAssociativeArrayTypeLike); + return value?.kind === BscTypeKind.AssociativeArrayType || isBuiltInType(value, 'roAssociativeArray') || isCompoundTypeOf(value, isAssociativeArrayTypeLike); } export function isBuiltInType(value: any, name: string): value is InterfaceType { @@ -553,14 +553,14 @@ export function isUnionTypeOf(value: any, typeGuard: (val: any) => boolean) { return isUnionType(value) && value.types.every(typeGuard); } -export function isComplexTypeOf(value: any, typeGuard: (val: any) => boolean) { +export function isCompoundTypeOf(value: any, typeGuard: (val: any) => boolean) { // TODO: add more complex type checks as needed, like IntersectionType return isTypeStatementTypeOf(value, typeGuard) || isUnionTypeOf(value, typeGuard); } -export function isComplexType(value: any): value is UnionType | IntersectionType { +export function isCompoundType(value: any): value is UnionType | IntersectionType { return isUnionType(value) || isIntersectionType(value); } diff --git a/src/types/IntersectionType.ts b/src/types/IntersectionType.ts index 34eac1caa..24b1fad7e 100644 --- a/src/types/IntersectionType.ts +++ b/src/types/IntersectionType.ts @@ -2,7 +2,7 @@ import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; import { isDynamicType, isIntersectionType, isObjectType, isTypedFunctionType } from '../astUtils/reflection'; import { BscType } from './BscType'; import { IntersectionWithDefaultDynamicReferenceType, ReferenceType } from './ReferenceType'; -import { addAssociatedTypesTableAsSiblingToMemberTable, getAllTypesFromComplexType, isEnumTypeCompatible, isTypeWithPotentialDefaultDynamicMember, joinTypesString, reduceTypesForIntersectionType } from './helpers'; +import { addAssociatedTypesTableAsSiblingToMemberTable, getAllTypesFromCompoundType, isEnumTypeCompatible, isTypeWithPotentialDefaultDynamicMember, joinTypesString, reduceTypesForIntersectionType } from './helpers'; import { BscTypeKind } from './BscTypeKind'; import type { TypeCacheEntry } from '../SymbolTable'; import { SymbolTable } from '../SymbolTable'; @@ -198,7 +198,7 @@ export class IntersectionType extends BscType { * Used for transpilation */ toTypeString(): string { - const uniqueTypeStrings = new Set(getAllTypesFromComplexType(this).map(t => t.toTypeString())); + const uniqueTypeStrings = new Set(getAllTypesFromCompoundType(this).map(t => t.toTypeString())); if (uniqueTypeStrings.size === 1) { return uniqueTypeStrings.values().next().value; diff --git a/src/types/UnionType.ts b/src/types/UnionType.ts index 91e620bbb..0c657a3ea 100644 --- a/src/types/UnionType.ts +++ b/src/types/UnionType.ts @@ -2,7 +2,7 @@ import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces'; import { isDynamicType, isObjectType, isTypedFunctionType, isUnionType } from '../astUtils/reflection'; import { BscType } from './BscType'; import { ReferenceType } from './ReferenceType'; -import { addAssociatedTypesTableAsSiblingToMemberTable, findTypeUnion, findTypeUnionDeepCheck, getAllTypesFromComplexType, getUniqueType, isEnumTypeCompatible, joinTypesString } from './helpers'; +import { addAssociatedTypesTableAsSiblingToMemberTable, findTypeUnion, findTypeUnionDeepCheck, getAllTypesFromCompoundType, getUniqueType, isEnumTypeCompatible, joinTypesString } from './helpers'; import { BscTypeKind } from './BscTypeKind'; import type { TypeCacheEntry } from '../SymbolTable'; import { SymbolTable } from '../SymbolTable'; @@ -149,7 +149,7 @@ export class UnionType extends BscType { * Used for transpilation */ toTypeString(): string { - const uniqueTypeStrings = new Set(getAllTypesFromComplexType(this).map(t => t.toTypeString())); + const uniqueTypeStrings = new Set(getAllTypesFromCompoundType(this).map(t => t.toTypeString())); if (uniqueTypeStrings.size === 1) { return uniqueTypeStrings.values().next().value; diff --git a/src/types/helpers.ts b/src/types/helpers.ts index 5375099f8..0fc9af8b9 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -1,5 +1,5 @@ import type { TypeCompatibilityData } from '../interfaces'; -import { isAnyReferenceType, isArrayDefaultTypeReferenceType, isAssociativeArrayTypeLike, isComplexType, isDynamicType, isEnumMemberType, isEnumType, isInheritableType, isInterfaceType, isIntersectionType, isObjectType, isReferenceType, isTypePropertyReferenceType, isUnionType, isUnionTypeOf, isVoidType } from '../astUtils/reflection'; +import { isAnyReferenceType, isArrayDefaultTypeReferenceType, isAssociativeArrayTypeLike, isCompoundType, isDynamicType, isEnumMemberType, isEnumType, isInheritableType, isInterfaceType, isIntersectionType, isObjectType, isReferenceType, isTypePropertyReferenceType, isUnionType, isUnionTypeOf, isVoidType } from '../astUtils/reflection'; import type { BscType } from './BscType'; import type { UnionType } from './UnionType'; import type { SymbolTable } from '../SymbolTable'; @@ -287,12 +287,12 @@ export function isNativeInterfaceCompatibleNumber(thisType: BscType, otherType: return false; } -export function getAllTypesFromComplexType(complex: UnionType | IntersectionType): BscType[] { +export function getAllTypesFromCompoundType(complex: UnionType | IntersectionType): BscType[] { const results = []; for (const type of complex.types) { - if (isComplexType(type)) { - results.push(...getAllTypesFromComplexType(type)); + if (isCompoundType(type)) { + results.push(...getAllTypesFromCompoundType(type)); } else { results.push(type); } diff --git a/src/util.ts b/src/util.ts index 3882b2f61..d2231eab7 100644 --- a/src/util.ts +++ b/src/util.ts @@ -25,7 +25,7 @@ import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionP import { LogLevel, createLogger } from './logging'; import { isToken, type Identifier, type Token } from './lexer/Token'; import { TokenKind } from './lexer/TokenKind'; -import { isAnyReferenceType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isComplexType, isComponentType, isDottedGetExpression, isDoubleTypeLike, isDynamicType, isEnumMemberType, isExpression, isFloatTypeLike, isIndexedGetExpression, isIntegerTypeLike, isIntersectionType, isInvalidTypeLike, isLiteralString, isLongIntegerTypeLike, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberTypeLike, isObjectType, isPrimitiveType, isReferenceType, isStatement, isStringTypeLike, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection'; +import { isAnyReferenceType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isCompoundType, isComponentType, isDottedGetExpression, isDoubleTypeLike, isDynamicType, isEnumMemberType, isExpression, isFloatTypeLike, isIndexedGetExpression, isIntegerTypeLike, isIntersectionType, isInvalidTypeLike, isLiteralString, isLongIntegerTypeLike, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberTypeLike, isObjectType, isPrimitiveType, isReferenceType, isStatement, isStringTypeLike, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection'; import { WalkMode } from './astUtils/visitors'; import { SourceNode } from 'source-map'; import * as requireRelative from 'require-relative'; @@ -2203,7 +2203,7 @@ export class Util { let typeString = chainItem.type?.toString(); let typeToFindStringFor = chainItem.type; while (typeToFindStringFor) { - if (isComplexType(chainItem.type)) { + if (isCompoundType(chainItem.type)) { typeString = `(${typeToFindStringFor.toString()})`; break; } else if (isCallableType(typeToFindStringFor)) { @@ -2328,7 +2328,7 @@ export class Util { } public getCustomTypesInSymbolTree(setToFill: Set, type: BscType, filter?: (t: BscType) => boolean) { - const subSymbolTypes = isComplexType(type) + const subSymbolTypes = isCompoundType(type) ? type.types : type.getMemberTable()?.getAllSymbols(SymbolTypeFlag.runtime).map(sym => sym.type) ?? []; for (const subSymbolType of subSymbolTypes) { From 492e7ad7c16a396e075707f4f857d477ee3dab6e Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Tue, 13 Jan 2026 10:23:24 -0400 Subject: [PATCH 17/17] More tests for Intersections with order of operations and callfuncs --- src/astUtils/reflection.ts | 16 +- .../validation/ScopeValidator.spec.ts | 224 ++++++++++++++++++ src/bscPlugin/validation/ScopeValidator.ts | 4 +- src/types/InterfaceType.ts | 4 +- 4 files changed, 240 insertions(+), 8 deletions(-) diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 2fe8af243..4143e9f1e 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -41,6 +41,8 @@ import type { Program } from '../Program'; import type { Project } from '../lsp/Project'; import type { IntersectionType } from '../types/IntersectionType'; import type { TypeStatementType } from '../types/TypeStatementType'; +import type { BscType } from '../types/BscType'; +import type { SymbolTable } from '../SymbolTable'; // File reflection @@ -482,7 +484,7 @@ export function isTypeStatementType(value: any): value is TypeStatementType { } export function isInheritableType(target): target is InheritableType { - return isClassType(target) || isCallFuncableType(target); + return isClassType(target) || isInterfaceType(target) || isComponentType(target); } export function isCallFuncableType(target): target is CallFuncableType { @@ -530,6 +532,10 @@ export function isAssociativeArrayTypeLike(value: any): value is AssociativeArra return value?.kind === BscTypeKind.AssociativeArrayType || isBuiltInType(value, 'roAssociativeArray') || isCompoundTypeOf(value, isAssociativeArrayTypeLike); } +export function isCallFuncableTypeLike(target): target is BscType & { callFuncMemberTable: SymbolTable } { + return isCallFuncableType(target) || isCompoundTypeOf(target, isCallFuncableTypeLike); +} + export function isBuiltInType(value: any, name: string): value is InterfaceType { return (isInterfaceType(value) && value.name.toLowerCase() === name.toLowerCase() && value.isBuiltIn) || (isTypeStatementType(value) && isBuiltInType(value.wrappedType, name)); @@ -552,14 +558,16 @@ export function isTypeStatementTypeOf(value: any, typeGuard: (val: any) => boole export function isUnionTypeOf(value: any, typeGuard: (val: any) => boolean) { return isUnionType(value) && value.types.every(typeGuard); } +export function isIntersectionTypeOf(value: any, typeGuard: (val: any) => boolean) { + return isIntersectionType(value) && value.types.some(typeGuard); +} export function isCompoundTypeOf(value: any, typeGuard: (val: any) => boolean) { - // TODO: add more complex type checks as needed, like IntersectionType return isTypeStatementTypeOf(value, typeGuard) || - isUnionTypeOf(value, typeGuard); + isUnionTypeOf(value, typeGuard) || + isIntersectionTypeOf(value, typeGuard); } - export function isCompoundType(value: any): value is UnionType | IntersectionType { return isUnionType(value) || isIntersectionType(value); } diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index e21a988fc..aee1a478f 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -2027,6 +2027,82 @@ describe('ScopeValidator', () => { }).message ]); }); + + it('accepts a valid intersection when parameter is a union with an intersection', () => { + program.setFile('source/main.bs', ` + sub fooA(x as {a as integer} and {b as string} or {c as float}) + ' noop + end sub + + + sub fooB(y as object) + fooA({a: 32, b: y}) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('validates an incomplete intersection when parameter is a union with an intersection', () => { + program.setFile('source/main.bs', ` + sub fooA(x as {a as integer} and {b as string} or {c as float}) + ' noop + end sub + + + sub fooB(y as object) + fooA({a: 32}) + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.argumentTypeMismatch('roAssociativeArray', '({a as integer} and {b as string}) or {c as float}', { + missingFields: [ + { name: 'b', expectedType: StringType.instance }, + { name: 'c', expectedType: FloatType.instance } + ] + }).message + ]); + }); + + it('accepts a valid intersection when parameter is an intersection with a union', () => { + program.setFile('source/main.bs', ` + sub fooA(x as {a as integer} and ({b as string} or {c as float})) + ' noop + end sub + + + sub fooB(y as dynamic) + fooA({a: 32, b: y}) ' meets first half of union + fooA({a: 32, c: y}) ' meets second half of union + fooA({a: 32, b: "hello", c: 2.178}) ' meets both halves of union + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('validates an incomplete intersection when parameter is an intersection with a union', () => { + program.setFile('source/main.bs', ` + sub fooA(x as {a as integer} and ({b as string} or {c as float})) + ' noop + end sub + + + sub fooB(y as object) + fooA({a: 32}) + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.argumentTypeMismatch('roAssociativeArray', '{a as integer} and ({b as string} or {c as float})', { + missingFields: [ + { name: 'b', expectedType: StringType.instance }, + { name: 'c', expectedType: FloatType.instance } + ] + }).message + ]); + }); }); }); @@ -6187,5 +6263,153 @@ describe('ScopeValidator', () => { }).message ]); }); + + it('allows callfunc on intersection of callfuncable types', () => { + program.setFile('components/Widget.xml', trim` + + +