From ccee6adf8a642decf9ce7f7cb595355cf194b88e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 16:16:54 +0000 Subject: [PATCH 1/5] Initial plan From 492201163ff57b5c743405b3a5ff48558688cb45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 16:24:34 +0000 Subject: [PATCH 2/5] feat: add before/on/after hooks across plugin events Agent-Logs-Url: https://github.com/rokucommunity/brighterscript/sessions/a7fc302f-ff61-4651-b61a-5bd4306ce21f Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com> --- docs/plugins.md | 45 ++++++++++++++++++- src/Program.spec.ts | 89 +++++++++++++++++++++++++++++++++++++- src/Program.ts | 46 +++++++++++++++----- src/ProgramBuilder.spec.ts | 5 ++- src/ProgramBuilder.ts | 3 ++ src/interfaces.ts | 28 +++++++++++- 6 files changed, 198 insertions(+), 18 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 662ae87ec..d3a50bf32 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -51,13 +51,20 @@ While there are no restrictions on plugin names, it helps others to find your pl Full compiler lifecycle: - `beforeProgramCreate` +- `onProgramCreate` - `afterProgramCreate` + - `beforeScopeCreate` ("source" scope) + - `onScopeCreate` ("source" scope) - `afterScopeCreate` ("source" scope) - For each file: - `beforeFileParse` + - `onFileParse` - `afterFileParse` + - `beforeScopeCreate` (component scope) + - `onScopeCreate` (component scope) - `afterScopeCreate` (component scope) - `beforeProgramValidate` + - `onProgramValidate` - For each file: - `beforeFileValidate` - `onFileValidate` @@ -68,15 +75,21 @@ Full compiler lifecycle: - `afterScopeValidate` - `afterProgramValidate` - `beforePrepublish` +- `onPrepublish` - `afterPrepublish` - `beforePublish` +- `onPublish` - `beforeProgramTranspile` + - `onProgramTranspile` - For each file: - `beforeFileTranspile` + - `onFileTranspile` - `afterFileTranspile` - `afterProgramTranspile` - `afterPublish` - `beforeProgramDispose` +- `onProgramDispose` +- `afterProgramDispose` ### Language server @@ -86,13 +99,18 @@ When a file is removed: - `beforeFileDispose` - `beforeScopeDispose` (component scope) +- `onScopeDispose` (component scope) - `afterScopeDispose` (component scope) +- `onFileDispose` - `afterFileDispose` When a file is added: - `beforeFileParse` +- `onFileParse` - `afterFileParse` +- `beforeScopeCreate` (component scope) +- `onScopeCreate` (component scope) - `afterScopeCreate` (component scope) - `afterFileValidate` @@ -104,17 +122,21 @@ When any file is modified: After file addition/removal (note: throttled/debounced): - `beforeProgramValidate` +- `onProgramValidate` - For each invalidated/not-yet-validated file - `beforeFileValidate` - `onFileValidate` - `afterFileValidate` - For each invalidated scope: - `beforeScopeValidate` + - `onScopeValidate` - `afterScopeValidate` - `afterProgramValidate` Code Actions + - `beforeGetCodeActions` - `onGetCodeActions` + - `afterGetCodeActions` Completions - `beforeProvideCompletions` @@ -127,7 +149,9 @@ Hovers - `afterProvideHover` Semantic Tokens - - `onGetSemanticTokens` + - `beforeGetSemanticTokens` + - `onGetSemanticTokens` + - `afterGetSemanticTokens` ## Compiler API @@ -157,17 +181,26 @@ export interface CompilerPlugin { name: string; //program events beforeProgramCreate?: (builder: ProgramBuilder) => void; + onProgramCreate?: (program: Program) => void; + afterProgramCreate?: (program: Program) => void; beforePrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void; + onPrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void; afterPrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void; beforePublish?: (builder: ProgramBuilder, files: FileObj[]) => void; + onPublish?: (builder: ProgramBuilder, files: FileObj[]) => void; afterPublish?: (builder: ProgramBuilder, files: FileObj[]) => void; - afterProgramCreate?: (program: Program) => void; beforeProgramValidate?: (program: Program) => void; + onProgramValidate?: (program: Program) => void; afterProgramValidate?: (program: Program) => void; beforeProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; + onProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; afterProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; beforeProgramDispose?: PluginHandler; + onProgramDispose?: PluginHandler; + afterProgramDispose?: PluginHandler; + beforeGetCodeActions?: PluginHandler; onGetCodeActions?: PluginHandler; + afterGetCodeActions?: PluginHandler; /** * Emitted before the program starts collecting completions @@ -259,16 +292,22 @@ export interface CompilerPlugin { afterProvideWorkspaceSymbols?(event: AfterProvideWorkspaceSymbolsEvent): any; + beforeGetSemanticTokens?: PluginHandler; onGetSemanticTokens?: PluginHandler; + afterGetSemanticTokens?: PluginHandler; //scope events + beforeScopeCreate?: (scope: Scope) => void; + onScopeCreate?: (scope: Scope) => void; afterScopeCreate?: (scope: Scope) => void; beforeScopeDispose?: (scope: Scope) => void; + onScopeDispose?: (scope: Scope) => void; afterScopeDispose?: (scope: Scope) => void; beforeScopeValidate?: ValidateHandler; onScopeValidate?: PluginHandler; afterScopeValidate?: ValidateHandler; //file events beforeFileParse?: (source: SourceObj) => void; + onFileParse?: (source: SourceObj) => void; afterFileParse?: (file: BscFile) => void; /** * Called before each file is validated @@ -283,8 +322,10 @@ export interface CompilerPlugin { */ afterFileValidate?: (file: BscFile) => void; beforeFileTranspile?: PluginHandler; + onFileTranspile?: PluginHandler; afterFileTranspile?: PluginHandler; beforeFileDispose?: (file: BscFile) => void; + onFileDispose?: (file: BscFile) => void; afterFileDispose?: (file: BscFile) => void; } diff --git a/src/Program.spec.ts b/src/Program.spec.ts index f86d6a1ab..80a5110e8 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -185,21 +185,29 @@ describe('Program', () => { it(`emits events for scope and file creation`, () => { const beforeProgramValidate = sinon.spy(); + const onProgramValidate = sinon.spy(); const afterProgramValidate = sinon.spy(); + const beforeScopeCreate = sinon.spy(); + const onScopeCreate = sinon.spy(); const afterScopeCreate = sinon.spy(); const beforeScopeValidate = sinon.spy(); const afterScopeValidate = sinon.spy(); const beforeFileParse = sinon.spy(); + const onFileParse = sinon.spy(); const afterFileParse = sinon.spy(); const afterFileValidate = sinon.spy(); program.plugins = new PluginInterface([{ name: 'emits events for scope and file creation', beforeProgramValidate: beforeProgramValidate, + onProgramValidate: onProgramValidate, afterProgramValidate: afterProgramValidate, + beforeScopeCreate: beforeScopeCreate, + onScopeCreate: onScopeCreate, afterScopeCreate: afterScopeCreate, beforeScopeValidate: beforeScopeValidate, afterScopeValidate: afterScopeValidate, beforeFileParse: beforeFileParse, + onFileParse: onFileParse, afterFileParse: afterFileParse, afterFileValidate: afterFileValidate }], { logger: createLogger() }); @@ -216,14 +224,18 @@ describe('Program', () => { //program events expect(beforeProgramValidate.callCount).to.equal(1); + expect(onProgramValidate.callCount).to.equal(1); expect(afterProgramValidate.callCount).to.equal(1); //scope events //(we get component scope event only because source is created in beforeEach) + expect(beforeScopeCreate.callCount).to.equal(1); + expect(onScopeCreate.callCount).to.equal(1); expect(afterScopeCreate.callCount).to.equal(1); expect(beforeScopeValidate.callCount).to.equal(2); expect(afterScopeValidate.callCount).to.equal(2); //file events expect(beforeFileParse.callCount).to.equal(2); + expect(onFileParse.callCount).to.equal(2); expect(afterFileParse.callCount).to.equal(2); expect(afterFileValidate.callCount).to.equal(2); }); @@ -2439,12 +2451,18 @@ describe('Program', () => { print "hello world" end sub `); + const onFileTranspile = sinon.spy(); + const onProgramTranspile = sinon.spy(); + const afterProgramTranspile = sinon.spy(); const plugin = program.plugins.add({ name: 'TestPlugin', beforeFileTranspile: (event) => { const stmt = ((event.file as BrsFile).ast.statements[0] as FunctionStatement).func.body.statements[0] as PrintStatement; event.editor.setProperty((stmt.expressions[0] as LiteralExpression).token, 'text', '"hello there"'); }, + onFileTranspile: onFileTranspile, + onProgramTranspile: onProgramTranspile, + afterProgramTranspile: afterProgramTranspile, afterFileTranspile: sinon.spy() }); expect( @@ -2454,6 +2472,9 @@ describe('Program', () => { print "hello there" end sub` ); + expect(onFileTranspile.callCount).to.be.greaterThan(0); + expect(onProgramTranspile.callCount).to.be.greaterThan(0); + expect(afterProgramTranspile.callCount).to.be.greaterThan(0); expect(plugin.afterFileTranspile.callCount).to.be.greaterThan(0); }); @@ -3673,14 +3694,78 @@ describe('Program', () => { expect(plugin.afterFileValidate.callCount).to.equal(1); }); - it('emits program dispose event', () => { + it('emits file and scope dispose events', () => { const plugin = { name: 'test', - beforeProgramDispose: sinon.spy() + beforeFileDispose: sinon.spy(), + onFileDispose: sinon.spy(), + afterFileDispose: sinon.spy(), + beforeScopeDispose: sinon.spy(), + onScopeDispose: sinon.spy(), + afterScopeDispose: sinon.spy() + }; + program.plugins.add(plugin); + program.setFile('components/main.xml', ` + + `); + program.removeFile('components/main.xml'); + expect(plugin.beforeFileDispose.callCount).to.equal(1); + expect(plugin.onFileDispose.callCount).to.equal(1); + expect(plugin.afterFileDispose.callCount).to.equal(1); + expect(plugin.beforeScopeDispose.callCount).to.equal(1); + expect(plugin.onScopeDispose.callCount).to.equal(1); + expect(plugin.afterScopeDispose.callCount).to.equal(1); + }); + + it('emits program dispose events', () => { + const plugin = { + name: 'test', + beforeProgramDispose: sinon.spy(), + onProgramDispose: sinon.spy(), + afterProgramDispose: sinon.spy() }; program.plugins.add(plugin); program.dispose(); expect(plugin.beforeProgramDispose.callCount).to.equal(1); + expect(plugin.onProgramDispose.callCount).to.equal(1); + expect(plugin.afterProgramDispose.callCount).to.equal(1); + }); + + it('emits before/on/after code-action-like events', () => { + const plugin = { + name: 'test', + beforeGetCodeActions: sinon.spy(), + onGetCodeActions: sinon.spy(), + afterGetCodeActions: sinon.spy(), + beforeGetSourceFixAllCodeActions: sinon.spy(), + onGetSourceFixAllCodeActions: sinon.spy(), + afterGetSourceFixAllCodeActions: sinon.spy(), + beforeGetSemanticTokens: sinon.spy(), + onGetSemanticTokens: sinon.spy(), + afterGetSemanticTokens: sinon.spy() + }; + program.plugins.add(plugin); + program.setFile('source/main.bs', ` + sub main() + end sub + `); + program.validate(); + const srcPath = s`${rootDir}/source/main.bs`; + program.getCodeActions(srcPath, Range.create(0, 0, 0, 0)); + program.getSourceFixAllCodeActions(srcPath); + program.getSemanticTokens(srcPath); + + expect(plugin.beforeGetCodeActions.callCount).to.equal(1); + expect(plugin.onGetCodeActions.callCount).to.equal(1); + expect(plugin.afterGetCodeActions.callCount).to.equal(1); + + expect(plugin.beforeGetSourceFixAllCodeActions.callCount).to.equal(1); + expect(plugin.onGetSourceFixAllCodeActions.callCount).to.equal(1); + expect(plugin.afterGetSourceFixAllCodeActions.callCount).to.equal(1); + + expect(plugin.beforeGetSemanticTokens.callCount).to.equal(1); + expect(plugin.onGetSemanticTokens.callCount).to.equal(1); + expect(plugin.afterGetSemanticTokens.callCount).to.equal(1); }); }); diff --git a/src/Program.ts b/src/Program.ts index b687c9c84..fb653aceb 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -10,7 +10,7 @@ import { SymbolTable } from './SymbolTable'; import { DiagnosticMessages } from './DiagnosticMessages'; import { BrsFile } from './files/BrsFile'; import { XmlFile } from './files/XmlFile'; -import type { BsDiagnostic, File, FileReference, FileObj, BscFile, SemanticToken, AfterFileTranspileEvent, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, ProvideSelectionRangesEvent, OnGetSourceFixAllCodeActionsEvent } from './interfaces'; +import type { BsDiagnostic, File, FileReference, FileObj, BscFile, SemanticToken, AfterFileTranspileEvent, BeforeFileTranspileEvent, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, ProvideSelectionRangesEvent, OnGetCodeActionsEvent, OnGetSourceFixAllCodeActionsEvent, OnGetSemanticTokensEvent } from './interfaces'; import type { SourceFixAllCodeAction } from './CodeActionUtil'; import { codeActionUtil } from './CodeActionUtil'; import { standardizePath as s, util } from './util'; @@ -572,6 +572,7 @@ export class Program { source: fileContents }; this.plugins.emit('beforeFileParse', sourceObj); + this.plugins.emit('onFileParse', sourceObj); this.logger.time(LogLevel.debug, ['parse', chalk.green(srcPath)], () => { brsFile.parse(sourceObj.source); @@ -602,6 +603,7 @@ export class Program { source: fileContents }; this.plugins.emit('beforeFileParse', sourceObj); + this.plugins.emit('onFileParse', sourceObj); this.logger.time(LogLevel.debug, ['parse', chalk.green(srcPath)], () => { xmlFile.parse(sourceObj.source); @@ -614,12 +616,14 @@ export class Program { //create a new scope for this xml file let scope = new XmlScope(xmlFile, this); + this.plugins.emit('beforeScopeCreate', scope); this.addScope(scope); //register this compoent now that we have parsed it and know its component name this.registerComponent(xmlFile, scope); //notify plugins that the scope is created and the component is registered + this.plugins.emit('onScopeCreate', scope); this.plugins.emit('afterScopeCreate', scope); } else { //TODO do we actually need to implement this? Figure out how to handle img paths @@ -706,8 +710,10 @@ export class Program { public createSourceScope() { if (!this.scopes.source) { const sourceScope = new Scope('source', this, 'scope:source'); + this.plugins.emit('beforeScopeCreate', sourceScope); sourceScope.attachDependencyGraph(this.dependencyGraph); this.addScope(sourceScope); + this.plugins.emit('onScopeCreate', sourceScope); this.plugins.emit('afterScopeCreate', sourceScope); } } @@ -779,6 +785,7 @@ export class Program { let scope = this.scopes[file.pkgPath]; if (scope) { this.plugins.emit('beforeScopeDispose', scope); + this.plugins.emit('onScopeDispose', scope); scope.dispose(); //notify dependencies of this scope that it has been removed this.dependencyGraph.remove(scope.dependencyGraphKey!); @@ -800,6 +807,7 @@ export class Program { this.unregisterComponent(file); } //dispose file + this.plugins.emit('onFileDispose', file); file?.dispose(); this.plugins.emit('afterFileDispose', file); } @@ -872,6 +880,7 @@ export class Program { .once(() => { this.diagnostics = []; this.plugins.emit('beforeProgramValidate', this); + this.plugins.emit('onProgramValidate', this); beforeProgramValidateWasEmitted = true; }) .forEach(() => Object.values(this.files), (file) => { @@ -1249,15 +1258,18 @@ export class Program { .filter(x => util.rangesIntersectOrTouch(x.range, range)); const scopes = this.getScopesForFile(file); - - this.plugins.emit('onGetCodeActions', { + const event: OnGetCodeActionsEvent = { program: this, file: file, range: range, diagnostics: diagnostics, scopes: scopes, codeActions: codeActions - }); + }; + this.plugins.emit('beforeGetCodeActions', event); + + this.plugins.emit('onGetCodeActions', event); + this.plugins.emit('afterGetCodeActions', event); } return codeActions; } @@ -1275,13 +1287,16 @@ export class Program { .getDiagnostics() .filter(x => x.file === file); const scopes = this.getScopesForFile(file); - this.plugins.emit('onGetSourceFixAllCodeActions', { + const event = { program: this, file: file, diagnostics: diagnostics, scopes: scopes, actions: actions - } as OnGetSourceFixAllCodeActionsEvent); + } as OnGetSourceFixAllCodeActionsEvent; + this.plugins.emit('beforeGetSourceFixAllCodeActions', event); + this.plugins.emit('onGetSourceFixAllCodeActions', event); + this.plugins.emit('afterGetSourceFixAllCodeActions', event); } return actions.map(action => codeActionUtil.createCodeAction({ ...action, @@ -1296,12 +1311,15 @@ export class Program { const file = this.getFile(srcPath); if (file) { const result = [] as SemanticToken[]; - this.plugins.emit('onGetSemanticTokens', { + const event = { program: this, file: file, scopes: this.getScopesForFile(file), semanticTokens: result - }); + } as OnGetSemanticTokensEvent; + this.plugins.emit('beforeGetSemanticTokens', event); + this.plugins.emit('onGetSemanticTokens', event); + this.plugins.emit('afterGetSemanticTokens', event); return result; } } @@ -1422,12 +1440,14 @@ export class Program { */ private _getTranspiledFileContents(file: BscFile, outputPath?: string): FileTranspileResult { const editor = new AstEditor(); - this.plugins.emit('beforeFileTranspile', { + const beforeEvent = { program: this, file: file, outputPath: outputPath, editor: editor - }); + } as BeforeFileTranspileEvent; + this.plugins.emit('beforeFileTranspile', beforeEvent); + this.plugins.emit('onFileTranspile', beforeEvent); //if we have any edits, assume the file needs to be transpiled if (editor.hasChanges) { @@ -1522,6 +1542,7 @@ export class Program { const astEditor = new AstEditor(); this.plugins.emit('beforeProgramTranspile', this, entries, astEditor); + this.plugins.emit('onProgramTranspile', this, entries, astEditor); return { entries: entries, getOutputPath: getOutputPath, @@ -1791,7 +1812,9 @@ export class Program { } public dispose() { - this.plugins.emit('beforeProgramDispose', { program: this }); + const event = { program: this }; + this.plugins.emit('beforeProgramDispose', event); + this.plugins.emit('onProgramDispose', event); for (let filePath in this.files) { this.files[filePath].dispose(); @@ -1801,6 +1824,7 @@ export class Program { } this.globalScope.dispose(); this.dependencyGraph.dispose(); + this.plugins.emit('afterProgramDispose', event); } } diff --git a/src/ProgramBuilder.spec.ts b/src/ProgramBuilder.spec.ts index 2382481a1..7bc96e268 100644 --- a/src/ProgramBuilder.spec.ts +++ b/src/ProgramBuilder.spec.ts @@ -41,16 +41,19 @@ describe('ProgramBuilder', () => { builder.dispose(); }); - it('includes .program in the afterProgramCreate event', async () => { + it('includes .program in the onProgramCreate and afterProgramCreate events', async () => { builder = new ProgramBuilder(); const deferred = new Deferred(); + const onProgramCreate = sinon.spy(); builder.plugins.add({ name: 'test', + onProgramCreate: onProgramCreate, afterProgramCreate: () => { deferred.resolve(builder.program); } }); builder['createProgram'](); + expect(onProgramCreate.callCount).to.equal(1); expect( await deferred.promise ).to.exist; diff --git a/src/ProgramBuilder.ts b/src/ProgramBuilder.ts index f459fac08..3fa6959db 100644 --- a/src/ProgramBuilder.ts +++ b/src/ProgramBuilder.ts @@ -169,6 +169,7 @@ export class ProgramBuilder { protected createProgram() { this.program = new Program(this.options, this.logger, this.plugins); + this.plugins.emit('onProgramCreate', this.program); this.plugins.emit('afterProgramCreate', this.program); return this.program; @@ -491,6 +492,7 @@ export class ProgramBuilder { } this.plugins.emit('beforePrepublish', this, filteredFileMap); + this.plugins.emit('onPrepublish', this, filteredFileMap); await this.logger.time(LogLevel.log, ['Copying to staging directory'], async () => { //prepublish all non-program-loaded files to staging @@ -502,6 +504,7 @@ export class ProgramBuilder { this.plugins.emit('afterPrepublish', this, filteredFileMap); this.plugins.emit('beforePublish', this, fileMap); + this.plugins.emit('onPublish', this, fileMap); await this.logger.time(LogLevel.log, ['Transpiling'], async () => { //transpile any brighterscript files diff --git a/src/interfaces.ts b/src/interfaces.ts index c629aa700..ee5753f71 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -222,17 +222,26 @@ export interface Plugin { name: string; //program events beforeProgramCreate?: (builder: ProgramBuilder) => void; + onProgramCreate?: (program: Program) => void; + afterProgramCreate?: (program: Program) => void; beforePrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void; + onPrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void; afterPrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void; beforePublish?: (builder: ProgramBuilder, files: FileObj[]) => void; + onPublish?: (builder: ProgramBuilder, files: FileObj[]) => void; afterPublish?: (builder: ProgramBuilder, files: FileObj[]) => void; - afterProgramCreate?: (program: Program) => void; beforeProgramValidate?: (program: Program) => void; + onProgramValidate?: (program: Program) => void; afterProgramValidate?: (program: Program, wasCancelled: boolean) => void; beforeProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; + onProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; afterProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; beforeProgramDispose?: PluginHandler; + onProgramDispose?: PluginHandler; + afterProgramDispose?: PluginHandler; + beforeGetCodeActions?: PluginHandler; onGetCodeActions?: PluginHandler; + afterGetCodeActions?: PluginHandler; /** * Emitted when VS Code requests "source fix all" source actions for a file. * Plugins push one or more `SourceFixAllCodeAction` objects onto `event.actions`, @@ -240,7 +249,9 @@ export interface Plugin { * Plugins are responsible for assembling and merging all changes within each action. */ // For possible future use, but not currently implemented: + beforeGetSourceFixAllCodeActions?: PluginHandler; onGetSourceFixAllCodeActions?: PluginHandler; + afterGetSourceFixAllCodeActions?: PluginHandler; /** * Emitted before the program starts collecting completions @@ -345,17 +356,22 @@ export interface Plugin { */ afterProvideSelectionRanges?(event: AfterProvideSelectionRangesEvent): any; - + beforeGetSemanticTokens?: PluginHandler; onGetSemanticTokens?: PluginHandler; + afterGetSemanticTokens?: PluginHandler; //scope events + beforeScopeCreate?: (scope: Scope) => void; + onScopeCreate?: (scope: Scope) => void; afterScopeCreate?: (scope: Scope) => void; beforeScopeDispose?: (scope: Scope) => void; + onScopeDispose?: (scope: Scope) => void; afterScopeDispose?: (scope: Scope) => void; beforeScopeValidate?: ValidateHandler; onScopeValidate?: PluginHandler; afterScopeValidate?: ValidateHandler; //file events beforeFileParse?: (source: SourceObj) => void; + onFileParse?: (source: SourceObj) => void; afterFileParse?: (file: BscFile) => void; /** * Called before each file is validated @@ -370,8 +386,10 @@ export interface Plugin { */ afterFileValidate?: (file: BscFile) => void; beforeFileTranspile?: PluginHandler; + onFileTranspile?: PluginHandler; afterFileTranspile?: PluginHandler; beforeFileDispose?: (file: BscFile) => void; + onFileDispose?: (file: BscFile) => void; afterFileDispose?: (file: BscFile) => void; } export type PluginHandler = (event: T) => R; @@ -397,6 +415,10 @@ export interface OnGetSourceFixAllCodeActionsEvent { */ actions: SourceFixAllCodeAction[]; } +export type BeforeGetCodeActionsEvent = OnGetCodeActionsEvent; +export type AfterGetCodeActionsEvent = OnGetCodeActionsEvent; +export type BeforeGetSourceFixAllCodeActionsEvent = OnGetSourceFixAllCodeActionsEvent; +export type AfterGetSourceFixAllCodeActionsEvent = OnGetSourceFixAllCodeActionsEvent; export interface ProvideCompletionsEvent { program: Program; @@ -534,6 +556,8 @@ export interface OnGetSemanticTokensEvent { */ semanticTokens: SemanticToken[]; } +export type BeforeGetSemanticTokensEvent = OnGetSemanticTokensEvent; +export type AfterGetSemanticTokensEvent = OnGetSemanticTokensEvent; export interface BeforeFileValidateEvent { program: Program; From 176f617c3bca56b3e6ddcc4a069e5410bea4db98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 16:27:20 +0000 Subject: [PATCH 3/5] chore: refine lifecycle event typing names Agent-Logs-Url: https://github.com/rokucommunity/brighterscript/sessions/a7fc302f-ff61-4651-b61a-5bd4306ce21f Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com> --- docs/plugins.md | 6 +++--- src/Program.ts | 2 +- src/interfaces.ts | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index d3a50bf32..68d3609f3 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -196,8 +196,8 @@ export interface CompilerPlugin { onProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; afterProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; beforeProgramDispose?: PluginHandler; - onProgramDispose?: PluginHandler; - afterProgramDispose?: PluginHandler; + onProgramDispose?: PluginHandler; + afterProgramDispose?: PluginHandler; beforeGetCodeActions?: PluginHandler; onGetCodeActions?: PluginHandler; afterGetCodeActions?: PluginHandler; @@ -322,7 +322,7 @@ export interface CompilerPlugin { */ afterFileValidate?: (file: BscFile) => void; beforeFileTranspile?: PluginHandler; - onFileTranspile?: PluginHandler; + onFileTranspile?: PluginHandler; afterFileTranspile?: PluginHandler; beforeFileDispose?: (file: BscFile) => void; onFileDispose?: (file: BscFile) => void; diff --git a/src/Program.ts b/src/Program.ts index fb653aceb..0f2c22956 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -619,7 +619,7 @@ export class Program { this.plugins.emit('beforeScopeCreate', scope); this.addScope(scope); - //register this compoent now that we have parsed it and know its component name + //register this component now that we have parsed it and know its component name this.registerComponent(xmlFile, scope); //notify plugins that the scope is created and the component is registered diff --git a/src/interfaces.ts b/src/interfaces.ts index ee5753f71..de47b3ac6 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -237,8 +237,8 @@ export interface Plugin { onProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; afterProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; beforeProgramDispose?: PluginHandler; - onProgramDispose?: PluginHandler; - afterProgramDispose?: PluginHandler; + onProgramDispose?: PluginHandler; + afterProgramDispose?: PluginHandler; beforeGetCodeActions?: PluginHandler; onGetCodeActions?: PluginHandler; afterGetCodeActions?: PluginHandler; @@ -386,7 +386,7 @@ export interface Plugin { */ afterFileValidate?: (file: BscFile) => void; beforeFileTranspile?: PluginHandler; - onFileTranspile?: PluginHandler; + onFileTranspile?: PluginHandler; afterFileTranspile?: PluginHandler; beforeFileDispose?: (file: BscFile) => void; onFileDispose?: (file: BscFile) => void; @@ -587,6 +587,7 @@ export interface BeforeFileTranspileEvent { */ editor: Editor; } +export type OnFileTranspileEvent = BeforeFileTranspileEvent; export interface AfterFileTranspileEvent { /** @@ -618,6 +619,8 @@ export interface AfterFileTranspileEvent { export interface BeforeProgramDisposeEvent { program: Program; } +export type OnProgramDisposeEvent = BeforeProgramDisposeEvent; +export type AfterProgramDisposeEvent = BeforeProgramDisposeEvent; export interface SemanticToken { range: Range; From 6a399548312f69978e3d7f4d360c01d58075dcd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 16:29:54 +0000 Subject: [PATCH 4/5] docs: clarify onProgramCreate lifecycle semantics Agent-Logs-Url: https://github.com/rokucommunity/brighterscript/sessions/a7fc302f-ff61-4651-b61a-5bd4306ce21f Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com> --- src/Program.spec.ts | 2 +- src/interfaces.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Program.spec.ts b/src/Program.spec.ts index 80a5110e8..58e352403 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -3694,7 +3694,7 @@ describe('Program', () => { expect(plugin.afterFileValidate.callCount).to.equal(1); }); - it('emits file and scope dispose events', () => { + it('emits xml file and component scope dispose events', () => { const plugin = { name: 'test', beforeFileDispose: sinon.spy(), diff --git a/src/interfaces.ts b/src/interfaces.ts index de47b3ac6..dc663368b 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -221,8 +221,17 @@ export type CompilerPlugin = Plugin; export interface Plugin { name: string; //program events + /** + * Called before the Program instance is created. + */ beforeProgramCreate?: (builder: ProgramBuilder) => void; + /** + * Called after the Program instance is created but before afterProgramCreate. + */ onProgramCreate?: (program: Program) => void; + /** + * Called after the Program instance has been created. + */ afterProgramCreate?: (program: Program) => void; beforePrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void; onPrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void; From f0274b95e7eda8fb9369f6c4cb79b5117a984e5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 16:32:24 +0000 Subject: [PATCH 5/5] test: add brs dispose event coverage Agent-Logs-Url: https://github.com/rokucommunity/brighterscript/sessions/a7fc302f-ff61-4651-b61a-5bd4306ce21f Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com> --- src/Program.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Program.spec.ts b/src/Program.spec.ts index 58e352403..23dfd1eb8 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -3717,6 +3717,24 @@ describe('Program', () => { expect(plugin.afterScopeDispose.callCount).to.equal(1); }); + it('emits brs file dispose events', () => { + const plugin = { + name: 'test', + beforeFileDispose: sinon.spy(), + onFileDispose: sinon.spy(), + afterFileDispose: sinon.spy() + }; + program.plugins.add(plugin); + program.setFile('source/main.brs', ` + sub main() + end sub + `); + program.removeFile('source/main.brs'); + expect(plugin.beforeFileDispose.callCount).to.equal(1); + expect(plugin.onFileDispose.callCount).to.equal(1); + expect(plugin.afterFileDispose.callCount).to.equal(1); + }); + it('emits program dispose events', () => { const plugin = { name: 'test',