Skip to content

Commit 9305d48

Browse files
committed
Adds new reference type for intersections with types with default dynamic members
1 parent e7a8074 commit 9305d48

File tree

11 files changed

+278
-39
lines changed

11 files changed

+278
-39
lines changed

src/Scope.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3061,6 +3061,84 @@ describe('Scope', () => {
30613061
expect(stmt.tokens.type.text).to.eq('type');
30623062
expect(stmt.value).to.exist;
30633063
});
3064+
3065+
it('unknown members of intersections with AA types return dynamic', () => {
3066+
const mainFile = program.setFile<BrsFile>('source/main.bs', `
3067+
sub printData(data as {customData as string} and roAssociativeArray)
3068+
x = data.someDynamicKey
3069+
y = data.customData
3070+
print x
3071+
print y
3072+
end sub
3073+
`);
3074+
program.validate();
3075+
expectZeroDiagnostics(program);
3076+
const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24));
3077+
const sourceScope = program.getScopeByName('source');
3078+
expect(sourceScope).to.exist;
3079+
sourceScope.linkSymbolTable();
3080+
expect(mainFnScope).to.exist;
3081+
const mainSymbolTable = mainFnScope.symbolTable;
3082+
const dataType = mainSymbolTable.getSymbolType('data', { flags: SymbolTypeFlag.runtime }) as IntersectionType;
3083+
expectTypeToBe(dataType, IntersectionType);
3084+
expect(dataType.types).to.have.lengthOf(2);
3085+
const xType = mainSymbolTable.getSymbolType('x', { flags: SymbolTypeFlag.runtime });
3086+
expectTypeToBe(xType, DynamicType);
3087+
const yType = mainSymbolTable.getSymbolType('y', { flags: SymbolTypeFlag.runtime });
3088+
expectTypeToBe(yType, StringType);
3089+
});
3090+
3091+
it('unknown members of intersections with object type return dynamic', () => {
3092+
const mainFile = program.setFile<BrsFile>('source/main.bs', `
3093+
sub printData(data as {customData as string} and object)
3094+
x = data.someDynamicKey
3095+
y = data.customData
3096+
print x
3097+
print y
3098+
end sub
3099+
`);
3100+
program.validate();
3101+
expectZeroDiagnostics(program);
3102+
const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24));
3103+
const sourceScope = program.getScopeByName('source');
3104+
expect(sourceScope).to.exist;
3105+
sourceScope.linkSymbolTable();
3106+
expect(mainFnScope).to.exist;
3107+
const mainSymbolTable = mainFnScope.symbolTable;
3108+
const dataType = mainSymbolTable.getSymbolType('data', { flags: SymbolTypeFlag.runtime }) as IntersectionType;
3109+
expectTypeToBe(dataType, IntersectionType);
3110+
expect(dataType.types).to.have.lengthOf(2);
3111+
const xType = mainSymbolTable.getSymbolType('x', { flags: SymbolTypeFlag.runtime });
3112+
expectTypeToBe(xType, DynamicType);
3113+
const yType = mainSymbolTable.getSymbolType('y', { flags: SymbolTypeFlag.runtime });
3114+
expectTypeToBe(yType, StringType);
3115+
});
3116+
3117+
it('order doesnt matter for intersections with object type and finding members', () => {
3118+
const mainFile = program.setFile<BrsFile>('source/main.bs', `
3119+
sub printData(data as object and {customData as string})
3120+
x = data.someDynamicKey
3121+
y = data.customData
3122+
print x
3123+
print y
3124+
end sub
3125+
`);
3126+
program.validate();
3127+
expectZeroDiagnostics(program);
3128+
const mainFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24));
3129+
const sourceScope = program.getScopeByName('source');
3130+
expect(sourceScope).to.exist;
3131+
sourceScope.linkSymbolTable();
3132+
expect(mainFnScope).to.exist;
3133+
const mainSymbolTable = mainFnScope.symbolTable;
3134+
const dataType = mainSymbolTable.getSymbolType('data', { flags: SymbolTypeFlag.runtime }) as IntersectionType;
3135+
expectTypeToBe(dataType, IntersectionType);
3136+
expect(dataType.types).to.have.lengthOf(2);
3137+
const xType = mainSymbolTable.getSymbolType('x', { flags: SymbolTypeFlag.runtime });
3138+
expectTypeToBe(xType, DynamicType);
3139+
const yType = mainSymbolTable.getSymbolType('y', { flags: SymbolTypeFlag.runtime });
3140+
expectTypeToBe(yType, StringType);
3141+
});
30643142
});
30653143

