diff --git a/src/AstValidationSegmenter.ts b/src/AstValidationSegmenter.ts index 62d13e444..7f1e3e9dc 100644 --- a/src/AstValidationSegmenter.ts +++ b/src/AstValidationSegmenter.ts @@ -1,5 +1,5 @@ import type { DottedGetExpression, TypeExpression, VariableExpression } from './parser/Expression'; -import { isAliasStatement, isBinaryExpression, isBlock, isBody, isClassStatement, isConditionalCompileStatement, isDottedGetExpression, isInterfaceStatement, isNamespaceStatement, isTypecastStatement, isTypeExpression, isVariableExpression } from './astUtils/reflection'; +import { isAliasStatement, isArrayType, isBinaryExpression, isBlock, isBody, isClassStatement, isConditionalCompileStatement, isDottedGetExpression, isInterfaceStatement, isNamespaceStatement, isTypecastStatement, isTypeExpression, isVariableExpression } from './astUtils/reflection'; import { ChildrenSkipper, WalkMode, createVisitor } from './astUtils/visitors'; import type { ExtraSymbolData, GetTypeOptions, TypeChainEntry } from './interfaces'; import type { AstNode, Expression } from './parser/AstNode'; @@ -77,8 +77,11 @@ export class AstValidationSegmenter { this.checkExpressionForUnresolved(segment, expression.expression.right as VariableExpression, assignedSymbolsNames); } if (isTypeExpression(expression)) { - const typeIntypeExpression = expression.getType({ flags: SymbolTypeFlag.typetime }); - if (typeIntypeExpression.isResolvable()) { + let typeInTypeExpression = expression.getType({ flags: SymbolTypeFlag.typetime }); + if (isArrayType(typeInTypeExpression)) { + typeInTypeExpression = typeInTypeExpression.defaultType; + } + if (typeInTypeExpression.isResolvable()) { return this.handleTypeCastTypeExpression(segment, expression); } } @@ -90,7 +93,10 @@ export class AstValidationSegmenter { let typeChain: TypeChainEntry[] = []; const extraData = {} as ExtraSymbolData; const options: GetTypeOptions = { flags: flag, onlyCacheResolvedTypes: true, typeChain: typeChain, data: extraData }; - const nodeType = expression.getType(options); + let nodeType = expression.getType(options); + if (isArrayType(nodeType)) { + nodeType = nodeType.defaultType; + } if (!nodeType?.isResolvable()) { let symbolsSet: Set; if (!assignedSymbolsNames?.has(typeChain[0].name.toLowerCase())) { diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index 59f103bf7..fef1d1334 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -1,4 +1,4 @@ -import { isAliasStatement, isArrayType, isBlock, isBody, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isInvalidType, isLibraryStatement, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypecastExpression, isTypecastStatement, isUnaryExpression, isVariableExpression, isVoidType, isWhileStatement } from '../../astUtils/reflection'; +import { isAliasStatement, isBlock, isBody, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isInvalidType, isLibraryStatement, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypecastExpression, isTypecastStatement, isUnaryExpression, isVariableExpression, isVoidType, isWhileStatement } from '../../astUtils/reflection'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import { DiagnosticMessages } from '../../DiagnosticMessages'; import type { BrsFile } from '../../files/BrsFile'; @@ -123,11 +123,8 @@ export class BrsFileValidator { ForEachStatement: (node) => { //register the for loop variable const loopTargetType = node.target.getType({ flags: SymbolTypeFlag.runtime }); - let loopVarType = isArrayType(loopTargetType) ? loopTargetType.defaultType : DynamicType.instance; + const loopVarType = new ArrayDefaultTypeReferenceType(loopTargetType); - if (!loopTargetType.isResolvable()) { - loopVarType = new ArrayDefaultTypeReferenceType(loopTargetType); - } node.parent.getSymbolTable()?.addSymbol(node.tokens.item.text, { definingNode: node, isInstance: true, canUseInDefinedAstNode: true }, loopVarType, SymbolTypeFlag.runtime); }, NamespaceStatement: (node) => { diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index 9518876fd..303a23004 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -4430,6 +4430,48 @@ describe('ScopeValidator', () => { // there should be no more errors expectZeroDiagnostics(program); }); + + it('recognizes when the type of a for-each loop variable changes', () => { + program.setFile('source/file1.bs', ` + interface FooFace + prop as string + end interface + `); + + program.setFile('source/file2.bs', ` + function loopFooFace(input as FooFace[]) + out = "" + for each ff in input + out += ff.prop + end for + return out + end function + `); + program.validate(); + //currently no error + expectZeroDiagnostics(program); + + // change FooFace.prop to not work in other file + program.setFile('source/file1.bs', ` + interface FooFace + prop as integer + end interface + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.operatorTypeMismatch('+=', 'string', 'integer') + ]); + + // change FooFace.prop to back to working + program.setFile('source/file1.bs', ` + interface FooFace + prop as string + end interface + `); + program.validate(); + //currently no error + expectZeroDiagnostics(program); + }); }); diff --git a/src/types/ReferenceType.ts b/src/types/ReferenceType.ts index 2a9274939..097e45ed2 100644 --- a/src/types/ReferenceType.ts +++ b/src/types/ReferenceType.ts @@ -487,8 +487,6 @@ export class BinaryOperatorReferenceType extends BscType { * Use this class for when there is a presumable array type that is a referenceType */ export class ArrayDefaultTypeReferenceType extends BscType { - cachedType: BscType; - constructor(public objType: BscType) { super('ArrayDefaultType'); // eslint-disable-next-line no-constructor-return @@ -506,9 +504,6 @@ export class ArrayDefaultTypeReferenceType extends BscType { if (propName === 'getTarget') { return () => { - if (this.cachedType) { - return this.cachedType; - } if (isReferenceType(this.objType)) { (this.objType as any).getTarget(); } @@ -516,26 +511,23 @@ export class ArrayDefaultTypeReferenceType extends BscType { }; } - let resultType: BscType = this.cachedType ?? DynamicType.instance; - if (!this.cachedType) { - if ((isAnyReferenceType(this.objType) && !this.objType.isResolvable()) - ) { - if (propName === 'isResolvable') { - return () => false; - } - if (propName === 'getTarget') { - return () => undefined; - } + let resultType: BscType = DynamicType.instance; + if ((isAnyReferenceType(this.objType) && !this.objType.isResolvable()) + ) { + if (propName === 'isResolvable') { + return () => false; + } + if (propName === 'getTarget') { + return () => undefined; + } + } else { + if (isArrayType(this.objType)) { + resultType = this.objType.defaultType; } else { - if (isArrayType(this.objType)) { - resultType = this.objType.defaultType; - } else { - resultType = DynamicType.instance; - } - this.cachedType = resultType; + resultType = DynamicType.instance; } - } + if (resultType) { const result = Reflect.get(resultType, propName, resultType); return result;