From f9aa96dfa632639000da3af068e2bdaadf9c6bc4 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Sat, 1 Nov 2025 17:58:39 -0300 Subject: [PATCH 1/6] Only validate Scopes that we have decided we need to validate --- src/Program.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Program.ts b/src/Program.ts index e28c28124..41b1d64e1 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -1159,7 +1159,7 @@ export class Program { this.validationDetails.brsFilesValidated = []; this.validationDetails.xmlFilesValidated = []; }) - .once('tracks changed symbols and prepares files and scopes for validation.', () => { + .once('tracks changed symbols and prepares files and scopes for validation', () => { if (this.options.logLevel === LogLevel.debug) { const changedRuntime = Array.from(changedSymbols.get(SymbolTypeFlag.runtime)).sort(); this.logger.debug('Changed Symbols (runTime):', changedRuntime.join(', ')); @@ -1204,9 +1204,7 @@ export class Program { }); } }) - .forEach('validate scope', () => this.getSortedScopeNames(), (scopeName) => { - //sort the scope names so we get consistent results - let scope = this.scopes[scopeName]; + .forEach('validate scope', () => scopesToValidate, (scope) => { scope.validate(this.currentScopeValidationOptions); }) .forEach('afterScopeValidate', () => scopesToValidate, (scope) => { From 17d3ee2746e21b010521339299e52cc5856ab7f1 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Sat, 1 Nov 2025 19:16:13 -0300 Subject: [PATCH 2/6] Only recreate Components if a file in that scoe has symbol changes --- src/Program.ts | 64 +++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/Program.ts b/src/Program.ts index 41b1d64e1..6b27e8a39 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -1020,30 +1020,13 @@ export class Program { program: this }); }) - //handle some component symbol stuff - .forEach('addDeferredComponentTypeSymbolCreation', - () => { - filesToProcess = Object.values(this.files).sort(firstBy(x => x.srcPath)).filter(x => !x.isValidated); - for (const file of filesToProcess) { - filesToBeValidatedInScopeContext.add(file); - } - - //return the list of files that need to be processed - return filesToProcess; - }, (file) => { - // cast a wide net for potential changes in components - if (isXmlFile(file)) { - this.addDeferredComponentTypeSymbolCreation(file); - } else if (isBrsFile(file)) { - for (const scope of this.getScopesForFile(file)) { - if (isXmlScope(scope)) { - this.addDeferredComponentTypeSymbolCreation(scope.xmlFile); - } - } - } + .once('get files to be validated', () => { + filesToProcess = Object.values(this.files).sort(firstBy(x => x.srcPath)).filter(x => !x.isValidated); + for (const file of filesToProcess) { + filesToBeValidatedInScopeContext.add(file); } - ) - .once('addComponentReferenceTypes', () => { + }) + .once('add component reference types', () => { // Create reference component types for any component that changes for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) { this.addComponentReferenceType(componentKey, componentName); @@ -1076,8 +1059,24 @@ export class Program { file: file }); }) - .once('Build component types for any component that changes', () => { + .forEach('do deferred component creation', () => [...brsFilesValidated, ...xmlFilesValidated], (file) => { + if (isXmlFile(file)) { + this.addDeferredComponentTypeSymbolCreation(file); + } else if (isBrsFile(file)) { + const fileHasChanges = file.providedSymbols.changes.get(SymbolTypeFlag.runtime).size > 0 || file.providedSymbols.changes.get(SymbolTypeFlag.typetime).size > 0; + if (fileHasChanges) { + this.logger.info('File changes: ', file.srcPath, file.providedSymbols.changes); + for (const scope of this.getScopesForFile(file)) { + if (isXmlScope(scope)) { + this.addDeferredComponentTypeSymbolCreation(scope.xmlFile); + } + } + } + } + }) + .once('build component types for any component that changes', () => { this.logger.time(LogLevel.info, ['Build component types'], () => { + this.logger.info(`Component Symbols to update:`, [...this.componentSymbolsToUpdate.entries()].sort()); for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) { if (this.updateComponentSymbolInGlobalScope(componentKey, componentName)) { changedComponentTypes.push(util.getSgNodeTypeName(componentName).toLowerCase()); @@ -1160,11 +1159,11 @@ export class Program { this.validationDetails.xmlFilesValidated = []; }) .once('tracks changed symbols and prepares files and scopes for validation', () => { - if (this.options.logLevel === LogLevel.debug) { + if (this.options.logLevel === LogLevel.debug || this.options.logLevel === LogLevel.info) { const changedRuntime = Array.from(changedSymbols.get(SymbolTypeFlag.runtime)).sort(); - this.logger.debug('Changed Symbols (runTime):', changedRuntime.join(', ')); + this.logger.info('Changed Symbols (runTime):', changedRuntime.join(', ')); const changedTypetime = Array.from(changedSymbols.get(SymbolTypeFlag.typetime)).sort(); - this.logger.debug('Changed Symbols (typeTime):', changedTypetime.join(', ')); + this.logger.info('Changed Symbols (typeTime):', changedTypetime.join(', ')); } const scopesToCheck = this.getScopesForCrossScopeValidation(changedComponentTypes.length > 0); @@ -1193,9 +1192,16 @@ export class Program { } } }) - .forEach('validate scopes', () => this.getSortedScopeNames(), (scopeName) => { + .once('checking scopes to validate', () => { //sort the scope names so we get consistent results - let scope = this.scopes[scopeName]; + for (const scopeName of this.getSortedScopeNames()) { + let scope = this.scopes[scopeName]; + if (scope.shouldValidate(this.currentScopeValidationOptions)) { + scopesToValidate.push(scope); + } + } + }) + .forEach('beforeScopeValidate', () => scopesToValidate, (scope) => { if (scope.shouldValidate(this.currentScopeValidationOptions)) { scopesToValidate.push(scope); this.plugins.emit('beforeScopeValidate', { From 6381632f23a900a03d9752673643cff4c20e1010 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Sat, 1 Nov 2025 19:46:48 -0300 Subject: [PATCH 3/6] Be more careful about adding scopes to list of scopes to be checked for cross-scope issues --- src/Program.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Program.ts b/src/Program.ts index 6b27e8a39..d9a09c365 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -1159,14 +1159,15 @@ export class Program { this.validationDetails.xmlFilesValidated = []; }) .once('tracks changed symbols and prepares files and scopes for validation', () => { - if (this.options.logLevel === LogLevel.debug || this.options.logLevel === LogLevel.info) { + if (this.options.logLevel === LogLevel.debug) { const changedRuntime = Array.from(changedSymbols.get(SymbolTypeFlag.runtime)).sort(); - this.logger.info('Changed Symbols (runTime):', changedRuntime.join(', ')); + this.logger.debug('Changed Symbols (runTime):', changedRuntime.join(', ')); const changedTypetime = Array.from(changedSymbols.get(SymbolTypeFlag.typetime)).sort(); - this.logger.info('Changed Symbols (typeTime):', changedTypetime.join(', ')); + this.logger.debug('Changed Symbols (typeTime):', changedTypetime.join(', ')); } - - const scopesToCheck = this.getScopesForCrossScopeValidation(changedComponentTypes.length > 0); + const didComponentChange = changedComponentTypes.length > 0; + const didProvidedSymbolChange = changedSymbols.get(SymbolTypeFlag.runtime).size > 0 || changedSymbols.get(SymbolTypeFlag.runtime).size > 0; + const scopesToCheck = this.getScopesForCrossScopeValidation(didComponentChange, didProvidedSymbolChange); this.crossScopeValidation.buildComponentsMap(); this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck); const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols); @@ -1278,11 +1279,18 @@ export class Program { this.logger.info(`Validation Metrics: ${logs.join(', ')}`); } - private getScopesForCrossScopeValidation(someComponentTypeChanged = false) { - const scopesForCrossScopeValidation = []; + private getScopesForCrossScopeValidation(someComponentTypeChanged: boolean, providedSymbolsChanged: boolean) { + const scopesForCrossScopeValidation: Scope[] = []; for (let scopeName of this.getSortedScopeNames()) { let scope = this.scopes[scopeName]; - if (this.globalScope !== scope && (someComponentTypeChanged || !scope.isValidated)) { + if (this.globalScope === scope) { + continue; + } + if (someComponentTypeChanged) { + scopesForCrossScopeValidation.push(scope); + } + + if (providedSymbolsChanged && !scope.isValidated) { scopesForCrossScopeValidation.push(scope); } } From 6239d248d729e71d3bc57d50a430a5b86b60ffde Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Sat, 1 Nov 2025 19:53:43 -0300 Subject: [PATCH 4/6] Don't add scopes twice, duh --- src/Program.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Program.ts b/src/Program.ts index d9a09c365..3d603f12a 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -1203,13 +1203,10 @@ export class Program { } }) .forEach('beforeScopeValidate', () => scopesToValidate, (scope) => { - if (scope.shouldValidate(this.currentScopeValidationOptions)) { - scopesToValidate.push(scope); - this.plugins.emit('beforeScopeValidate', { - program: this, - scope: scope - }); - } + this.plugins.emit('beforeScopeValidate', { + program: this, + scope: scope + }); }) .forEach('validate scope', () => scopesToValidate, (scope) => { scope.validate(this.currentScopeValidationOptions); From 15659f60779fe4f75d93dda5dcf79ebb3f6902a0 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Sat, 1 Nov 2025 19:59:23 -0300 Subject: [PATCH 5/6] Fixed tests --- src/Program.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Program.ts b/src/Program.ts index 3d603f12a..4049ae7cc 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -1065,7 +1065,6 @@ export class Program { } else if (isBrsFile(file)) { const fileHasChanges = file.providedSymbols.changes.get(SymbolTypeFlag.runtime).size > 0 || file.providedSymbols.changes.get(SymbolTypeFlag.typetime).size > 0; if (fileHasChanges) { - this.logger.info('File changes: ', file.srcPath, file.providedSymbols.changes); for (const scope of this.getScopesForFile(file)) { if (isXmlScope(scope)) { this.addDeferredComponentTypeSymbolCreation(scope.xmlFile); @@ -1076,7 +1075,7 @@ export class Program { }) .once('build component types for any component that changes', () => { this.logger.time(LogLevel.info, ['Build component types'], () => { - this.logger.info(`Component Symbols to update:`, [...this.componentSymbolsToUpdate.entries()].sort()); + this.logger.debug(`Component Symbols to update:`, [...this.componentSymbolsToUpdate.entries()].sort()); for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) { if (this.updateComponentSymbolInGlobalScope(componentKey, componentName)) { changedComponentTypes.push(util.getSgNodeTypeName(componentName).toLowerCase()); @@ -1166,7 +1165,7 @@ export class Program { this.logger.debug('Changed Symbols (typeTime):', changedTypetime.join(', ')); } const didComponentChange = changedComponentTypes.length > 0; - const didProvidedSymbolChange = changedSymbols.get(SymbolTypeFlag.runtime).size > 0 || changedSymbols.get(SymbolTypeFlag.runtime).size > 0; + const didProvidedSymbolChange = changedSymbols.get(SymbolTypeFlag.runtime).size > 0 || changedSymbols.get(SymbolTypeFlag.typetime).size > 0; const scopesToCheck = this.getScopesForCrossScopeValidation(didComponentChange, didProvidedSymbolChange); this.crossScopeValidation.buildComponentsMap(); this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck); From b25ee4b98754b7b8ae9826830193e61d5594127c Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Sun, 2 Nov 2025 19:00:51 -0400 Subject: [PATCH 6/6] limits rebuilding Component Node types to only those nodes that had symbols chnaged --- src/CrossScopeValidator.ts | 19 +++++++++ src/Program.ts | 51 ++++++++++++++++------- src/Scope.spec.ts | 83 ++++++++++++++++++++++++++++++++++++++ src/files/XmlFile.ts | 53 +++++++++++++++++++++++- 4 files changed, 191 insertions(+), 15 deletions(-) diff --git a/src/CrossScopeValidator.ts b/src/CrossScopeValidator.ts index 764a9d91a..9efeb51bb 100644 --- a/src/CrossScopeValidator.ts +++ b/src/CrossScopeValidator.ts @@ -493,6 +493,25 @@ export class CrossScopeValidator { return filesThatNeedRevalidation; } + getScopesRequiringChangedSymbol(scopes: Scope[], changedSymbols: Map>) { + const scopesThatNeedRevalidation = new Set(); + const filesAlreadyChecked = new Set(); + + for (const scope of scopes) { + scope.enumerateBrsFiles((file) => { + if (filesAlreadyChecked.has(file) || scopesThatNeedRevalidation.has(scope)) { + return; + } + filesAlreadyChecked.add(file); + + if (util.hasAnyRequiredSymbolChanged(file.requiredSymbols, changedSymbols)) { + scopesThatNeedRevalidation.add(scope); + } + }); + } + return scopesThatNeedRevalidation; + } + buildComponentsMap() { this.componentsMap.clear(); // Add custom components diff --git a/src/Program.ts b/src/Program.ts index 4049ae7cc..e93cc3b40 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -50,7 +50,7 @@ import { CrossScopeValidator } from './CrossScopeValidator'; import { DiagnosticManager } from './DiagnosticManager'; import { ProgramValidatorDiagnosticsTag } from './bscPlugin/validation/ProgramValidator'; import type { ProvidedSymbolInfo, BrsFile } from './files/BrsFile'; -import type { XmlFile } from './files/XmlFile'; +import type { UnresolvedXMLSymbol, XmlFile } from './files/XmlFile'; import { SymbolTable } from './SymbolTable'; import { ReferenceType, TypesCreated } from './types'; import type { Statement } from './parser/AstNode'; @@ -944,6 +944,18 @@ export class Program { filesToBeValidatedInScopeContext: new Set() }; + public lastValidationInfo: { + brsFilesSrcPath: Set; + xmlFilesSrcPath: Set; + scopeNames: Set; + componentsRebuilt: Set; + } = { + brsFilesSrcPath: new Set(), + xmlFilesSrcPath: new Set(), + scopeNames: new Set(), + componentsRebuilt: new Set() + }; + /** * Counter used to track which validation run is being logged */ @@ -1066,7 +1078,7 @@ export class Program { 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)) { - if (isXmlScope(scope)) { + if (isXmlScope(scope) && this.doesXmlFileRequireProvidedSymbols(scope.xmlFile, file.providedSymbols.changes)) { this.addDeferredComponentTypeSymbolCreation(scope.xmlFile); } } @@ -1076,7 +1088,9 @@ export class Program { .once('build component types for any component that changes', () => { this.logger.time(LogLevel.info, ['Build component types'], () => { this.logger.debug(`Component Symbols to update:`, [...this.componentSymbolsToUpdate.entries()].sort()); + this.lastValidationInfo.componentsRebuilt = new Set(); for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) { + this.lastValidationInfo.componentsRebuilt.add(componentName?.toLowerCase()); if (this.updateComponentSymbolInGlobalScope(componentKey, componentName)) { changedComponentTypes.push(util.getSgNodeTypeName(componentName).toLowerCase()); } @@ -1153,6 +1167,9 @@ export class Program { changedSymbols.set(SymbolTypeFlag.typetime, new Set([...changedSymbols.get(SymbolTypeFlag.typetime), ...changedTypeSymbols, ...dependentTypesChanged])); + this.lastValidationInfo.brsFilesSrcPath = new Set(this.validationDetails.brsFilesValidated.map(f => f.srcPath?.toLowerCase() ?? '')); + this.lastValidationInfo.xmlFilesSrcPath = new Set(this.validationDetails.xmlFilesValidated.map(f => f.srcPath?.toLowerCase() ?? '')); + // can reset filesValidatedList, because they are no longer needed this.validationDetails.brsFilesValidated = []; this.validationDetails.xmlFilesValidated = []; @@ -1167,8 +1184,11 @@ export class Program { const didComponentChange = changedComponentTypes.length > 0; const didProvidedSymbolChange = changedSymbols.get(SymbolTypeFlag.runtime).size > 0 || changedSymbols.get(SymbolTypeFlag.typetime).size > 0; const scopesToCheck = this.getScopesForCrossScopeValidation(didComponentChange, didProvidedSymbolChange); + this.crossScopeValidation.buildComponentsMap(); - this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck); + 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); @@ -1200,6 +1220,7 @@ export class Program { scopesToValidate.push(scope); } } + this.lastValidationInfo.scopeNames = new Set(scopesToValidate.map(s => s.name?.toLowerCase() ?? '')); }) .forEach('beforeScopeValidate', () => scopesToValidate, (scope) => { this.plugins.emit('beforeScopeValidate', { @@ -1267,15 +1288,7 @@ export class Program { } } - protected logValidationMetrics(metrics: Record) { - let logs = [] as string[]; - for (const key in metrics) { - logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`); - } - this.logger.info(`Validation Metrics: ${logs.join(', ')}`); - } - - private getScopesForCrossScopeValidation(someComponentTypeChanged: boolean, providedSymbolsChanged: boolean) { + private getScopesForCrossScopeValidation(someComponentTypeChanged: boolean, didProvidedSymbolChange: boolean) { const scopesForCrossScopeValidation: Scope[] = []; for (let scopeName of this.getSortedScopeNames()) { let scope = this.scopes[scopeName]; @@ -1285,14 +1298,24 @@ export class Program { if (someComponentTypeChanged) { scopesForCrossScopeValidation.push(scope); } - - if (providedSymbolsChanged && !scope.isValidated) { + if (didProvidedSymbolChange && !scope.isValidated) { scopesForCrossScopeValidation.push(scope); } } return scopesForCrossScopeValidation; } + private doesXmlFileRequireProvidedSymbols(file: XmlFile, providedSymbolsByFlag: Map>) { + for (const required of file.requiredSymbols) { + const symbolNameLower = (required as UnresolvedXMLSymbol).name.toLowerCase(); + const requiredSymbolIsProvided = providedSymbolsByFlag.get(required.flags).has(symbolNameLower); + if (requiredSymbolIsProvided) { + return true; + } + } + return false; + } + /** * Flag all duplicate component names */ diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts index f0d8c6c3f..91bcc59fa 100644 --- a/src/Scope.spec.ts +++ b/src/Scope.spec.ts @@ -4522,6 +4522,89 @@ describe('Scope', () => { }); }); + describe('xmlFiles', () => { + + it('does not rebuild component if import file has no substantive changes', () => { + program.setFile('source/util.bs', ` + function test1() as integer + return 1 + end function + + + function test2() as boolean + return true + end function + `); + program.setFile('source/file2.bs', ``); + //let widgetXml = + program.setFile('components/Widget.xml', trim` + + +