Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions src/bscPlugin/hover/HoverProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,16 +829,16 @@ describe('HoverProcessor', () => {
if deviceInfo.getClockFormat() = "24h" then
hour = stringUtil.pad(hour)
' "22:01" | "01:01"
return substitute("{0}:{1}", hour, minutes as string)
return substitute("{0}:{1}", hour as string, minutes as string)
else
hour = hour mod 12
if hour = 0 then hour = 12
' "10:01 AM" | "1:01 AM"
return substitute("{0}:{1} {2}", hour, minutes as string, meridiem)
return substitute("{0}:{1} {2}", hour.toStr(), minutes as string, meridiem)
end if
end function
`);
const expectedHourHoverStr = `hour as dynamic`;
const expectedHourHoverStr = `hour as integer or string`;

program.validate();
expectZeroDiagnostics(program);
Expand Down
12 changes: 12 additions & 0 deletions src/bscPlugin/validation/ScopeValidator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3487,6 +3487,18 @@ describe('ScopeValidator', () => {
DiagnosticMessages.operatorTypeMismatch('=', 'uninitialized', 'invalid').message
]);
});

it('allows string comparisons with object', () => {
program.setFile<BrsFile>('source/main.brs', `
sub test(x as object)
if x <> "test"
print x
end if
end sub
`);
program.validate();
expectZeroDiagnostics(program);
});
});

describe('memberAccessibilityMismatch', () => {
Expand Down
12 changes: 10 additions & 2 deletions src/files/BrsFile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { SymbolTypeFlag } from '../SymbolTypeFlag';
import { ClassType, EnumType, FloatType, InterfaceType } from '../types';
import type { StandardizedFileEntry } from 'roku-deploy';
import * as fileUrl from 'file-url';
import { isAALiteralExpression, isBlock } from '../astUtils/reflection';
import { isAALiteralExpression, isBlock, isFunctionExpression } from '../astUtils/reflection';
import type { AALiteralExpression } from '../parser/Expression';
import { CallExpression, FunctionExpression, LiteralExpression } from '../parser/Expression';
import { Logger } from '@rokucommunity/logger';
Expand Down Expand Up @@ -192,7 +192,7 @@ describe('BrsFile', () => {
it('does not crazy during validation with unique binary operator', () => {
//monitor the logging system, if we detect an error, this test fails
const spy = sinon.spy(Logger.prototype, 'error');
program.setFile('source/main.bs', `
const file = program.setFile<BrsFile>('source/main.bs', `
namespace date
function timeElapsedInDay()
time = 1
Expand All @@ -215,6 +215,14 @@ describe('BrsFile', () => {
expect(
spy.getCalls().map(x => (x.args?.[0] as string)?.toString()).filter(x => x?.includes('Error when calling plugin'))
).to.eql([]);

// Check the result type too
const sourceScope = program.getScopeByName('source');
sourceScope.linkSymbolTable();
const timeElapsedFunc = file.ast.findChild<FunctionExpression>(isFunctionExpression);
const symbolTable = timeElapsedFunc.body.getSymbolTable();
const offsetType = symbolTable.getSymbolType('offset', { flags: SymbolTypeFlag.runtime });
expectTypeToBe(offsetType, IntegerType);
});

it('supports the third parameter in CreateObject', () => {
Expand Down
7 changes: 7 additions & 0 deletions src/types/BscType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,11 @@ export abstract class BscType {
}
this.hasAddedBuiltInInterfaces = true;
}

/**
* The level of priority of this type when in a binary operation
* For example Float is higher priority than integer, so Float + Int => Float
* Lower numbers have higher priority
*/
readonly binaryOpPriorityLevel: number = 0;
}
2 changes: 2 additions & 0 deletions src/types/DoubleType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export class DoubleType extends BscType {
public isEqual(targetType: BscType): boolean {
return isDoubleType(targetType);
}

readonly binaryOpPriorityLevel = 1;
}

BuiltInInterfaceAdder.primitiveTypeInstanceCache.set('double', DoubleType.instance);
2 changes: 2 additions & 0 deletions src/types/DynamicType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export class DynamicType extends BscType {
getMemberType(memberName: string, options: GetTypeOptions) {
return DynamicType.instance;
}


}

BuiltInInterfaceAdder.primitiveTypeInstanceCache.set('dynamic', DynamicType.instance);
2 changes: 2 additions & 0 deletions src/types/FloatType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export class FloatType extends BscType {
public isEqual(targetType: BscType): boolean {
return isFloatType(targetType);
}

readonly binaryOpPriorityLevel = 2;
}

BuiltInInterfaceAdder.primitiveTypeInstanceCache.set('float', FloatType.instance);
2 changes: 2 additions & 0 deletions src/types/IntegerType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export class IntegerType extends BscType {
isEqual(otherType: BscType) {
return isIntegerType(otherType);
}

readonly binaryOpPriorityLevel = 4;
}

BuiltInInterfaceAdder.primitiveTypeInstanceCache.set('integer', IntegerType.instance);
2 changes: 2 additions & 0 deletions src/types/LongIntegerType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export class LongIntegerType extends BscType {
isEqual(targetType: BscType): boolean {
return isLongIntegerType(targetType);
}

readonly binaryOpPriorityLevel = 3;
}

BuiltInInterfaceAdder.primitiveTypeInstanceCache.set('longinteger', LongIntegerType.instance);
6 changes: 4 additions & 2 deletions src/types/UnionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class UnionType extends BscType {
}

isTypeCompatible(targetType: BscType, data?: TypeCompatibilityData): boolean {
if (isDynamicType(targetType) || isObjectType(targetType)) {
if (isDynamicType(targetType) || isObjectType(targetType) || this === targetType) {
return true;
}
if (isEnumTypeCompatible(this, targetType, data)) {
Expand All @@ -86,7 +86,6 @@ export class UnionType extends BscType {
}
}


return false;
}
toString(): string {
Expand All @@ -106,6 +105,9 @@ export class UnionType extends BscType {
if (!isUnionType(targetType)) {
return false;
}
if (this === targetType) {
return true;
}
return this.isTypeCompatible(targetType) && targetType.isTypeCompatible(this);
}

Expand Down
31 changes: 30 additions & 1 deletion src/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, InvalidType, LongIntegerType, StringType, TypedFunctionType, VoidType } from './types';
import { BooleanType, DoubleType, DynamicType, FloatType, IntegerType, InvalidType, LongIntegerType, ObjectType, StringType, TypedFunctionType, UnionType, VoidType } from './types';
import { TokenKind } from './lexer/TokenKind';
import { createToken } from './astUtils/creators';
import { createDottedIdentifier, createVariableExpression } from './astUtils/creators';
Expand Down Expand Up @@ -1100,6 +1100,31 @@ describe('util', () => {
// "and" / "or" are bitwise operators with number
expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Or), DynamicType.instance), IntegerType);
});

it('handles union types ', () => {
const floatIntUnion = new UnionType([IntegerType.instance, FloatType.instance]);
expectTypeToBe(util.binaryOperatorResultType(floatIntUnion, createToken(TokenKind.Plus), DoubleType.instance), DoubleType);
});

it('handles 2 union types', () => {
const floatIntUnion = new UnionType([IntegerType.instance, FloatType.instance]);
expectTypeToBe(util.binaryOperatorResultType(floatIntUnion, createToken(TokenKind.Plus), floatIntUnion), FloatType);
});

it('handles union types with diverse member types', () => {
const myUnion = new UnionType([FloatType.instance, StringType.instance, BooleanType.instance]);
expectTypeToBe(util.binaryOperatorResultType(myUnion, createToken(TokenKind.Plus), myUnion), DynamicType);
});

it('handles union types with self-referencing unions', () => {
const myUnion = new UnionType([FloatType.instance, StringType.instance, DynamicType.instance]);
myUnion.addType(myUnion);
expectTypeToBe(util.binaryOperatorResultType(myUnion, createToken(TokenKind.Plus), myUnion), DynamicType);
});

it('handles object Types', () => {
expectTypeToBe(util.binaryOperatorResultType(new ObjectType(), createToken(TokenKind.Plus), IntegerType.instance), IntegerType);
});
});

describe('unaryOperatorResultType', () => {
Expand All @@ -1124,6 +1149,10 @@ describe('util', () => {
expectTypeToBe(util.unaryOperatorResultType(notToken, LongIntegerType.instance), LongIntegerType);
expect(util.unaryOperatorResultType(notToken, VoidType.instance)).to.be.undefined;
});

it('handles object Types', () => {
expectTypeToBe(util.unaryOperatorResultType(createToken(TokenKind.Minus), new ObjectType()), ObjectType);
});
});

describe('getTokenDocumentation', () => {
Expand Down
71 changes: 69 additions & 2 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionP
import { LogLevel, createLogger } from './logging';
import { isToken, type Identifier, type Locatable, type Token } from './lexer/Token';
import { TokenKind } from './lexer/TokenKind';
import { isAnyReferenceType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isDottedGetExpression, isDoubleType, isDynamicType, isEnumMemberType, isExpression, isFloatType, isIndexedGetExpression, isInvalidType, isLiteralString, isLongIntegerType, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isPrimitiveType, isReferenceType, isStatement, isStringType, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
import { isAnyReferenceType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isDottedGetExpression, isDoubleType, isDynamicType, isEnumMemberType, isExpression, isFloatType, isIndexedGetExpression, isInvalidType, isLiteralString, isLongIntegerType, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isObjectType, isPrimitiveType, isReferenceType, isStatement, isStringType, 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';
Expand Down Expand Up @@ -1551,6 +1551,17 @@ export class Util {
});
}

// Try to find a common value of union type
leftType = getUniqueType([leftType], unionTypeFactory);
rightType = getUniqueType([rightType], unionTypeFactory);

if (isUnionType(leftType)) {
leftType = this.getHighestPriorityType(leftType.types);
}
if (isUnionType(rightType)) {
rightType = this.getHighestPriorityType(rightType.types);
}

if (isVoidType(leftType) || isVoidType(rightType) || isUninitializedType(leftType) || isUninitializedType(rightType)) {
return undefined;
}
Expand All @@ -1561,6 +1572,15 @@ export class Util {
if (isEnumMemberType(rightType)) {
rightType = rightType.underlyingType;
}

// treat object type like dynamic
if (isObjectType(leftType)) {
leftType = DynamicType.instance;
}
if (isObjectType(rightType)) {
rightType = DynamicType.instance;
}

let hasDouble = isDoubleType(leftType) || isDoubleType(rightType);
let hasFloat = isFloatType(leftType) || isFloatType(rightType);
let hasLongInteger = isLongIntegerType(leftType) || isLongIntegerType(rightType);
Expand Down Expand Up @@ -1723,16 +1743,63 @@ export class Util {
return undefined;
}

public getHighestPriorityType(types: BscType[], depth = 0): BscType {
let result: BscType;
if (depth > 4) {
// shortcut for very complicated types, or self-referencing union types
return DynamicType.instance;
}
for (let type of types) {
if (isUnionType(type)) {
type = getUniqueType([type], unionTypeFactory);
if (isUnionType(type)) {
type = this.getHighestPriorityType(type.types, depth + 1);
}
}
if (!result) {
result = type;
} else {
if (type.binaryOpPriorityLevel < result.binaryOpPriorityLevel) {
result = type;
} else if (type.binaryOpPriorityLevel === result.binaryOpPriorityLevel && !result.isEqual(type)) {
// equal priority types, but not equal types, like Boolean and String... just be dynamic at this point
result = DynamicType.instance;
}
}
if (isUninitializedType(type)) {
return type;
}
if (isVoidType(type)) {
return type;
}
if (isInvalidType(type)) {
return type;
}
if (isObjectType(type) && !isDynamicType(type)) {
result = type;
}
if (isDynamicType(type)) {
result = type;
}
}
return result ?? DynamicType.instance;
}

/**
* Return the type of the result of a binary operator
*/
public unaryOperatorResultType(operator: Token, exprType: BscType): BscType {

if (isUnionType(exprType)) {
exprType = this.getHighestPriorityType(exprType.types);
}

if (isVoidType(exprType) || isInvalidType(exprType) || isUninitializedType(exprType)) {
return undefined;
}

if (isDynamicType(exprType)) {

if (isDynamicType(exprType) || isObjectType(exprType)) {
return exprType;
}

Expand Down
Loading