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
5 changes: 5 additions & 0 deletions src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,11 @@ export let DiagnosticMessages = {
message: `'${name}' is not callable'`,
severity: DiagnosticSeverity.Error,
code: 'not-callable'
}),
notIterable: (typeName: string) => ({
message: `Type '${typeName}' is not iterable`,
severity: DiagnosticSeverity.Error,
code: 'not-iterable'
})
};
export const defaultMaximumTruncationLength = 160;
Expand Down
158 changes: 150 additions & 8 deletions src/Scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { InterfaceType } from './types/InterfaceType';
import { ComponentType } from './types/ComponentType';
import { WalkMode, createVisitor } from './astUtils/visitors';
import type { BinaryExpression, CallExpression, DottedGetExpression, FunctionExpression } from './parser/Expression';
import { ObjectType } from './types';
import { ObjectType, UninitializedType } from './types';
import undent from 'undent';
import * as fsExtra from 'fs-extra';
import { InlineInterfaceType } from './types/InlineInterfaceType';
Expand Down Expand Up @@ -3697,9 +3697,15 @@ describe('Scope', () => {
expectZeroDiagnostics(program);
const forEachStmt = mainFile.ast.findChild<ForEachStatement>(isForEachStatement);
const symbolTable = forEachStmt.getSymbolTable();
const opts = { flags: SymbolTypeFlag.runtime };
const opts = { flags: SymbolTypeFlag.runtime, statementIndex: forEachStmt.statementIndex + 1 };
expectTypeToBe(symbolTable.getSymbolType('total', opts), IntegerType);
expectTypeToBe(symbolTable.getSymbolType('num', opts), IntegerType);
// at top level, num could possible be uninitialized if loop is not run
const loopVarType = symbolTable.getSymbolType('num', opts) as UnionType;
expectTypeToBe(loopVarType, UnionType);
expect(loopVarType.types).to.include(UninitializedType.instance);
// inside the loop, num must be an integer
const loopBodySymbolTable = forEachStmt.body.getSymbolTable();
expectTypeToBe(loopBodySymbolTable.getSymbolType('num', opts), IntegerType);
expectTypeToBe(symbolTable.getSymbolType('nums', opts), ArrayType);
});

Expand All @@ -3717,17 +3723,104 @@ describe('Scope', () => {
`);
program.validate();
expectZeroDiagnostics(program);
const sourceScope = program.getScopeByName('source');
sourceScope.linkSymbolTable();
const forEachStmt = mainFile.ast.findChild<ForEachStatement>(isForEachStatement);
const symbolTable = forEachStmt.getSymbolTable();
const symbolTable = forEachStmt.body.getSymbolTable();
const opts = { flags: SymbolTypeFlag.runtime };
expectTypeToBe(symbolTable.getSymbolType('data', opts), ArrayType);
expectTypeToBe((symbolTable.getSymbolType('data', opts) as ArrayType).defaultType, IntegerType);

expectTypeToBe(symbolTable.getSymbolType('Alpha', opts).getMemberType('data', opts), ArrayType);
expectTypeToBe(((symbolTable.getSymbolType('Alpha', opts).getMemberType('data', opts)) as ArrayType).defaultType, IntegerType);

expectTypeToBe(symbolTable.getSymbolType('item', opts), IntegerType);
});

it('should set correct type on for each loop items of multi-dimensional arrays', () => {
let mainFile = program.setFile<BrsFile>('source/main.bs', `
sub process(numsArray as integer[][]) as integer
total = 0
for each nums in numsArray
for each num in nums
total += num
end for
end for
return total
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const forEachStmts = mainFile.ast.findChildren<ForEachStatement>(isForEachStatement);
const outerSymbolTable = forEachStmts[0].body.getSymbolTable();
const innerSymbolTable = forEachStmts[1].body.getSymbolTable();
const opts = { flags: SymbolTypeFlag.runtime };
expectTypeToBe(outerSymbolTable.getSymbolType('total', opts), IntegerType);
expectTypeToBe(outerSymbolTable.getSymbolType('nums', opts), ArrayType);
expectTypeToBe(innerSymbolTable.getSymbolType('num', opts), IntegerType);
});

