Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
49 changes: 26 additions & 23 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -633,35 +633,38 @@ 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 { Plugin, BeforePrepareFileEvent, isBrsFile, WalkMode, createVisitor, TokenKind, AstNode, EmptyStatement } from 'brighterscript';

export default function plugin() {
export default function (): Plugin {
return {
name: 'removeCommentAndPrintStatements',
beforeFileTranspile: (event: BeforeFileTranspileEvent) => {
if (isBrsFile(event.file)) {
// visit functions bodies
event.file.ast.walk(createVisitor({
PrintStatement: (statement) => {
//replace `PrintStatement` transpilation with empty string
event.editor.overrideTranspileResult(statement, '');
},
AstNode: (node: AstNode, _parent, owner, key) => {
const trivia = node.getLeadingTrivia();
for(let i = 0; i < trivia.length; i++) {
let triviaItem = trivia[i].
if (triviaItem.kind === TokenKind.Comment) {
//remove comment tokens
event.editor.removeProperty(trivia, i);
}
beforePrepareFile: (event: BeforePrepareFileEvent) => {
if (!isBrsFile(event.file)) {
return;
}
//delete comments from the EOF token
event.editor.arraySplice(event.file.parser.eofToken.leadingTrivia, 0, Number.MAX_SAFE_INTEGER);

event.file.ast.walk(createVisitor({
PrintStatement: (node: AstNode, _parent, owner, key) => {
return new EmptyStatement();
},
AstNode: (node: AstNode, _parent, owner, key) => {
const trivia = node.leadingTrivia;
for (let i = 0; i < trivia.length; i++) {
if (trivia[i]?.kind === TokenKind.Comment) {
//remove this token
event.editor.arraySplice(trivia, i, 1);
//replay this index again for the new item that took our spot since we deleted from the array
i--;
}
}
}), {
walkMode: WalkMode.visitStatements | WalkMode.visitComments
});
}
}
}), {
walkMode: WalkMode.visitAllRecursive
});
}
} as CompilerPlugin;
};
}
```

Expand Down
7 changes: 5 additions & 2 deletions src/PluginInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default class PluginInterface<T extends Plugin = Plugin> {
/**
* Call `event` on plugins, but allow the plugins to return promises that will be awaited before the next plugin is notified
*/
public async emitAsync<K extends keyof PluginEventArgs<T> & string>(event: K, ...args: PluginEventArgs<T>[K]): Promise< PluginEventArgs<T>[K][0]> {
public async emitAsync<K extends keyof PluginEventArgs<T> & string>(event: K, ...args: PluginEventArgs<T>[K]): Promise<PluginEventArgs<T>[K][0]> {
this.logger.debug(`Emitting async plugin event: ${event}`);
for (let plugin of this.plugins) {
if ((plugin as any)[event]) {
Expand All @@ -87,7 +87,10 @@ export default class PluginInterface<T extends Plugin = Plugin> {
);
});
} catch (err) {
this.logger?.error(`Error when calling plugin ${plugin.name}.${event}:`, err);
this.logger?.error(`Error when calling plugin ${plugin.name}.${event}:`, (err as Error).stack);
if (!this.suppressErrors) {
throw err;
}
}
}
}
Expand Down
47 changes: 46 additions & 1 deletion src/files/BrsFile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import * as fileUrl from 'file-url';
import type { AALiteralExpression } from '../parser/Expression';
import { CallExpression, FunctionExpression, LiteralExpression } from '../parser/Expression';
import { Logger } from '@rokucommunity/logger';
import { isFunctionExpression, isAALiteralExpression, isBlock } from '../astUtils/reflection';
import { isFunctionExpression, isAALiteralExpression, isBlock, isBrsFile } from '../astUtils/reflection';
import { createVisitor, WalkMode } from '../astUtils/visitors';

let sinon = sinonImport.createSandbox();

Expand Down Expand Up @@ -3076,6 +3077,50 @@ describe('BrsFile', () => {
`, 'trim', 'source/main.bs');
});

