Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugins Must Declare Annotations [WIP] #1386

Draft
wants to merge 5 commits into
base: release-1.0.0
Choose a base branch
from
Draft
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`
Comment on lines 77 to +85
Copy link
Collaborator

Choose a reason for hiding this comment

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

The remaining API methods are all named with a specific order of "time" > "subject" > "action", i.e. beforeProgramCreate, afterFileValidate, etc. But these either disregard the "subject" entirely, or have their "subject" and "action" reversed.

Also, not sure I understand the difference between the program's serialize and build steps, and the need for the beforePrepublish and afterPrepublish steps. Is there a need for all of these?

- `afterProgramCreate`
    - (...)
    - `afterProgramValidate`
- `beforeProgramPublish`
    - For each file:
        - `beforeFilePrepare`
        - `afterFilePrepare`
- `afterProgramPublish`
- `beforeProgramDispose`

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 agree the naming should be more consistent.

@TwitchBronBron can we make this change?
also, maybe you could add a little more to these docs to explain what these events represent?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I'm definitely open to tweaking some of these names so they're more consistent.

With regard to the prepublish, serialize, build steps, they each have their own purpose, but we should definitely add more documentation around them to better explain their differences.

- `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`
Comment on lines +101 to +102
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above:

- `beforeFileProvide`
- `afterFileProvide`

Copy link
Member

Choose a reason for hiding this comment

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

Here, file isn't the subject—we're asking plugins to provide a file, not the files to provide something. So the term subject is a bit different, though I can see it being read slightly that way.

But perhaps it doesn't matter, and we should just align on the naming convention anyway?

- `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 = [
'inline()',
'log(prefix as string, addLineNumbers = false as boolean)'
];
```

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