it('should set the type of the loop item based on the declared type', () => {
let mainFile = program.setFile<BrsFile>('source/main.bs', `
sub process(strs)
for each str as string in strs
print str
end for
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const forEachStmt = mainFile.ast.findChild<ForEachStatement>(isForEachStatement);
const symbolTable = forEachStmt.body.getSymbolTable();
const opts = { flags: SymbolTypeFlag.runtime };
expectTypeToBe(symbolTable.getSymbolType('str', opts), StringType);
});

it('should set the type of the loop item based on the inferred type for ByteArray', () => {
let mainFile = program.setFile<BrsFile>('source/main.bs', `
sub process(bytes as roByteArray)
for each byte in bytes
print byte
end for
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const forEachStmt = mainFile.ast.findChild<ForEachStatement>(isForEachStatement);
const symbolTable = forEachStmt.body.getSymbolTable();
const opts = { flags: SymbolTypeFlag.runtime };
expectTypeToBe(symbolTable.getSymbolType('byte', opts), IntegerType);
});


it('should set the type of the loop item based on the inferred type for AAs', () => {
let mainFile = program.setFile<BrsFile>('source/main.bs', `
sub process(data as roAssociativeArray)
for each key in data
print key
end for
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const forEachStmt = mainFile.ast.findChild<ForEachStatement>(isForEachStatement);
const symbolTable = forEachStmt.body.getSymbolTable();
const opts = { flags: SymbolTypeFlag.runtime };
expectTypeToBe(symbolTable.getSymbolType('key', opts), StringType);
});

it('should use dynamic type for loop item when type cannot be inferred', () => {
let mainFile = program.setFile<BrsFile>('source/main.bs', `
sub process(data as roList)
for each item in data
print item
end for
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const forEachStmt = mainFile.ast.findChild<ForEachStatement>(isForEachStatement);
const symbolTable = forEachStmt.body.getSymbolTable();
const opts = { flags: SymbolTypeFlag.runtime };
expectTypeToBe(symbolTable.getSymbolType('item', opts), DynamicType);
});

it('should set correct type on array literals', () => {
Expand Down Expand Up @@ -3767,8 +3860,8 @@ describe('Scope', () => {
`);
program.validate();
expectZeroDiagnostics(program);
const processFnScope = utilFile.getFunctionScopeAtPosition(util.createPosition(2, 24));
const symbolTable = processFnScope.symbolTable;
const forEachStmt = utilFile.ast.findChild<ForEachStatement>(isForEachStatement);
const symbolTable = forEachStmt.body.getSymbolTable();
const opts = { flags: SymbolTypeFlag.runtime };
expectTypeToBe(symbolTable.getSymbolType('data', opts), ArrayType);
expectTypeToBe(symbolTable.getSymbolType('datum', opts), InterfaceType);
Expand Down Expand Up @@ -4934,6 +5027,55 @@ describe('Scope', () => {
program.validate();
expectZeroDiagnostics(program);
});

it('properly handles type assignment in for loops', () => {
const testFile = program.setFile<BrsFile>('source/test.bs', `
sub testPocket1()
i = "hello"
print i ' i is string at this point

for i = 0 to 2
print i ' i is a string at this point
end for
print i ' i is integer or string at this point
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const printStmts = testFile.ast.findChildren<PrintStatement>(isPrintStatement);
let itemVar = printStmts[0].expressions[0];
expectTypeToBe(itemVar.getType({ flags: SymbolTypeFlag.runtime }), StringType);
itemVar = printStmts[1].expressions[0];
expectTypeToBe(itemVar.getType({ flags: SymbolTypeFlag.runtime }), IntegerType);
itemVar = printStmts[2].expressions[0];
let itemVarType = itemVar.getType({ flags: SymbolTypeFlag.runtime });
expectTypeToBe(itemVarType, UnionType);
});

it('properly handles type assignment in for each loops', () => {
const testFile = program.setFile<BrsFile>('source/test.bs', `
sub testPocket1()
items = ["one", "two", "three"]
item = 1234
print item ' item is integer at this point

for each item in items
print item ' item is a string at this point
end for
print item ' item is integer or string at this point
end sub
`);
program.validate();
expectZeroDiagnostics(program);
const printStmts = testFile.ast.findChildren<PrintStatement>(isPrintStatement);
let itemVar = printStmts[0].expressions[0];
expectTypeToBe(itemVar.getType({ flags: SymbolTypeFlag.runtime }), IntegerType);
itemVar = printStmts[1].expressions[0];
expectTypeToBe(itemVar.getType({ flags: SymbolTypeFlag.runtime }), StringType);
itemVar = printStmts[2].expressions[0];
let itemVarType = itemVar.getType({ flags: SymbolTypeFlag.runtime });
expectTypeToBe(itemVarType, UnionType);
});
});

describe('unlinkSymbolTable', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/SymbolTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ export class SymbolTable implements SymbolTypeGetter {
}

if (maxAllowedStatementIndex >= 0 && t.data?.definingNode) {
if (memberOfAncestor || t.data.canUseInDefinedAstNode) {
if (memberOfAncestor) {
// if we've already gone up a level, it's possible to have a variable assigned and used
// in the same statement, eg. for loop
return t.data.definingNode.statementIndex <= maxAllowedStatementIndex;
Expand Down
17 changes: 17 additions & 0 deletions src/astUtils/reflection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,10 @@ export function isAssociativeArrayTypeLike(value: any): value is AssociativeArra
return value?.kind === BscTypeKind.AssociativeArrayType || isBuiltInType(value, 'roAssociativeArray') || isCompoundTypeOf(value, isAssociativeArrayTypeLike);
}

export function isArrayTypeLike(value: any): value is ArrayType | InterfaceType {
return value?.kind === BscTypeKind.ArrayType || isBuiltInType(value, 'roArray') || isCompoundTypeOf(value, isArrayTypeLike);
}

export function isCallFuncableTypeLike(target): target is BscType & { callFuncMemberTable: SymbolTable } {
return isCallFuncableType(target) || isCompoundTypeOf(target, isCallFuncableTypeLike);
}
Expand Down Expand Up @@ -572,6 +576,19 @@ export function isCompoundType(value: any): value is UnionType | IntersectionTyp
return isUnionType(value) || isIntersectionType(value);
}

export function isIterableType(value: any): boolean {
if (isDynamicType(value) || isObjectType(value)) {
return true;
}
if (isArrayTypeLike(value) || isAssociativeArrayTypeLike(value)) {
return true;
}
if (isBuiltInType(value, 'roByteArray') || isBuiltInType(value, 'roList') || isBuiltInType(value, 'roXMLList') || isBuiltInType(value, 'roMessagePort')) {
return true;
}
return false;
}

// Literal reflection

export function isLiteralInvalid(value: any): value is LiteralExpression & { type: InvalidType } {
Expand Down
17 changes: 17 additions & 0 deletions src/bscPlugin/hover/HoverProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,23 @@ describe('HoverProcessor', () => {
expect(hover?.contents).to.be.undefined;
});

it('should have correct hovers for loop-items of for-each-loops', () => {
let file = program.setFile('source/main.bs', `
sub test(strArray)
for each thing as string in strArray
print thing
end for
end sub
`);
program.validate();
// for each thing as string in str|Array
let hover = program.getHover(file.srcPath, util.createPosition(2, 52))[0];
expect(hover?.contents).to.eql([fence('strArray as dynamic')]);
// print th|ing
hover = program.getHover(file.srcPath, util.createPosition(3, 33))[0];
expect(hover?.contents).to.eql([fence('thing as string')]);
});

it('should show unresolved members as invalid', () => {
const file = program.setFile('source/main.bs', `
interface MyIFace
Expand Down
11 changes: 8 additions & 3 deletions src/bscPlugin/hover/HoverProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isAssignmentStatement, isBrsFile, isCallfuncExpression, isClassStatement, isDottedGetExpression, isEnumMemberStatement, isEnumStatement, isEnumType, isInheritableType, isInterfaceStatement, isMemberField, isNamespaceStatement, isNamespaceType, isNewExpression, isTypedFunctionType, isXmlFile } from '../../astUtils/reflection';
import { isAssignmentStatement, isBrsFile, isCallfuncExpression, isClassStatement, isDottedGetExpression, isEnumMemberStatement, isEnumStatement, isEnumType, isForStatement, isInheritableType, isInterfaceStatement, isMemberField, isNamespaceStatement, isNamespaceType, isNewExpression, isTypedFunctionType, isXmlFile } from '../../astUtils/reflection';
import type { BrsFile } from '../../files/BrsFile';
import type { XmlFile } from '../../files/XmlFile';
import type { ExtraSymbolData, Hover, ProvideHoverEvent, TypeChainEntry } from '../../interfaces';
Expand Down Expand Up @@ -161,8 +161,13 @@ export class HoverProcessor {
let exprType: BscType;

if (isAssignmentStatement(expression) && token === expression.tokens.name) {
// if this is an assignment, but we're really interested in the value AFTER the assignment
exprType = expression.getSymbolTable().getSymbolType(expression.tokens.name.text, { flags: typeFlag, typeChain: typeChain, data: extraData, statementIndex: expression.statementIndex + 1 });
if (isForStatement(expression.parent) && expression.parent.counterDeclaration === expression) {
// for loop counter variable - its type is always integer
exprType = expression.value.getType({ flags: typeFlag, typeChain: typeChain, data: extraData, statementIndex: expression.statementIndex });
} else {
// if this is an assignment, but we're really interested in the value AFTER the assignment
exprType = expression.getSymbolTable().getSymbolType(expression.tokens.name.text, { flags: typeFlag, typeChain: typeChain, data: extraData, statementIndex: expression.statementIndex + 1 });
}
} else {
exprType = expression.getType({ flags: typeFlag, typeChain: typeChain, data: extraData, ignoreCall: isCallfuncExpression(expression) });
}
Expand Down
22 changes: 16 additions & 6 deletions src/bscPlugin/validation/BrsFileValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { CallExpression, type FunctionExpression, type LiteralExpression } from
import { ParseMode } from '../../parser/Parser';
import type { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, Body, WhileStatement, TypecastStatement, Block, AliasStatement, IfStatement, ConditionalCompileStatement } from '../../parser/Statement';
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
import { ArrayDefaultTypeReferenceType } from '../../types/ReferenceType';
import { AssociativeArrayType } from '../../types/AssociativeArrayType';
import { DynamicType } from '../../types/DynamicType';
import util from '../../util';
Expand Down Expand Up @@ -116,6 +115,10 @@ export class BrsFileValidator {
if (!node?.tokens?.name) {
return;
}
if (isForStatement(node.parent) && node.parent.counterDeclaration === node) {
// for loop variable variable is added to the block symbol table elsewhere
return;
}
const data: ExtraSymbolData = {};
//register this variable
let nodeType = node.getType({ flags: SymbolTypeFlag.runtime, data: data });
Expand All @@ -132,10 +135,6 @@ export class BrsFileValidator {
},
ForEachStatement: (node) => {
//register the for loop variable
const loopTargetType = node.target.getType({ flags: SymbolTypeFlag.runtime });
const loopVarType = new ArrayDefaultTypeReferenceType(loopTargetType);

node.parent.getSymbolTable()?.addSymbol(node.tokens.item.text, { definingNode: node, isInstance: true, canUseInDefinedAstNode: true }, loopVarType, SymbolTypeFlag.runtime);
},
NamespaceStatement: (node) => {
if (!node?.nameExpression) {
Expand Down Expand Up @@ -353,10 +352,21 @@ export class BrsFileValidator {
// we're a block inside another block (or body). This block is a pocket in the bigger block
node.parent.getSymbolTable().addPocketTable({
index: node.parent.statementIndex,
table: node.symbolTable,
table: blockSymbolTable,
// code always flows through ConditionalCompiles, because we walk according to defined BSConsts
willAlwaysBeExecuted: isConditionalCompileStatement(node.parent)
});

if (isForStatement(node.parent)) {
const counterDecl = node.parent.counterDeclaration;
const loopVar = counterDecl.tokens.name;
const loopVarType = counterDecl.getType({ flags: SymbolTypeFlag.runtime });
blockSymbolTable.addSymbol(loopVar.text, { isInstance: true }, loopVarType, SymbolTypeFlag.runtime);

} else if (isForEachStatement(node.parent)) {
const loopVarType = node.parent.getLoopVariableType({ flags: SymbolTypeFlag.runtime });
blockSymbolTable.addSymbol(node.parent.tokens.item.text, { isInstance: true }, loopVarType, SymbolTypeFlag.runtime);
}
}
},
AstNode: (node) => {
Expand Down
Loading