From 591b1004e5ff42a95d9f471ade8603cd95d06626 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 5 Jun 2026 11:49:54 -0300 Subject: [PATCH 1/7] Only compute full function name in validation if needed --- src/bscPlugin/validation/ScopeValidator.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index 15bd2f884..300b2305a 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -738,13 +738,21 @@ export class ScopeValidator { while (isTypeStatementType(funcType)) { funcType = funcType.wrappedType; } + let _funcName: string; + // Only get the function name if we need it for a diagnostic, since it can be expensive to calculate for complex expressions + function getFuncName() { + if (_funcName) { + return _funcName; + } + _funcName = util.getAllDottedGetPartsAsString(callee, ParseMode.BrighterScript, isCallfuncExpression(callee) ? '@.' : '.'); + return _funcName; + } if (!funcType?.isResolvable() || !isCallableType(funcType) || isCompoundType(funcType)) { - const funcName = util.getAllDottedGetPartsAsString(callee, ParseMode.BrighterScript, isCallfuncExpression(callee) ? '@.' : '.'); if (isUnionType(funcType)) { if (!util.isUnionOfFunctions(funcType) && !isCallfuncExpression(callee)) { // union of func and non func. not callable this.addMultiScopeDiagnostic({ - ...DiagnosticMessages.notCallable(funcName), + ...DiagnosticMessages.notCallable(getFuncName()), location: callErrorLocation }); return; @@ -764,7 +772,7 @@ export class ScopeValidator { // param differences! this.addMultiScopeDiagnostic({ ...DiagnosticMessages.incompatibleSymbolDefinition( - funcName, + getFuncName(), { isUnion: true, data: compatibilityData }), location: callErrorLocation }); @@ -777,17 +785,16 @@ export class ScopeValidator { } if (funcType && !isCallableType(funcType) && !isReferenceType(funcType)) { - const globalFuncWithVarName = globalCallableMap.get(funcName.toLowerCase()); + const globalFuncWithVarName = globalCallableMap.get(getFuncName().toLowerCase()); if (globalFuncWithVarName) { funcType = globalFuncWithVarName.type; } else { this.addMultiScopeDiagnostic({ - ...DiagnosticMessages.notCallable(funcName), + ...DiagnosticMessages.notCallable(getFuncName()), location: callErrorLocation }); return; } - } } From 676b363b791fe7c0cb6fec429756c612db9d11d1 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 5 Jun 2026 16:40:05 -0300 Subject: [PATCH 2/7] Make Initial Validation faster by assuming everything needs to be validated --- src/Program.ts | 69 +++++++++++++++++----- src/bscPlugin/validation/ScopeValidator.ts | 6 +- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/Program.ts b/src/Program.ts index 654311ff6..fd4cc9470 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -761,6 +761,7 @@ export class Program { file: file, program: this }; + this.scopesPerFile.clear(); this.plugins.emit('beforeAddFile', fileAddEvent); @@ -778,6 +779,7 @@ export class Program { private unassignFile(file: T) { delete this.files[file.srcPath.toLowerCase()]; this.destMap.delete(file.destPath.toLowerCase()); + this.scopesPerFile.clear(); return file; } @@ -1101,6 +1103,7 @@ export class Program { xmlFilesValidated: XmlFile[]; changedSymbols: Map>; changedComponentTypes: string[]; + scopesToInvalidate: Scope[]; scopesToValidate: Scope[]; filesToBeValidatedInScopeContext: Set; @@ -1109,6 +1112,7 @@ export class Program { xmlFilesValidated: [], changedSymbols: new Map>(), changedComponentTypes: [], + scopesToInvalidate: [], scopesToValidate: [], filesToBeValidatedInScopeContext: new Set() }; @@ -1166,6 +1170,7 @@ export class Program { const xmlFilesValidated: XmlFile[] = this.validationDetails.xmlFilesValidated; const changedSymbols = this.validationDetails.changedSymbols; const changedComponentTypes = this.validationDetails.changedComponentTypes; + const scopesToInvalidate = this.validationDetails.scopesToInvalidate; const scopesToValidate = this.validationDetails.scopesToValidate; const filesToBeValidatedInScopeContext = this.validationDetails.filesToBeValidatedInScopeContext; @@ -1173,7 +1178,7 @@ export class Program { let logValidateEnd = (status?: string) => { }; - //will be populated later on during the correspnding sequencer event + //will be populated later on during the corresponding sequencer event let filesToProcess: BscFile[]; const sequencer = new Sequencer({ @@ -1246,7 +1251,7 @@ export class Program { .forEach('do deferred component creation', () => [...brsFilesValidated, ...xmlFilesValidated], (file) => { if (isXmlFile(file)) { this.addDeferredComponentTypeSymbolCreation(file); - } else if (isBrsFile(file)) { + } else if (isBrsFile(file) && !this.isFirstValidation) { const fileHasChanges = file.providedSymbols.changes.get(SymbolTypeFlag.runtime).size > 0 || file.providedSymbols.changes.get(SymbolTypeFlag.typetime).size > 0; if (fileHasChanges) { for (const scope of this.getScopesForFile(file)) { @@ -1361,9 +1366,16 @@ export class Program { this.logger.time(LogLevel.info, ['addDiagnosticsForScopes'], () => { this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck); }); - const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols); - for (const file of filesToRevalidate) { - filesToBeValidatedInScopeContext.add(file); + if (this.isFirstValidation) { + //on the first validation, we want to validate all scopes, so we add all files to the set of files to be validated in scope context + for (const file of Object.values(this.files)) { + filesToBeValidatedInScopeContext.add(file); + } + } else { + const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols); + for (const file of filesToRevalidate) { + filesToBeValidatedInScopeContext.add(file); + } } this.currentScopeValidationOptions = { @@ -1376,18 +1388,28 @@ export class Program { //can reset changedComponent types this.validationDetails.changedComponentTypes = []; }) - .forEach('invalidate affected scopes', () => filesToBeValidatedInScopeContext, (file) => { + .forEach('invalidating file segments', () => filesToBeValidatedInScopeContext, (file) => { if (isBrsFile(file)) { file.validationSegmenter.unValidateAllSegments(); - for (const scope of this.getScopesForFile(file)) { + if (!this.isFirstValidation) { + scopesToInvalidate.push(...this.getScopesForFile(file)); + } + } + }) + .once('invalidate affected scopes', () => { + if (this.isFirstValidation) { + for (const scope of this.getAllUserScopes()) { + scope.invalidate(); + } + } else { + for (const scope of scopesToInvalidate) { scope.invalidate(); } } }) .once('checking scopes to validate', () => { //sort the scope names so we get consistent results - for (const scopeName of this.getSortedScopeNames()) { - let scope = this.scopes[scopeName]; + for (const scope of this.getAllUserScopes()) { if (scope.shouldValidate(this.currentScopeValidationOptions)) { scopesToValidate.push(scope); } @@ -1411,12 +1433,7 @@ export class Program { }) .once('detect duplicate component names', () => { this.detectDuplicateComponentNames(); - this.isFirstValidation = false; - // can reset other validation details - this.validationDetails.changedSymbols = new Map>(); - this.validationDetails.scopesToValidate = []; - this.validationDetails.filesToBeValidatedInScopeContext = new Set(); }) .onCancel(() => { @@ -1424,6 +1441,13 @@ export class Program { }) .onSuccess(() => { logValidateEnd(); + this.isFirstValidation = false; + + // can reset other validation details + this.validationDetails.changedSymbols = new Map>(); + this.validationDetails.scopesToInvalidate = []; + this.validationDetails.scopesToValidate = []; + this.validationDetails.filesToBeValidatedInScopeContext = new Set(); }) .onComplete(() => { //if we emitted the beforeValidateProgram hook, emit the afterValidateProgram hook as well @@ -1462,8 +1486,7 @@ export class Program { private getScopesForCrossScopeValidation(someComponentTypeChanged: boolean, didProvidedSymbolChange: boolean) { const scopesForCrossScopeValidation: Scope[] = []; - for (let scopeName of this.getSortedScopeNames()) { - let scope = this.scopes[scopeName]; + for (let scope of this.getAllUserScopes()) { if (this.globalScope === scope) { continue; } @@ -1612,6 +1635,14 @@ export class Program { return this.sortedScopeNames; } + public getAllUserScopes() { + return Object.values(this.scopes).filter(s => s.name !== 'global'); + } + + + private scopesPerFile: Map = new Map(); + + /** * Get a list of all scopes the file is loaded into * @param file the file @@ -1619,6 +1650,11 @@ export class Program { public getScopesForFile(file: BscFile | string) { const resolvedFile = typeof file === 'string' ? this.getFile(file) : file; + const cachedResult = this.scopesPerFile.get(resolvedFile); + if (cachedResult) { + return cachedResult; + } + let result = [] as Scope[]; if (resolvedFile) { const scopeKeys = this.getSortedScopeNames(); @@ -1630,6 +1666,7 @@ export class Program { } } } + this.scopesPerFile.set(resolvedFile, result); return result; } diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index 300b2305a..41373073f 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -830,15 +830,13 @@ export class ScopeValidator { } let paramIndex = 0; for (let arg of argsForCall) { - const data = {} as ExtraSymbolData; - let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data }); - const paramType = funcType.params[paramIndex]?.type; if (!paramType) { // unable to find a paramType -- maybe there are more args than params break; } - + const data = {} as ExtraSymbolData; + let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data }); if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) { argType = data.definingNode.getConstructorType(); } From 2659502a19fc09095942210afa98ee7c60f33369 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 5 Jun 2026 21:20:18 -0300 Subject: [PATCH 3/7] Fix issue with built-in roAssociativeArray not working with AAType --- src/Scope.spec.ts | 74 ++++++++++++++++++++++++++++++++++++++ src/types/InterfaceType.ts | 8 ++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts index ef3ca6a74..1b7bf8022 100644 --- a/src/Scope.spec.ts +++ b/src/Scope.spec.ts @@ -2486,6 +2486,80 @@ describe('Scope', () => { }); }); + describe('types from XML components', () => { + it('allows using an assocArray from an XML component in a callfunc parameter', () => { + program.setFile('components/Parent.xml', trim` + + +