Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
110 changes: 90 additions & 20 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ While there are no restrictions on plugin names, it helps others to find your pl

Full compiler lifecycle:

- `onPluginConfigure`
- `beforeProgramCreate`
- `afterProgramCreate`
- `afterScopeCreate` ("source" scope)
Expand All @@ -75,13 +76,13 @@ Full compiler lifecycle:
- `afterProgramValidate`
- `beforePrepublish`
- `afterPrepublish`
- `beforePublish`
- `beforeProgramTranspile`
- `beforeSerializeProgram`
- `beforeBuildProgram`
- For each file:
- `beforeFileTranspile`
- `afterFileTranspile`
- `afterProgramTranspile`
- `afterPublish`
- `beforePrepareFile`
- `afterPrepareFile`
- `afterBuildProgram`
- `afterSerializeProgram`
- `beforeProgramDispose`

### Language server
Expand All @@ -90,15 +91,15 @@ Once the program has been validated, the language server runs a special loop - i

When a file is removed:

- `beforeFileDispose`
- `beforeFileRemove`
- `beforeScopeDispose` (component scope)
- `afterScopeDispose` (component scope)
- `afterFileDispose`
- `afterFileRemove`

When a file is added:

- `beforeFileParse`
- `afterFileParse`
- `beforeProvideFile`
- `afterProvideFile`
- `afterScopeCreate` (component scope)
- `afterFileValidate`

