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
19 changes: 19 additions & 0 deletions src/CrossScopeValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,25 @@ export class CrossScopeValidator {
return filesThatNeedRevalidation;
}

getScopesRequiringChangedSymbol(scopes: Scope[], changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
const scopesThatNeedRevalidation = new Set<Scope>();
const filesAlreadyChecked = new Set<BrsFile>();

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
Expand Down
131 changes: 81 additions & 50 deletions src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 type { BscType } from './types/BscType';
import { ReferenceType } from './types/ReferenceType';
Expand Down Expand Up @@ -955,6 +955,18 @@ export class Program {
filesToBeValidatedInScopeContext: new Set<BscFile>()
};

public lastValidationInfo: {
brsFilesSrcPath: Set<string>;
xmlFilesSrcPath: Set<string>;
scopeNames: Set<string>;
componentsRebuilt: Set<string>;
} = {
brsFilesSrcPath: new Set<string>(),
xmlFilesSrcPath: new Set<string>(),
scopeNames: new Set<string>(),
componentsRebuilt: new Set<string>()
};

/**
* Counter used to track which validation run is being logged
*/
Expand Down Expand Up @@ -1031,30 +1043,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);
Expand Down Expand Up @@ -1087,9 +1082,26 @@ 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) {
for (const scope of this.getScopesForFile(file)) {
if (isXmlScope(scope) && this.doesXmlFileRequireProvidedSymbols(scope.xmlFile, file.providedSymbols.changes)) {
this.addDeferredComponentTypeSymbolCreation(scope.xmlFile);
}
}
}
}
})
.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<string>();
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());
}
Expand Down Expand Up @@ -1166,21 +1178,28 @@ export class Program {

changedSymbols.set(SymbolTypeFlag.typetime, new Set([...changedSymbols.get(SymbolTypeFlag.typetime), ...changedTypeSymbols, ...dependentTypesChanged]));

this.lastValidationInfo.brsFilesSrcPath = new Set<string>(this.validationDetails.brsFilesValidated.map(f => f.srcPath?.toLowerCase() ?? ''));
this.lastValidationInfo.xmlFilesSrcPath = new Set<string>(this.validationDetails.xmlFilesValidated.map(f => f.srcPath?.toLowerCase() ?? ''));

// can reset filesValidatedList, because they are no longer needed
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(', '));
const changedTypetime = Array.from(changedSymbols.get(SymbolTypeFlag.typetime)).sort();
this.logger.debug('Changed Symbols (typeTime):', changedTypetime.join(', '));
}
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);