30663144
describe('type casts', () => {

src/astUtils/reflection.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type { ObjectType } from '../types/ObjectType';
2323
import type { AstNode, Expression, Statement } from '../parser/AstNode';
2424
import type { AssetFile } from '../files/AssetFile';
2525
import { AstNodeKind } from '../parser/AstNode';
26-
import type { TypePropertyReferenceType, ReferenceType, BinaryOperatorReferenceType, ArrayDefaultTypeReferenceType, AnyReferenceType, ParamTypeFromValueReferenceType } from '../types/ReferenceType';
26+
import type { TypePropertyReferenceType, ReferenceType, BinaryOperatorReferenceType, ArrayDefaultTypeReferenceType, AnyReferenceType, ParamTypeFromValueReferenceType, IntersectionWithDefaultDynamicReferenceType } from '../types/ReferenceType';
2727
import type { EnumMemberType, EnumType } from '../types/EnumType';
2828
import type { UnionType } from '../types/UnionType';
2929
import type { UninitializedType } from '../types/UninitializedType';
@@ -456,6 +456,9 @@ export function isArrayDefaultTypeReferenceType(value: any): value is ArrayDefau
456456
export function isParamTypeFromValueReferenceType(value: any): value is ParamTypeFromValueReferenceType {
457457
return value?.__reflection?.name === 'ParamTypeFromValueReferenceType';
458458
}
459+
export function isIntersectionWithDefaultDynamicReferenceType(value: any): value is IntersectionWithDefaultDynamicReferenceType {
460+
return value?.__reflection?.name === 'IntersectionWithDefaultDynamicReferenceType';
461+
}
459462
export function isNamespaceType(value: any): value is NamespaceType {
460463
return value?.kind === BscTypeKind.NamespaceType;
461464
}
@@ -492,7 +495,7 @@ export function isCallableType(target): target is BaseFunctionType {
492495

493496
export function isAnyReferenceType(target): target is AnyReferenceType {
494497
const name = target?.__reflection?.name;
495-
return name === 'ReferenceType' || name === 'TypePropertyReferenceType' || name === 'BinaryOperatorReferenceType' || name === 'ArrayDefaultTypeReferenceType' || name === 'ParamTypeFromValueReferenceType';
498+
return name === 'ReferenceType' || name === 'TypePropertyReferenceType' || name === 'BinaryOperatorReferenceType' || name === 'ArrayDefaultTypeReferenceType' || name === 'ParamTypeFromValueReferenceType' || name === 'IntersectionWithDefaultDynamicReferenceType';
496499
}
497500

498501
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 |
523526
isTypeStatementTypeOf(value, isPrimitiveTypeLike);
524527
}
525528

529+
export function isAssociativeArrayTypeLike(value: any): value is AssociativeArrayType | InterfaceType {
530+
return value?.kind === BscTypeKind.AssociativeArrayType || isBuiltInType(value, 'roAssociativeArray') || isComplexTypeOf(value, isAssociativeArrayTypeLike);
531+
}
532+
526533
export function isBuiltInType(value: any, name: string): value is InterfaceType {
527534
return (isInterfaceType(value) && value.name.toLowerCase() === name.toLowerCase() && value.isBuiltIn) ||
528535
(isTypeStatementType(value) && isBuiltInType(value.wrappedType, name));

src/bscPlugin/validation/ScopeValidator.spec.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2578,6 +2578,83 @@ describe('ScopeValidator', () => {
25782578
]);
25792579
});
25802580
});
2581+
2582+
describe('intersection types', () => {
2583+
it('finds members from intersection types', () => {
2584+
program.setFile('source/main.bs', `
2585+
interface IFirst
2586+
num as integer
2587+
end interface
2588+
interface ISecond
2589+
function doThing2(a as integer, b as string) as void
2590+
end interface
2591+
2592+
sub main(thing as IFirst and ISecond)
2593+
print thing.num
2594+
thing.doThing2(thing.num, "hello")
2595+
end sub
2596+
`);
2597+
program.validate();
2598+
expectZeroDiagnostics(program);
2599+
});
2600+
2601+
it('finds random members of intersections with AA types', () => {
2602+
program.setFile<BrsFile>('source/main.bs', `
2603+
sub printData(data as {customData as string} and roAssociativeArray)
2604+
x = data.someDynamicKey
2605+
y = data.customData
2606+
print x
2607+
print y
2608+
end sub
2609+
`);
2610+
program.validate();
2611+
expectZeroDiagnostics(program);
2612+
});
2613+
2614+
2615+
it('handles type statements that are intersections of classes and AA', () => {
2616+
program.setFile<BrsFile>('source/main.bs', `
2617+
sub printData(data as MyKlassAA)
2618+
x = data.customData
2619+
data.append({
2620+
newKey: "newValue"
2621+
})
2622+
print x
2623+
y = data.newKey
2624+
print y
2625+
end sub
2626+
2627+
2628+
type MyKlassAA = MyKlass and roAssociativeArray
2629+
2630+
class MyKlass
2631+
customData as string
2632+
end class
2633+
`);
2634+
program.validate();
2635+
expectZeroDiagnostics(program);
2636+
});
2637+
2638+
it('validates missing members from intersection types', () => {
2639+
program.setFile('source/main.bs', `
2640+
interface IFirst
2641+
num as integer
2642+
end interface
2643+
interface ISecond
2644+
function doThing2(a as integer, b as string) as void
2645+
end interface
2646+
2647+
sub main(thing as IFirst and ISecond)
2648+
print thing.nonExistentMember
2649+
thing.doThing2(thing.num, "hello")
2650+
end sub
2651+
`);
2652+
program.validate();
2653+
expectDiagnostics(program, [
2654+
DiagnosticMessages.cannotFindName('nonExistentMember', '(IFirst and ISecond).nonExistentMember', '(IFirst and ISecond)')
2655+
]);
2656+
});
2657+
});
25812658
});
25822659