it('does not crash on undefined trivia', async () => {
//plugin that mangles trivia collections
program.plugins.add({
name: 'test-plugin',
prepareFile: (event) => {
if (isBrsFile(event.file)) {
//delete trivia from the eof token
event.file.parser.eofToken.leadingTrivia = [];
event.file.ast.walk(createVisitor({
AstNode: (node) => {
//delete all trivia from the node
for (let i = 0; i < (node.leadingTrivia.length ?? 0); i++) {
delete node.leadingTrivia[i];
}
for (let i = 0; i < (node.endTrivia.length ?? 0); i++) {
delete node.endTrivia[i];
}
}
}), {
walkMode: WalkMode.visitAllRecursive
});
console.log('trivia removed');
}
}
});

//ensure plugin errors are not suppressed
program.plugins['suppressErrors'] = false;

await testTranspile(`
'comment 1
sub main() 'comment 2
'comment 3
'comment 4
print "main" 'comment 5
end sub 'comment 6
'comment 7
`, `
sub main()
print "main"
end sub
`, undefined, 'source/main.bs');
});

it('includes annotation comments for class', async () => {
await testTranspile(`
'comment1
Expand Down
4 changes: 2 additions & 2 deletions src/parser/Statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -921,14 +921,14 @@ export class PrintStatement extends Statement {
] as TranspileResult;

//if the first expression has no leading whitespace, add a single space between the `print` and the expression
if (this.expressions.length > 0 && !this.expressions[0].leadingTrivia.find(t => t.kind === TokenKind.Whitespace)) {
if (this.expressions.length > 0 && !this.expressions[0].leadingTrivia.find(t => t?.kind === TokenKind.Whitespace)) {
result.push(' ');
}

// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < this.expressions.length; i++) {
const expression = this.expressions[i];
let leadingWhitespace = expression.leadingTrivia.find(t => t.kind === TokenKind.Whitespace)?.text;
let leadingWhitespace = expression.leadingTrivia.find(t => t?.kind === TokenKind.Whitespace)?.text;
if (leadingWhitespace) {
result.push(leadingWhitespace);
//if the previous expression was NOT a separator, and this one is not also, add a space between them
Expand Down
48 changes: 48 additions & 0 deletions src/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1604,4 +1604,52 @@ describe('util', () => {
util.isClassUsedAsFunction(new ClassType(undefined), undefined, { flags: SymbolTypeFlag.runtime });
});
});

describe('hasLeadingComments', () => {
it('does not crash on undefined trivia', () => {
expect(util.hasLeadingComments(undefined)).to.be.false;
});

it('returns false when there are no leading comments', () => {
const token = createToken(TokenKind.Identifier);
token.leadingTrivia = [];
expect(util.hasLeadingComments(token)).to.be.false;
});

it('returns true when there are leading comments', () => {
const token = createToken(TokenKind.Identifier);
token.leadingTrivia = [createToken(TokenKind.Comment, `'comment`)];
expect(util.hasLeadingComments(token)).to.be.true;
});

it('does not crash on unexpected trivia item types', () => {
const token = createToken(TokenKind.Identifier);
token.leadingTrivia = [undefined, null, 1, true, 'string', {}, createToken(TokenKind.Comment, `'comment`)] as any[];
expect(util.hasLeadingComments(token)).to.be.true;
});
});

describe('getLeadingComments', () => {
it('does not crash on undefined trivia', () => {
expect(util.getLeadingComments(undefined)).to.eql([]);
});

it('returns [] when there are no leading comments', () => {
const token = createToken(TokenKind.Identifier);
token.leadingTrivia = [];
expect(util.getLeadingComments(token)).to.eql([]);
});

it('returns true when there are leading comments', () => {
const token = createToken(TokenKind.Identifier);
token.leadingTrivia = [createToken(TokenKind.Comment, `'comment 1`)];
expect(util.getLeadingComments(token)).eql([createToken(TokenKind.Comment, `'comment 1`)]);
});

it('does not crash on unexpected trivia item types', () => {
const token = createToken(TokenKind.Identifier);
token.leadingTrivia = [undefined, null, 1, true, 'string', {}, createToken(TokenKind.Comment, `'comment 2`)] as any[];
expect(util.getLeadingComments(token)).eql([createToken(TokenKind.Comment, `'comment 2`)]);
});
});
});
4 changes: 2 additions & 2 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2683,12 +2683,12 @@ export class Util {

public hasLeadingComments(input: Token | AstNode) {
const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
return !!leadingTrivia.find(t => t.kind === TokenKind.Comment);
return !!leadingTrivia.find(t => t?.kind === TokenKind.Comment);
}

public getLeadingComments(input: Token | AstNode) {
const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
return leadingTrivia.filter(t => t.kind === TokenKind.Comment);
return leadingTrivia.filter(t => t?.kind === TokenKind.Comment);
}

public isLeadingCommentOnSameLine(line: RangeLike, input: Token | AstNode) {
Expand Down
Loading