const scopesToCheck = this.getScopesForCrossScopeValidation(changedComponentTypes.length > 0);
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);
Expand All @@ -1204,20 +1223,23 @@ 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];
if (scope.shouldValidate(this.currentScopeValidationOptions)) {
scopesToValidate.push(scope);
this.plugins.emit('beforeScopeValidate', {
program: this,
scope: scope
});
for (const scopeName of this.getSortedScopeNames()) {
let scope = this.scopes[scopeName];
if (scope.shouldValidate(this.currentScopeValidationOptions)) {
scopesToValidate.push(scope);
}
}
this.lastValidationInfo.scopeNames = new Set<string>(scopesToValidate.map(s => s.name?.toLowerCase() ?? ''));
})
.forEach('validate scope', () => this.getSortedScopeNames(), (scopeName) => {
//sort the scope names so we get consistent results
let scope = this.scopes[scopeName];
.forEach('beforeScopeValidate', () => scopesToValidate, (scope) => {
this.plugins.emit('beforeScopeValidate', {
program: this,
scope: scope
});
})
.forEach('validate scope', () => scopesToValidate, (scope) => {
scope.validate(this.currentScopeValidationOptions);
})
.forEach('afterScopeValidate', () => scopesToValidate, (scope) => {
Expand Down Expand Up @@ -1277,25 +1299,34 @@ export class Program {
}
}

protected logValidationMetrics(metrics: Record<string, number | string>) {
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 = false) {
const scopesForCrossScopeValidation = [];
private getScopesForCrossScopeValidation(someComponentTypeChanged: boolean, didProvidedSymbolChange: 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 (didProvidedSymbolChange && !scope.isValidated) {
scopesForCrossScopeValidation.push(scope);
}
}
return scopesForCrossScopeValidation;
}

private doesXmlFileRequireProvidedSymbols(file: XmlFile, providedSymbolsByFlag: Map<SymbolTypeFlag, Set<string>>) {
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
*/
Expand Down
83 changes: 83 additions & 0 deletions src/Scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4621,6 +4621,89 @@ describe('Scope', () => {
});
});

describe('xmlFiles', () => {

it('does not rebuild component if import file has no substantive changes', () => {
program.setFile<BrsFile>('source/util.bs', `
function test1() as integer
return 1
end function


function test2() as boolean
return true
end function
`);
program.setFile<BrsFile>('source/file2.bs', ``);
//let widgetXml =
program.setFile<BrsFile>('components/Widget.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="Widget" extends="Group">
<script uri="pkg:/source/util.bs"/>
<interface>
<function name="test1" />
<function name="test2" />
</interface>
</component>
`);
program.validate();


program.setFile<BrsFile>('source/util.bs', `
function test1() as integer
return 1
end function


function test2() as boolean
return false ' changed value, but not type!
end function
`);
program.validate();
expectZeroDiagnostics(program);
expect(program.lastValidationInfo.componentsRebuilt).to.be.empty;
});

it('rebuilds component if import file has substantive changes', () => {
program.setFile<BrsFile>('source/util.bs', `
function test1() as integer
return 1
end function

function test2() as boolean
return true
end function
`);
program.setFile<BrsFile>('source/file2.bs', ``);
//let widgetXml =
program.setFile<BrsFile>('components/Widget.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="Widget" extends="Group">
<script uri="pkg:/source/util.bs"/>
<interface>
<function name="test1" />
<function name="test2" />
</interface>
</component>
`);
program.validate();


program.setFile<BrsFile>('source/util.bs', `
function test1() as integer
return 1
end function

function test2() as integer ' changed type
return 2
end function
`);
program.validate();
expectZeroDiagnostics(program);
expect(program.lastValidationInfo.componentsRebuilt.has('widget')).to.be.true;
});
});

});

describe('shadowing', () => {
Expand Down
53 changes: 52 additions & 1 deletion src/files/XmlFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@ import SGParser from '../parser/SGParser';
import chalk from 'chalk';
import { Cache } from '../Cache';
import type { DependencyChangedEvent, DependencyGraph } from '../DependencyGraph';
import type { SGToken } from '../parser/SGTypes';
import type { SGInterfaceField, SGInterfaceFunction, SGToken } from '../parser/SGTypes';
import { CommentFlagProcessor } from '../CommentFlagProcessor';
import type { IToken, TokenType } from 'chevrotain';
import { TranspileState } from '../parser/TranspileState';
import type { BscFile } from './BscFile';
import type { Editor } from '../astUtils/Editor';
import type { FunctionScope } from '../FunctionScope';
import { SymbolTypeFlag } from '../SymbolTypeFlag';


export interface UnresolvedXMLSymbol {
flags: SymbolTypeFlag;
name: string;
file: XmlFile;
}


export class XmlFile implements BscFile {
/**
Expand Down Expand Up @@ -188,6 +197,48 @@ export class XmlFile implements BscFile {
});
}

public get requiredSymbols() {
return this.cache.getOrAdd(`requiredSymbols`, () => {
this.program.logger.debug('Getting required symbols', this.srcPath);


const requiredSymbols: UnresolvedXMLSymbol[] = [];

const allInterfaceFunctions = this.parser.ast.componentElement?.interfaceElement?.getElementsByTagName<SGInterfaceFunction>('function') ?? [];

for (const node of allInterfaceFunctions) {
if (node.name) {
requiredSymbols.push({
flags: SymbolTypeFlag.runtime,
file: this,
name: node.name.toLowerCase()
});
}
}

const allInterfaceFields = this.parser.ast.componentElement?.interfaceElement?.getElementsByTagName<SGInterfaceField>('field') ?? [];

for (const node of allInterfaceFields) {
if (node.onChange) {
requiredSymbols.push({
flags: SymbolTypeFlag.runtime,
file: this,
name: node.onChange.toLowerCase()
});
}
// TODO: when we can specify proper types in fields, add those types too:
//if (node.type && isCustomXmlType(node.type)) {
// requiredSymbols.push({
// flags: SymbolTypeFlag.typetime,
// file: this,
// name: node.type.toLowerCase()
// });
//}
}
return requiredSymbols;
});
}


/**
* The range of the entire file
Expand Down