25832660
describe('itemCannotBeUsedAsVariable', () => {

src/interfaces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,9 +1043,9 @@ export interface GetTypeOptions {
10431043
statementIndex?: number | 'end';
10441044
ignoreParentTables?: boolean;
10451045
/**
1046-
* If this is true, AA's do not return dynamic if no member is found
1046+
* If this is true, AA's, objects, nodes, etc, do not return dynamic if no member is found
10471047
*/
1048-
ignoreAADefaultDynamicMembers?: boolean;
1048+
ignoreDefaultDynamicMembers?: boolean;
10491049
}
10501050

10511051
export class TypeChainEntry {

src/types/AssociativeArrayType.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SymbolTable } from '../SymbolTable';
22
import { SymbolTypeFlag } from '../SymbolTypeFlag';
3-
import { isAssociativeArrayType, isClassType, isDynamicType, isObjectType } from '../astUtils/reflection';
3+
import { isAssociativeArrayType, isAssociativeArrayTypeLike, isClassType, isDynamicType, isObjectType } from '../astUtils/reflection';
44
import type { GetTypeOptions, TypeCompatibilityData } from '../interfaces';
55
import { BscType } from './BscType';
66
import { BscTypeKind } from './BscTypeKind';
@@ -21,7 +21,7 @@ export class AssociativeArrayType extends BscType {
2121
return true;
2222
} else if (isUnionTypeCompatible(this, targetType)) {
2323
return true;
24-
} else if (isAssociativeArrayType(targetType)) {
24+
} else if (isAssociativeArrayTypeLike(targetType)) {
2525
return true;
2626
} else if (this.checkCompatibilityBasedOnMembers(targetType, SymbolTypeFlag.runtime, data)) {
2727
return true;
@@ -42,7 +42,7 @@ export class AssociativeArrayType extends BscType {
4242
getMemberType(name: string, options: GetTypeOptions) {
4343
// if a member has specifically been added, cool. otherwise, assume dynamic
4444
const memberType = super.getMemberType(name, options);
45-
if (!memberType && !options.ignoreAADefaultDynamicMembers) {
45+
if (!memberType && !options.ignoreDefaultDynamicMembers) {
4646
return DynamicType.instance;
4747
}
4848
return memberType;

src/types/InheritableType.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ export abstract class InheritableType extends BscType {
2020
let hasRoAssociativeArrayAsAncestor = this.name.toLowerCase() === 'roassociativearray' || this.getAncestorTypeList()?.find(ancestorType => ancestorType.name.toLowerCase() === 'roassociativearray');
2121

2222
if (hasRoAssociativeArrayAsAncestor) {
23-
return super.getMemberType(memberName, options) ?? (!options?.ignoreAADefaultDynamicMembers ? DynamicType.instance : undefined);
23+
return super.getMemberType(memberName, options) ?? (!options?.ignoreDefaultDynamicMembers ? DynamicType.instance : undefined);
2424
}
2525

2626
const resultType = super.getMemberType(memberName, { ...options, fullName: memberName, tableProvider: () => this.memberTable });
2727

2828
if (this.changeUnknownMemberToDynamic && !resultType.isResolvable()) {
29+
if (options.ignoreDefaultDynamicMembers) {
30+
return resultType;
31+
}
2932
return DynamicType.instance;
3033
}
3134
return resultType;

0 commit comments

Comments
 (0)