Skip to content
18 changes: 18 additions & 0 deletions src/BsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ export interface BsConfig {
*/
noEmit?: boolean;

/**
* @deprecated packaging is handled outside of `BsConfig`
*/
createPackage?: boolean;

/**
* @deprecated deployment is handled outside of `BsConfig`
*/
deploy?: boolean;

/**
* @deprecated staging copy behavior is handled outside of `BsConfig`
*/
copyToStaging?: boolean;

/**
* If true, the server will keep running and will watch and recompile on every file change
* @default false
Expand Down Expand Up @@ -281,6 +296,9 @@ type OptionalBsConfigFields =
| 'extends'
| 'require'
| 'outDir'
| 'createPackage'
| 'deploy'
| 'copyToStaging'
| 'diagnosticLevel'
| 'rootDir'
| 'stagingDir'
Expand Down
64 changes: 48 additions & 16 deletions src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ export class Program {
file: file,
program: this
};
this.scopesPerFile.clear();

this.plugins.emit('beforeAddFile', fileAddEvent);

Expand All @@ -778,6 +779,7 @@ export class Program {
private unassignFile<T extends BscFile = BscFile>(file: T) {
delete this.files[file.srcPath.toLowerCase()];
this.destMap.delete(file.destPath.toLowerCase());
this.scopesPerFile.clear();
return file;
}

Expand Down Expand Up @@ -1101,6 +1103,7 @@ export class Program {
xmlFilesValidated: XmlFile[];
changedSymbols: Map<SymbolTypeFlag, Set<string>>;
changedComponentTypes: string[];
scopesToInvalidate: Scope[];
scopesToValidate: Scope[];
filesToBeValidatedInScopeContext: Set<BscFile>;

Expand All @@ -1109,6 +1112,7 @@ export class Program {
xmlFilesValidated: [],
changedSymbols: new Map<SymbolTypeFlag, Set<string>>(),
changedComponentTypes: [],
scopesToInvalidate: [],
scopesToValidate: [],
filesToBeValidatedInScopeContext: new Set<BscFile>()
};
Expand Down Expand Up @@ -1166,14 +1170,15 @@ 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;

//validate every file

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({
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -1361,9 +1366,11 @@ 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) {
const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols);
for (const file of filesToRevalidate) {
filesToBeValidatedInScopeContext.add(file);
}
}

this.currentScopeValidationOptions = {
Expand All @@ -1376,18 +1383,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);
}
Expand All @@ -1411,19 +1428,21 @@ export class Program {
})
.once('detect duplicate component names', () => {
this.detectDuplicateComponentNames();
this.isFirstValidation = false;

// can reset other validation details
this.validationDetails.changedSymbols = new Map<SymbolTypeFlag, Set<string>>();
this.validationDetails.scopesToValidate = [];
this.validationDetails.filesToBeValidatedInScopeContext = new Set<BscFile>();

})
.onCancel(() => {
logValidateEnd('cancelled');
})
.onSuccess(() => {
logValidateEnd();
this.isFirstValidation = false;

// can reset other validation details
this.validationDetails.changedSymbols = new Map<SymbolTypeFlag, Set<string>>();
this.validationDetails.scopesToInvalidate = [];
this.validationDetails.scopesToValidate = [];
this.validationDetails.filesToBeValidatedInScopeContext = new Set<BscFile>();
})
.onComplete(() => {
//if we emitted the beforeValidateProgram hook, emit the afterValidateProgram hook as well
Expand Down Expand Up @@ -1462,8 +1481,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;
}
Expand Down Expand Up @@ -1612,13 +1630,26 @@ export class Program {
return this.sortedScopeNames;
}

public getAllUserScopes() {
return Object.values(this.scopes).filter(s => s.name !== 'global');
}


private scopesPerFile: Map<BscFile, Scope[]> = new Map();


/**
* Get a list of all scopes the file is loaded into
* @param file the file
*/
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();
Expand All @@ -1630,6 +1661,7 @@ export class Program {
}
}
}
this.scopesPerFile.set(resolvedFile, result);
return result;
}

