Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0d9745e
wip
markwpearce Dec 5, 2025
4849aad
Merge branch 'v1' into intersection_type
markwpearce Dec 5, 2025
a742506
Merge branch 'v1' into intersection_type
markwpearce Dec 15, 2025
8fde59b
Added IntersectionType tests
markwpearce Dec 15, 2025
48ac3f0
Added support for grouped type expressions
markwpearce Dec 15, 2025
a602261
Merge branch 'v1' into intersection_type
markwpearce Dec 15, 2025
723b945
Adding tests for member types of intersection types in scope contexts
markwpearce Dec 17, 2025
e202859
Adds code to handle order of operations when building types
markwpearce Jan 1, 2026
02ba2bb
Adds code to handle order of operations when building types
markwpearce Jan 1, 2026
29022b7
removed log statments, started adding validation tests
markwpearce Jan 2, 2026
6a73946
remove .only
markwpearce Jan 2, 2026
e49662b
Better handling of errors - missing right paren on function declratio…
markwpearce Jan 5, 2026
8f72d55
fix lint error
markwpearce Jan 5, 2026
e7a8074
Adds more validation and compatibility tests for intersections:
markwpearce Jan 6, 2026
9305d48
Adds new reference type for intersections with types with default dyn…
markwpearce Jan 9, 2026
a7c0f7b
Added comment
markwpearce Jan 9, 2026
b288f8a
Added docs
markwpearce Jan 12, 2026
7836adf
Added completion tests
markwpearce Jan 13, 2026
bbd1bec
Renamed 'ComplexType' -> 'CompoundType'
markwpearce Jan 13, 2026
492e7ad
More tests for Intersections with order of operations and callfuncs
markwpearce Jan 13, 2026
5e648aa
Merge branch 'v1' into intersection_type
markwpearce Jan 13, 2026
d277935
Merge branch 'v1' into intersection_type
TwitchBronBron Jan 20, 2026
92a7755
Update docs/intersection-types.md
markwpearce Jan 20, 2026
e129f4b
Changed Name IntersectionWithDefaultDynamicReferenceType -> Reference…
markwpearce Jan 22, 2026
db1138c
added more comments to ReferenceTypeWithDefault
markwpearce Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/CrossScopeValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
}
Expand Down
228 changes: 226 additions & 2 deletions src/Scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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();
Expand Down Expand Up @@ -2837,6 +2838,229 @@ describe('Scope', () => {
expect(resultType.types.map(t => t.toString())).includes(IntegerType.instance.toString());
});

it('should handle union types grouped', () => {
const mainFile = program.setFile<BrsFile>('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<BrsFile>('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<BrsFile>('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<BrsFile>('source/main.bs', `
sub printInfo(thing as Person and Pet)
name = thing.name
print name
legs = thing.legs
print legs
end sub
`);
program.setFile<BrsFile>('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<BrsFile>('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<BrsFile>('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', () => {
Expand Down
10 changes: 9 additions & 1 deletion src/astUtils/reflection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
import type { TypeStatementType } from '../types/TypeStatementType';


Expand Down Expand Up @@ -461,6 +462,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;
}
Expand All @@ -475,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 {
Expand Down Expand Up @@ -549,6 +553,10 @@ export function isComplexTypeOf(value: any, typeGuard: (val: any) => boolean) {
}


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 } {
Expand Down
Loading