diff --git a/src/Program.spec.ts b/src/Program.spec.ts
index 622042578..8cf89770b 100644
--- a/src/Program.spec.ts
+++ b/src/Program.spec.ts
@@ -24,6 +24,7 @@ import { StringType, TypedFunctionType, DynamicType, FloatType, IntegerType, Int
import { AssociativeArrayType } from './types/AssociativeArrayType';
import { ComponentType } from './types/ComponentType';
import * as path from 'path';
+import undent from 'undent';
const sinon = createSandbox();
@@ -2700,6 +2701,35 @@ describe('Program', () => {
fsExtra.pathExistsSync(`${stagingDir}/source/bslib.brs`)
).to.be.true;
});
+
+ it('transpiles namespaces properly', async () => {
+ program.options.autoImportComponentScript = true;
+ program.setFile('manifest', '');
+ program.setFile('components/MainScene.xml', trim`
+
+
+ `);
+ program.setFile('source/main.bs', `
+ namespace alpha
+ sub test()
+ end sub
+ end namespace
+ sub init()
+ alpha.test()
+ end sub
+ `);
+ await program.build();
+ expect(
+ fsExtra.readFileSync(`${stagingDir}/source/main.brs`).toString()
+ ).to.eql(undent`
+ sub alpha_test()
+ end sub
+
+ sub init()
+ alpha_test()
+ end sub
+ `);
+ });
});
describe('global symbol table', () => {
diff --git a/src/Program.ts b/src/Program.ts
index 6669b79c0..dca9e507e 100644
--- a/src/Program.ts
+++ b/src/Program.ts
@@ -5,7 +5,7 @@ import type { CodeAction, Position, Range, SignatureInformation, Location, Docum
import type { BsConfig, FinalizedBsConfig } from './BsConfig';
import { Scope } from './Scope';
import { DiagnosticMessages } from './DiagnosticMessages';
-import type { FileObj, SemanticToken, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, BeforeFileAddEvent, BeforeFileRemoveEvent, PrepareFileEvent, PrepareProgramEvent, ProvideFileEvent, SerializedFile, TranspileObj } from './interfaces';
+import type { FileObj, SemanticToken, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, BeforeFileAddEvent, BeforeFileRemoveEvent, PrepareFileEvent, PrepareProgramEvent, ProvideFileEvent, SerializedFile, TranspileObj, SerializeFileEvent } from './interfaces';
import { standardizePath as s, util } from './util';
import { XmlScope } from './XmlScope';
import { DependencyGraph } from './DependencyGraph';
@@ -285,6 +285,14 @@ export class Program {
protected addScope(scope: Scope) {
this.scopes[scope.name] = scope;
+ delete this.sortedScopeNames;
+ }
+
+ protected removeScope(scope: Scope) {
+ if (this.scopes[scope.name]) {
+ delete this.scopes[scope.name];
+ delete this.sortedScopeNames;
+ }
}
/**
@@ -773,7 +781,7 @@ export class Program {
scope.dispose();
//notify dependencies of this scope that it has been removed
this.dependencyGraph.remove(scope.dependencyGraphKey!);
- delete this.scopes[file.destPath];
+ this.removeScope(this.scopes[file.destPath]);
this.plugins.emit('afterScopeDispose', scopeDisposeEvent);
}
//remove the file from the program
@@ -1047,28 +1055,33 @@ export class Program {
}
}
+ private sortedScopeNames: string[] = undefined;
+
/**
* Gets a sorted list of all scopeNames, always beginning with "global", "source", then any others in alphabetical order
*/
private getSortedScopeNames() {
- return Object.keys(this.scopes).sort((a, b) => {
- if (a === 'global') {
- return -1;
- } else if (b === 'global') {
- return 1;
- }
- if (a === 'source') {
- return -1;
- } else if (b === 'source') {
- return 1;
- }
- if (a < b) {
- return -1;
- } else if (b < a) {
- return 1;
- }
- return 0;
- });
+ if (!this.sortedScopeNames) {
+ this.sortedScopeNames = Object.keys(this.scopes).sort((a, b) => {
+ if (a === 'global') {
+ return -1;
+ } else if (b === 'global') {
+ return 1;
+ }
+ if (a === 'source') {
+ return -1;
+ } else if (b === 'source') {
+ return 1;
+ }
+ if (a < b) {
+ return -1;
+ } else if (b < a) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+ return this.sortedScopeNames;
}
/**
@@ -1457,21 +1470,22 @@ export class Program {
* @param files the list of files that should be prepared
*/
private async prepare(files: BscFile[]) {
- const programEvent = {
+ const programEvent: PrepareProgramEvent = {
program: this,
editor: this.editor,
files: files
- } as PrepareProgramEvent;
+ };
//assign an editor to every file
- for (const file of files) {
+ for (const file of programEvent.files) {
//if the file doesn't have an editor yet, assign one now
if (!file.editor) {
file.editor = new Editor();
}
}
- files.sort((a, b) => {
+ //sort the entries to make transpiling more deterministic
+ programEvent.files.sort((a, b) => {
if (a.pkgPath < b.pkgPath) {
return -1;
} else if (a.pkgPath > b.pkgPath) {
@@ -1489,6 +1503,10 @@ export class Program {
const entries: TranspileObj[] = [];
for (const file of files) {
+ const scope = this.getFirstScopeForFile(file);
+ //link the symbol table for all the files in this scope
+ scope?.linkSymbolTable();
+
//if the file doesn't have an editor yet, assign one now
if (!file.editor) {
file.editor = new Editor();
@@ -1497,6 +1515,7 @@ export class Program {
program: this,
file: file,
editor: file.editor,
+ scope: scope,
outputPath: this.getOutputPath(file, stagingDir)
} as PrepareFileEvent & { outputPath: string };
@@ -1506,6 +1525,9 @@ export class Program {
//TODO remove this in v1
entries.push(event);
+
+ //unlink the symbolTable so the next loop iteration can link theirs
+ scope?.unlinkSymbolTable();
}
await this.plugins.emitAsync('afterPrepareProgram', programEvent);
@@ -1529,34 +1551,27 @@ export class Program {
files: files,
result: allFiles
});
- await this.plugins.emitAsync('onSerializeProgram', {
- program: this,
- files: files,
- result: allFiles
- });
-
- //sort the entries to make transpiling more deterministic
- files = serializeProgramEvent.files.sort((a, b) => {
- return a.srcPath < b.srcPath ? -1 : 1;
- });
+ await this.plugins.emitAsync('onSerializeProgram', serializeProgramEvent);
// serialize each file
for (const file of files) {
- const event = {
+ const scope = this.getFirstScopeForFile(file);
+ //link the symbol table for all the files in this scope
+ scope?.linkSymbolTable();
+ const event: SerializeFileEvent = {
program: this,
file: file,
+ scope: scope,
result: allFiles
};
await this.plugins.emitAsync('beforeSerializeFile', event);
await this.plugins.emitAsync('serializeFile', event);
await this.plugins.emitAsync('afterSerializeFile', event);
+ //unlink the symbolTable so the next loop iteration can link theirs
+ scope?.unlinkSymbolTable();
}
- this.plugins.emit('afterSerializeProgram', {
- program: this,
- files: files,
- result: allFiles
- });
+ this.plugins.emit('afterSerializeProgram', serializeProgramEvent);
return allFiles;
}
diff --git a/src/interfaces.ts b/src/interfaces.ts
index efa9863d4..a62785a16 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -785,6 +785,7 @@ export type BeforePrepareProgramEvent = PrepareProgramEvent;
export interface PrepareProgramEvent {
program: Program;
editor: Editor;
+ files: BscFile[];
}
export type AfterPrepareProgramEvent = PrepareProgramEvent;
@@ -797,6 +798,12 @@ export interface PrepareFileEvent {
program: Program;
file: TFile;
editor: Editor;
+ /**
+ * The scope that was linked for this event. A file may be included in multiple scopes, but we choose the most relevant scope.
+ * Plugins may unlink this scope and link another one, but must then reassign this property to that new scope so that other
+ * plugins can reference it.
+ */
+ scope: Scope;
}
export type OnPrepareFileEvent = PrepareFileEvent;
export type AfterPrepareFileEvent = PrepareFileEvent;
@@ -837,6 +844,12 @@ export type BeforeSerializeFileEvent = Serializ
export interface SerializeFileEvent {
program: Program;
file: TFile;
+ /**
+ * The scope that was linked for this event. A file may be included in multiple scopes, but we choose the most relevant scope.
+ * Plugins may unlink this scope and link another one, but must then reassign this property to that new scope so that other
+ * plugins can reference it.
+ */
+ scope: Scope;
/**
* The list of all files created across all the `SerializeFile` events.
* The key is the pkgPath of the file, and the