Expand Down
74 changes: 74 additions & 0 deletions src/Scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<?xml version="1.0" encoding="utf-8" ?>
<component name="Parent" extends="Group">
<script uri="Parent.bs"/>
<interface>
<field id="myAA" type="assocArray" />
</interface>
</component>
`);
program.setFile('components/ParentTypes.bs', `
interface iParent
top as roSGNodeParent
comp2 as roSGNodeComp2
end interface
`);

program.setFile('components/Parent.bs', `
import "pkg:/components/ParentTypes.bs"

typecast m as iParent

sub init()
m.comp2 = createObject("roSGNode", "Comp2")
end sub
`);

program.setFile('components/Comp.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="Comp" extends="Parent">
<script uri="Comp.bs"/>
</component>
`);
program.setFile(s`components/Comp.bs`, `
import "pkg:/components/CompTypes.bs"

typecast m as iComp

sub doSomething()
m.comp2@.takesInterface({name: "example", data: m.top.myAA})
end sub
`);
program.setFile(s`components/CompTypes.bs`, `
import "pkg:/components/ParentTypes.bs"
interface iComp extends iParent
top as roSGNodeComp
end interface
`);

program.setFile('components/Comp2.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="Comp2" extends="Group">
<script uri="Comp2.bs"/>
<interface>
<function name="takesInterface" />
</interface>
</component>
`);
program.setFile(s`components/Comp2.bs`, `
interface IfaceOptions
name as string
optional data as roAssociativeArray
end interface

sub takesInterface(aa as IfaceOptions)
print aa
end sub
`);
program.validate();
expectZeroDiagnostics(program);
});
});

describe('revalidations', () => {
it('revalidates dependent files when a file is changed', () => {
program.setFile('source/common.bs', `
Expand Down
11 changes: 10 additions & 1 deletion src/astUtils/reflection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Body, AssignmentStatement, Block, ExpressionStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement, DimStatement, TypecastStatement, AliasStatement, AugmentedAssignmentStatement, ConditionalCompileConstStatement, ConditionalCompileErrorStatement, ConditionalCompileStatement, ExitStatement, TypeStatement } from '../parser/Statement';
import type { Body, AssignmentStatement, Block, EmptyStatement, ExpressionStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement, DimStatement, TypecastStatement, AliasStatement, AugmentedAssignmentStatement, ConditionalCompileConstStatement, ConditionalCompileErrorStatement, ConditionalCompileStatement, ExitStatement, TypeStatement } from '../parser/Statement';
import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, AAIndexedMemberExpression, TernaryExpression, NullCoalescingExpression, PrintSeparatorExpression, TypecastExpression, TypedArrayExpression, TypeExpression, InlineInterfaceMemberExpression, InlineInterfaceExpression, TypedFunctionTypeExpression } from '../parser/Expression';
import type { BrsFile } from '../files/BrsFile';
import type { XmlFile } from '../files/XmlFile';
Expand Down Expand Up @@ -102,6 +102,15 @@ export function isBlock(element: AstNode | undefined): element is Block {
export function isExpressionStatement(element: AstNode | undefined): element is ExpressionStatement {
return element?.kind === AstNodeKind.ExpressionStatement;
}
export function isEmptyStatement(element: AstNode | undefined): element is EmptyStatement {
return element?.kind === AstNodeKind.EmptyStatement;
}
/**
* @deprecated use `isEmptyStatement` instead
*/
export function isCommentStatement(element: AstNode | undefined): element is EmptyStatement {
return isEmptyStatement(element);
}
export function isExitStatement(element: AstNode | undefined): element is ExitStatement {
return element?.kind === AstNodeKind.ExitStatement;
}
Expand Down
Loading
Loading