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. diff --git a/src/CrossScopeValidator.ts b/src/CrossScopeValidator.ts index 517fe30c8..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 { getAllTypesFromUnionType } 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 = getAllTypesFromUnionType(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/Scope.spec.ts b/src/Scope.spec.ts index 68c5f9483..efc5b88c7 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, ValidateScopeEvent } 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'; @@ -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(); @@ -2837,6 +2838,307 @@ 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('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); + }); + + 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; + }); + + 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 31cbac6c8..4143e9f1e 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'; @@ -39,7 +39,10 @@ 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'; import type { TypeStatementType } from '../types/TypeStatementType'; +import type { BscType } from '../types/BscType'; +import type { SymbolTable } from '../SymbolTable'; // File reflection @@ -339,7 +342,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 { @@ -353,7 +356,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 { @@ -363,7 +366,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 { @@ -373,7 +376,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 { @@ -383,7 +386,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 { @@ -393,7 +396,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 { @@ -403,7 +406,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 { @@ -413,7 +416,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 { @@ -455,12 +458,18 @@ 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; } 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; } @@ -475,11 +484,11 @@ 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) || isInterfaceType(target) || isComponentType(target); } 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 { @@ -488,7 +497,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 { @@ -503,7 +512,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 { @@ -519,6 +528,14 @@ 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') || 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)); @@ -541,13 +558,19 @@ 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 isComplexTypeOf(value: any, typeGuard: (val: any) => boolean) { - // TODO: add more complex type checks as needed, like IntersectionType +export function isCompoundTypeOf(value: any, typeGuard: (val: any) => boolean) { 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); +} // Literal reflection 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 + }]); + }); + }); }); diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index 607de3aa4..99460bae5 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('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,175 @@ describe('ScopeValidator', () => { program.validate(); expectZeroDiagnostics(program); }); + + describe('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 + ]); + }); + + 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 + ]); + }); + }); }); describe('cannotFindName', () => { @@ -2466,6 +2654,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', () => { @@ -3150,6 +3415,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', () => { @@ -3626,7 +3970,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 `); @@ -3640,7 +3984,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 `); @@ -5919,5 +6263,153 @@ describe('ScopeValidator', () => { }).message ]); }); + + it('allows callfunc on intersection of callfuncable types', () => { + program.setFile('components/Widget.xml', trim` + + +