diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index 1cfdf536b..0007d5406 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -4900,6 +4900,79 @@ describe('ScopeValidator', () => { program.validate(); expectZeroDiagnostics(program); }); + + it('detects when a union contains an incompatible type', () => { + program.setFile('source/main.bs', ` + function test(x as string or integer) + print x + "world" + end function + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.operatorTypeMismatch('+', 'string or integer', 'string').message + ]); + }); + + it('allows comparing a void value against a dynamic value with =', () => { + program.setFile('source/main.bs', ` + sub logEvent(name as string) + print name + end sub + + sub main(input as dynamic) + result = logEvent("started") + if result = input + print "same" + else + print "different" + end if + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows comparing a void value against invalid with <>', () => { + program.setFile('source/main.bs', ` + sub getConfig() + end sub + + sub main() + config = getConfig() + if config <> invalid + print "config exists" + else + print "no config" + end if + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows comparing a "void or " union against invalid with <>', () => { + program.setFile('source/main.bs', ` + sub fetchChannelData() + end sub + + sub main() + channel = fetchChannelData() + if true + channel = { + title: "Home" + } + end if + + if channel <> invalid + print channel.title + else + print "no channel" + end if + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); }); describe('memberAccessibilityMismatch', () => { diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index ee46f93d6..635029794 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -28,6 +28,7 @@ import type { XmlScope } from '../../XmlScope'; import type { XmlFile } from '../../files/XmlFile'; import { SGFieldTypes } from '../../parser/SGTypes'; import { DynamicType } from '../../types/DynamicType'; +import { getAllTypesFromCompoundType } from '../../types/helpers'; import { BscTypeKind } from '../../types/BscTypeKind'; import type { BrsDocWithType } from '../../parser/BrightScriptDocParser'; import brsDocParser from '../../parser/BrightScriptDocParser'; @@ -995,9 +996,23 @@ export class ScopeValidator { rightTypeToTest = rightType.underlyingType; } - if (isUnionType(leftType) || isUnionType(rightType)) { - // TODO: it is possible to validate based on innerTypes, but more complicated - // Because you need to verify each combination of types + if (isUnionType(leftTypeToTest) || isUnionType(rightTypeToTest)) { + // validate every combination of the union's inner types - if any combination is invalid, + // then it's possible for this operation to fail at runtime, so flag it + const leftTypesToTest = isUnionType(leftTypeToTest) ? getAllTypesFromCompoundType(leftTypeToTest) : [leftTypeToTest]; + const rightTypesToTest = isUnionType(rightTypeToTest) ? getAllTypesFromCompoundType(rightTypeToTest) : [rightTypeToTest]; + + for (const leftInnerType of leftTypesToTest) { + for (const rightInnerType of rightTypesToTest) { + if (!util.binaryOperatorResultType(leftInnerType, binaryExpr.tokens.operator, rightInnerType)) { + this.addMultiScopeDiagnostic({ + ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()), + location: binaryExpr.location + }); + return; + } + } + } return; } const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest); diff --git a/src/util.spec.ts b/src/util.spec.ts index 774292c08..640d0ac98 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -12,7 +12,7 @@ import { NamespaceType } from './types/NamespaceType'; import { ClassType } from './types/ClassType'; import { ReferenceType } from './types/ReferenceType'; import { SymbolTypeFlag } from './SymbolTypeFlag'; -import { BooleanType, DoubleType, DynamicType, FloatType, IntegerType, InterfaceType, InvalidType, LongIntegerType, ObjectType, StringType, TypedFunctionType, UnionType, VoidType } from './types'; +import { BooleanType, DoubleType, DynamicType, FloatType, IntegerType, InterfaceType, InvalidType, LongIntegerType, ObjectType, StringType, TypedFunctionType, UninitializedType, UnionType, VoidType } from './types'; import { TokenKind } from './lexer/TokenKind'; import { createToken } from './astUtils/creators'; import { createDottedIdentifier, createVariableExpression } from './astUtils/creators'; @@ -1494,6 +1494,32 @@ describe('util', () => { it('handles object Types', () => { expectTypeToBe(util.binaryOperatorResultType(new ObjectType(), createToken(TokenKind.Plus), IntegerType.instance), IntegerType); }); + + it('allows = and <> comparisons of a void type against invalid', () => { + expectTypeToBe(util.binaryOperatorResultType(VoidType.instance, createToken(TokenKind.Equal), InvalidType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.Equal), VoidType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(VoidType.instance, createToken(TokenKind.LessGreater), InvalidType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.LessGreater), VoidType.instance), BooleanType); + }); + + it('allows = and <> comparisons of a void type against dynamic', () => { + expectTypeToBe(util.binaryOperatorResultType(VoidType.instance, createToken(TokenKind.Equal), DynamicType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(VoidType.instance, createToken(TokenKind.LessGreater), DynamicType.instance), BooleanType); + }); + + it('still disallows non-comparison operators on a void type', () => { + expect(util.binaryOperatorResultType(VoidType.instance, createToken(TokenKind.Plus), InvalidType.instance)).to.be.undefined; + expect(util.binaryOperatorResultType(VoidType.instance, createToken(TokenKind.Less), InvalidType.instance)).to.be.undefined; + }); + + it('still disallows comparisons of a void type against non-invalid/dynamic types', () => { + expect(util.binaryOperatorResultType(VoidType.instance, createToken(TokenKind.Equal), StringType.instance)).to.be.undefined; + }); + + it('still disallows = and <> comparisons of an uninitialized type against invalid', () => { + expect(util.binaryOperatorResultType(UninitializedType.instance, createToken(TokenKind.Equal), InvalidType.instance)).to.be.undefined; + expect(util.binaryOperatorResultType(UninitializedType.instance, createToken(TokenKind.LessGreater), InvalidType.instance)).to.be.undefined; + }); }); describe('unaryOperatorResultType', () => { diff --git a/src/util.ts b/src/util.ts index 2e7e48136..549b680be 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1550,7 +1550,15 @@ export class Util { rightType = this.getHighestPriorityType(rightType.types); } - if (isVoidType(leftType) || isVoidType(rightType) || isUninitializedType(leftType) || isUninitializedType(rightType)) { + if (isUninitializedType(leftType) || isUninitializedType(rightType)) { + return undefined; + } + if (isVoidType(leftType) || isVoidType(rightType)) { + // = and <> can still be used to check a possibly-void value against invalid/dynamic + if ((operator.kind === TokenKind.Equal || operator.kind === TokenKind.LessGreater) && + (isInvalidTypeLike(leftType) || isInvalidTypeLike(rightType) || isDynamicType(leftType) || isDynamicType(rightType))) { + return BooleanType.instance; + } return undefined; }