Expand Down Expand Up @@ -157,10 +158,25 @@ The top level object is the `ProgramBuilder` which runs the overall process: pre
Here are some important interfaces. You can view them in the code at [this link](https://github.com/rokucommunity/brighterscript/blob/ddcb7b2cd219bd9fecec93d52fbbe7f9b972816b/src/interfaces.ts#L190:~:text=export%20interface%20CompilerPlugin%20%7B).

```typescript
export type CompilerPluginFactory = () => CompilierPlugin;
export type CompilerPluginFactory = () => CompilerPlugin;

export interface CompilerPlugin {
name: string;

/**
* A list of brighterscript-style function declarations of allowed annotations
* Eg.: [
* `inline()`,
* `suite(suiteConfig as object)`
* ]
*/
annotations?: string[];

/**
* Called when plugin is initially loaded
*/
onPluginConfigure?(event: onPluginConfigureEvent): any;

/**
* Called before a new program is created
*/
Expand Down Expand Up @@ -240,7 +256,6 @@ export interface CompilerPlugin {
afterScopeDispose?(event: AfterScopeDisposeEvent): any;

beforeScopeValidate?(event: BeforeScopeValidateEvent): any;

/**
* Called before the `provideDefinition` hook
*/
Expand All @@ -256,7 +271,6 @@ export interface CompilerPlugin {
*/
afterProvideDefinition?(event: AfterProvideDefinitionEvent): any;


/**
* Called before the `provideReferences` hook
*/
Expand Down Expand Up @@ -304,8 +318,6 @@ export interface CompilerPlugin {
*/
afterProvideWorkspaceSymbols?(event: AfterProvideWorkspaceSymbolsEvent): any;


onGetSemanticTokens?: PluginHandler<OnGetSemanticTokensEvent>;
//scope events
onScopeValidate?(event: OnScopeValidateEvent): any;
afterScopeValidate?(event: BeforeScopeValidateEvent): any;
Expand Down Expand Up @@ -554,7 +566,7 @@ export default function () {
## Modifying code
Sometimes plugins will want to modify code before the project is transpiled. While you can technically edit the AST directly at any point in the file's lifecycle, this is not recommended as those changes will remain changed as long as that file exists in memory and could cause issues with file validation if the plugin is used in a language-server context (i.e. inside vscode).

Instead, we provide an instace of an `Editor` class in the `beforeFileTranspile` event that allows you to modify AST before the file is transpiled, and then those modifications are undone `afterFileTranspile`.
Instead, we provide an instance of an `Editor` class in the `beforeBuildProgram` and `beforePrepareFile` events that allows you to modify AST before the file is transpiled, and then those modifications are undone after the `afterBuildProgram` event.

For example, consider the following brightscript code:
```brightscript
Expand All @@ -566,14 +578,14 @@ end sub
Here's the plugin:

```typescript
import { CompilerPlugin, BeforeFileTranspileEvent, isBrsFile, WalkMode, createVisitor, TokenKind } from 'brighterscript';
import { CompilerPlugin, BeforePrepareFileEvent, isBrsFile, WalkMode, createVisitor, TokenKind } from 'brighterscript';

// plugin factory
export default function () {
return {
name: 'replacePlaceholders',
// transform AST before transpilation
beforeFileTranspile: (event: BeforeFileTranspileEvent) => {
beforePrepareFile: (event: BeforePrepareFileEvent) => {
if (isBrsFile(event.file)) {
event.file.ast.walk(createVisitor({
LiteralExpression: (literal) => {
Expand All @@ -600,12 +612,12 @@ Another common use case is to remove print statements and comments. Here's a plu
Note: Comments are not regular nodes in the AST. They're considered "trivia". To access them, you need to ask each AstNode for its trivia. to help with this, we've included the `AstNode` visitor method. Here's how you'd do that:

```typescript
import { isBrsFile, createVisitor, WalkMode, BeforeFileTranspileEvent, CompilerPlugin } from 'brighterscript';
import { isBrsFile, createVisitor, WalkMode, BeforePrepareFileEvent, CompilerPlugin } from 'brighterscript';

export default function plugin() {
return {
name: 'removeCommentAndPrintStatements',
beforeFileTranspile: (event: BeforeFileTranspileEvent) => {
beforePrepareFile: (event: BeforePrepareFileEvent) => {
if (isBrsFile(event.file)) {
// visit functions bodies
event.file.ast.walk(createVisitor({
Expand All @@ -632,6 +644,64 @@ export default function plugin() {
}
```

## Providing Annotations via a plugin

Plugins may provide [annotations](annotations.md) that can be used to add metadata to any statement in the code.

Plugins must declare the annotations they support, so they can be validated properly. To declare an annotation, it must be listed in the `annotations` property - a list of Brighterscript-style function declarations.

For example:

```typescript
this.annotations = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really nice to enforce which statement types this annotation could be declared on. For example, rooibos @suite() only works when attached to classes. Adding @suite() to a namespace should be an error. BrighterScript could provide consistent validations for these. I envision configuring it something like this:

this.annotations.push({
    restrictTo: ['class'], //this would be a list of all types the annotation maybe declared above
    func:  new TypedFunctionType(VoidType.instance).setName('inline'),
        { 
            description: 'Add a log message whenever this function is called',
            type: new TypedFunctionType(VoidType.instance)
                .setName('log')
                .addParameter('prefix', StringType.instance)
                .addParameter('addLineNumbers', BooleanType.instance, true)
        }
    

'inline()',
'log(prefix as string, addLineNumbers = false as boolean)'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already itching to use this for our internal plugins once v1 arrives. I do have a couple of questions, maybe both could be reflected in the docs?

  • Can a plugin author expect the full type system to be supported, or will it only support the built-in types? (I see onlyAllowLiterals: true, so maybe that's a "no"? I'm not familiar with the type system yet)
  • If I have annotation declaration that is implemented by more than one plugin, should a developer assume that the prevalent annotation is based on the order in which the plugins are declared in bsconfig?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation arguments can only be literals - there’s no way of knowing at compile time what the value of a variable is.

That being said, we could support const and enum values as annotation args too. Plan on that in v1.1

so that means, literal strings, integers, floats, literal arrays (with literal values) and literal AA’s can be used as args.

This would be perfectly fine:

@annotation(“test”, {values: [1.1, 4, 7.2], data:{name: “Luis”}}, [{abc: 123}])

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll update the docs to reflect that

];
```

Annotations that do not require any arguments are listed as functions with no parameters. Annotations that require arguments may have their parameters types listed as well.

Here's an example plugin that provides the `log` annotation above:

```typescript
import { isBrsFile, createVisitor, WalkMode, BeforePrepareFileEvent, CompilerPlugin, FunctionStatement, PrintStatement, createStringLiteral, VariableExpression, createToken, TokenKind, Identifier } from 'brighterscript';

export default function plugin() {
return {
name: 'addLogging',
annotations: [
'log(prefix as string, addLineNumbers = false as boolean)'
],
beforePrepareFile: (event: BeforePrepareFileEvent) => {
if (isBrsFile(event.file)) {
event.file.ast.walk(createVisitor({
FunctionStatement: (funcStmt: FunctionStatement, _parent, owner, key) => {
const logAnnotation = funcStmt.annotations?.find(anno => anno.name === 'log');
if (logAnnotation) {
const args = logAnnotation.getArguments();
const logPrintStmt = new PrintStatement({
print: createToken(TokenKind.Print),
expressions:[
createStringLiteral(args[0].toString()), // prefix,
createStringLiteral(funcStmt.tokens.name.text) // function name
]
});
if(args[1]) { // add line num
logPrintStmt.expressions.unshift(new VariableExpression({ name: createToken(TokenKind.SourceLineNumLiteral) as Identifier }))
}
event.editor.arrayUnshift(funcStmt.func.body.statements, logPrintStmt)
}
}
}), {
walkMode: WalkMode.visitStatements
});
}
}
} as CompilerPlugin;
}
```


## Modifying `bsconfig.json` via a plugin

In some cases you may want to modify the project's configuration via a plugin, such as to change settings based on environment variables or to dynamically modify the project's `files` array. Plugins may do so in the `beforeProgramCreate` step. For example, here's a plugin which adds an additional file to the build:
Expand Down
3 changes: 3 additions & 0 deletions src/ProgramBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ export class ProgramBuilder {
for (let plugin of plugins) {
this.plugins.add(plugin);
}
this.plugins.emit('onPluginConfigure', {
builder: this
});

this.plugins.emit('beforeProgramCreate', {
builder: this
Expand Down
18 changes: 18 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,21 @@ export type CompilerPluginFactory = () => CompilerPlugin;

export interface CompilerPlugin {
name: string;

/**
* A list of brighterscript-style function declarations of allowed annotations
* Eg.: [
* `inline()`,
* `suite(suiteConfig as object)`
* ]
*/
annotations?: string[];

/**
* Called when plugin is initially loaded
*/
onPluginConfigure?(event: onPluginConfigureEvent): any;

/**
* Called before a new program is created
*/
Expand Down Expand Up @@ -506,6 +521,9 @@ export interface OnGetCodeActionsEvent<TFile extends BscFile = BscFile> {
codeActions: CodeAction[];
}

export interface onPluginConfigureEvent {
builder: ProgramBuilder;
}
export interface BeforeProgramCreateEvent {
builder: ProgramBuilder;
}
Expand Down
Loading