diff --git a/src/AstValidationSegmenter.ts b/src/AstValidationSegmenter.ts index 41586c199..06fa3f964 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, isTypeExpression, isVariableExpression } from './astUtils/reflection'; +import { isAliasStatement, 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'; @@ -9,6 +9,7 @@ import { SymbolTypeFlag } from './SymbolTypeFlag'; import type { Token } from './lexer/Token'; import type { BrsFile } from './files/BrsFile'; import { TokenKind } from './lexer/TokenKind'; +import type { BscSymbol } from './SymbolTable'; // eslint-disable-next-line no-bitwise export const InsideSegmentWalkMode = WalkMode.visitStatements | WalkMode.visitExpressions | WalkMode.recurseChildFunctions; @@ -73,12 +74,20 @@ export class AstValidationSegmenter { return this.checkExpressionForUnresolved(segment, expression.expression.left as VariableExpression, assignedSymbolsNames) || this.checkExpressionForUnresolved(segment, expression.expression.right as VariableExpression, assignedSymbolsNames); } + if (isTypeExpression(expression)) { + const typeIntypeExpression = expression.getType({ flags: SymbolTypeFlag.typetime }); + if (typeIntypeExpression.isResolvable()) { + return this.handleTypeCastTypeExpression(segment, expression); + } + } + return this.addUnresolvedSymbol(segment, expression, assignedSymbolsNames); + } + private addUnresolvedSymbol(segment: AstNode, expression: Expression, assignedSymbolsNames?: Set) { const flag = util.isInTypeExpression(expression) ? SymbolTypeFlag.typetime : SymbolTypeFlag.runtime; let typeChain: TypeChainEntry[] = []; const extraData = {} as ExtraSymbolData; const options: GetTypeOptions = { flags: flag, onlyCacheResolvedTypes: true, typeChain: typeChain, data: extraData }; - const nodeType = expression.getType(options); if (!nodeType?.isResolvable()) { let symbolsSet: Set; @@ -126,6 +135,8 @@ export class AstValidationSegmenter { private currentNamespaceStatement: NamespaceStatement; private currentClassStatement: ClassStatement; + private unresolvedTypeCastTypeExpressions: TypeExpression[] = []; + checkSegmentWalk(segment: AstNode) { if (isNamespaceStatement(segment) || isBody(segment)) { @@ -161,6 +172,7 @@ export class AstValidationSegmenter { return; } + this.segmentsForValidation.push(segment); this.validatedSegments.set(segment, false); let foundUnresolvedInSegment = false; @@ -169,6 +181,16 @@ export class AstValidationSegmenter { const assignedSymbolsNames = new Set(); this.currentClassStatement = segment.findAncestor(isClassStatement); + if (isTypecastStatement(segment)) { + if (this.checkExpressionForUnresolved(segment, segment.typecastExpression.typeExpression)) { + this.unresolvedTypeCastTypeExpressions.push(segment.typecastExpression.typeExpression); + } + } + let unresolvedTypeCastTypeExpression: TypeExpression; + if (this.unresolvedTypeCastTypeExpressions.length > 0) { + unresolvedTypeCastTypeExpression = this.unresolvedTypeCastTypeExpressions[this.unresolvedTypeCastTypeExpressions.length - 1]; + } + segment.walk(createVisitor({ AssignmentStatement: (stmt) => { if (stmt.tokens.equals.kind === TokenKind.Equal) { @@ -186,7 +208,11 @@ export class AstValidationSegmenter { assignedSymbolsNames.add(stmt.tokens.item.text.toLowerCase()); }, VariableExpression: (expr) => { - if (!assignedSymbolsNames.has(expr.tokens.name.text.toLowerCase())) { + const hasUnresolvedTypecastedM = unresolvedTypeCastTypeExpression && expr.tokens.name.text.toLowerCase() === 'm'; + if (hasUnresolvedTypecastedM) { + this.addUnresolvedSymbol(segment, unresolvedTypeCastTypeExpression); + + } else if (!assignedSymbolsNames.has(expr.tokens.name.text.toLowerCase())) { const expressionIsUnresolved = this.checkExpressionForUnresolved(segment, expr, assignedSymbolsNames); foundUnresolvedInSegment = expressionIsUnresolved || foundUnresolvedInSegment; } @@ -195,6 +221,18 @@ export class AstValidationSegmenter { DottedGetExpression: (expr) => { const expressionIsUnresolved = this.checkExpressionForUnresolved(segment, expr, assignedSymbolsNames); foundUnresolvedInSegment = expressionIsUnresolved || foundUnresolvedInSegment; + if (!foundUnresolvedInSegment && unresolvedTypeCastTypeExpression) { + let startOfDottedGet: Expression = expr; + while (isDottedGetExpression(startOfDottedGet)) { + startOfDottedGet = startOfDottedGet.obj; + } + if (isVariableExpression(startOfDottedGet)) { + const hasUnresolvedTypeCastedM = unresolvedTypeCastTypeExpression && startOfDottedGet.tokens.name.text.toLowerCase() === 'm'; + if (hasUnresolvedTypeCastedM) { + this.handleTypeCastTypeExpression(segment, unresolvedTypeCastTypeExpression); + } + } + } skipper.skip(); }, TypeExpression: (expr) => { @@ -212,7 +250,32 @@ export class AstValidationSegmenter { } this.currentClassStatement = undefined; this.currentClassStatement = undefined; + } + + + private handleTypeCastTypeExpression(segment: AstNode, typecastTypeExpression: TypeExpression) { + const expression = typecastTypeExpression; + if (isTypeExpression(expression)) { + const typeIntypeExpression = expression.getType({ flags: SymbolTypeFlag.typetime }); + + if (typeIntypeExpression.isResolvable()) { + const memberSymbols = typeIntypeExpression.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime); + const unresolvedMembers: BscSymbol[] = []; + for (const memberSymbol of memberSymbols) { + if (!memberSymbol.type.isResolvable()) { + unresolvedMembers.push(memberSymbol); + } + } + let addedSymbol = false; + for (const unresolvedMember of unresolvedMembers) { + addedSymbol = this.addUnresolvedSymbol(segment, unresolvedMember?.data?.definingNode) || addedSymbol; + } + return addedSymbol; + } + return this.addUnresolvedSymbol(segment, expression); + } + return false; } getAllUnvalidatedSegments() { @@ -228,6 +291,7 @@ export class AstValidationSegmenter { getSegmentsWithChangedSymbols(changedSymbols: Map>): AstNode[] { const segmentsToWalkForValidation: AstNode[] = []; + for (const segment of this.segmentsForValidation) { if (this.validatedSegments.get(segment)) { continue; diff --git a/src/CrossScopeValidator.ts b/src/CrossScopeValidator.ts index 89c3fa230..179478d26 100644 --- a/src/CrossScopeValidator.ts +++ b/src/CrossScopeValidator.ts @@ -277,7 +277,7 @@ export class CrossScopeValidator { if (this.providedTreeMap.has(scope)) { return this.providedTreeMap.get(scope); } - const providedTree = new ProvidedNode('', this.componentsMap); + const providedTree = new ProvidedNode('');//, this.componentsMap); const duplicatesMap = new Map>(); const referenceTypesMap = new Map<{ symbolName: string; file: BscFile; symbolObj: ProvidedSymbol }, Array<{ name: string; namespacedName?: string }>>(); @@ -499,7 +499,7 @@ export class CrossScopeValidator { const typeName = 'rosgnode' + componentName; const component = this.program.getComponent(componentName); const componentSymbol = this.program.globalScope.symbolTable.getSymbol(typeName, SymbolTypeFlag.typetime)?.[0]; - if (componentSymbol && component) { + if (componentSymbol && component && componentSymbol.type.isBuiltIn) { this.componentsMap.set(typeName, { file: component.file, symbol: componentSymbol }); } } diff --git a/src/DiagnosticManager.ts b/src/DiagnosticManager.ts index 140788b27..65f108ebc 100644 --- a/src/DiagnosticManager.ts +++ b/src/DiagnosticManager.ts @@ -237,7 +237,7 @@ export class DiagnosticManager { isMatch = !!context.tags?.includes(filter.tag); } if (isMatch && needToMatch.scope) { - isMatch = context.scope === filter.scope; + isMatch = context.scope?.name === filter.scope.name; } if (isMatch && needToMatch.fileUri) { isMatch = cachedData.diagnostic.location?.uri === filter.fileUri; diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index c0817ffef..e53530c0d 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -1031,6 +1031,17 @@ export let DiagnosticMessages = { legacyCode: 1151, severity: DiagnosticSeverity.Error, code: 'return-type-coercion-mismatch' + }), + cannotFindCallFuncFunction: (name: string, fullName: string, typeName: string) => ({ + message: `Cannot find callfunc function '${name}' for type '${typeName}'`, + data: { + name: name, + fullName: fullName, + typeName: typeName, + isCallfunc: true + }, + severity: DiagnosticSeverity.Error, + code: 'cannot-find-callfunc' }) }; export const defaultMaximumTruncationLength = 160; diff --git a/src/PluginInterface.ts b/src/PluginInterface.ts index a245b60a2..f9cddf326 100644 --- a/src/PluginInterface.ts +++ b/src/PluginInterface.ts @@ -54,6 +54,7 @@ export default class PluginInterface * Call `event` on plugins */ public emit & string>(event: K, ...args: PluginEventArgs[K]) { + this.logger.debug(`Emitting plugin event: ${event}`); for (let plugin of this.plugins) { if ((plugin as any)[event]) { try { @@ -75,6 +76,7 @@ export default class PluginInterface * Call `event` on plugins, but allow the plugins to return promises that will be awaited before the next plugin is notified */ public async emitAsync & string>(event: K, ...args: PluginEventArgs[K]): Promise< PluginEventArgs[K][0]> { + this.logger.debug(`Emitting async plugin event: ${event}`); for (let plugin of this.plugins) { if ((plugin as any)[event]) { try { diff --git a/src/Program.spec.ts b/src/Program.spec.ts index 50ef5cf3b..8ad1efee0 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -19,7 +19,7 @@ import type { SinonSpy } from 'sinon'; import { createSandbox } from 'sinon'; import { SymbolTypeFlag } from './SymbolTypeFlag'; import { AssetFile } from './files/AssetFile'; -import type { ProvideFileEvent, CompilerPlugin, BeforeProvideFileEvent, AfterProvideFileEvent, BeforeFileAddEvent, AfterFileAddEvent, BeforeFileRemoveEvent, AfterFileRemoveEvent } from './interfaces'; +import type { ProvideFileEvent, CompilerPlugin, BeforeProvideFileEvent, AfterProvideFileEvent, BeforeFileAddEvent, AfterFileAddEvent, BeforeFileRemoveEvent, AfterFileRemoveEvent, ScopeValidationOptions } from './interfaces'; import { StringType, TypedFunctionType, DynamicType, FloatType, IntegerType, InterfaceType, ArrayType, BooleanType, DoubleType, UnionType } from './types'; import { AssociativeArrayType } from './types/AssociativeArrayType'; import { ComponentType } from './types/ComponentType'; @@ -584,6 +584,161 @@ describe('Program', () => { program.validate(); expectZeroDiagnostics(program); }); + + describe('changed symbols', () => { + it('includes components when component interface changes', () => { + program.setFile('components/widget.xml', trim` + + + + + + `); + program.setFile('components/other.xml', trim` + + + + + + `); + program.setFile('source/main.bs', ` + sub sourceScopeFunc() + end sub + `); + program.validate(); + let options: ScopeValidationOptions = program['currentScopeValidationOptions']; + expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodewidget')).to.be.true; + expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodeother')).to.be.true; + + expectZeroDiagnostics(program); + //change widget + program.setFile('components/widget.xml', trim` + + + + + + `); + program.validate(); + expectZeroDiagnostics(program); + options = program['currentScopeValidationOptions']; + expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodewidget')).to.be.true; + expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodeother')).to.be.false; + }); + + it('includes components when component callfunc changes', () => { + program.setFile('components/widget.xml', trim` + +