Skip to content

Commit

Permalink
PLugin logic
Browse files Browse the repository at this point in the history
  • Loading branch information
iObject committed Oct 3, 2024
1 parent aed53f4 commit 8362b18
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 78 deletions.
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@
"**/bower_components": true,
"**/dist": true
},
"editor.formatOnSave": true,
"editor.tabSize": 4,
"editor.insertSpaces": true,
"typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": true,
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false,
"files.trimTrailingWhitespace": true,
"typescript.tsdk": "node_modules\\typescript\\lib"
"typescript.tsdk": "node_modules\\typescript\\lib",
"cSpell.words": [
"brighterscript",
"undent"
]
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"rimraf": "^2.7.1",
"source-map-support": "^0.5.13",
"ts-node": "8.9.1",
"typescript": "^4.7.2",
"typescript": "^4.8.3",
"typescript-formatter": "^7.2.2",
"undent": "^0.1.0"
},
Expand Down
76 changes: 76 additions & 0 deletions src/FileTranspilers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { AstEditor, FunctionStatement, BrsFile, BscFile, Identifier, CallExpression, Expression, ReturnStatement, Statement, AssignmentStatement } from 'brighterscript';
import { isBrsFile, Parser, WalkMode, createVisitor, util as bscUtil, isExpressionStatement, GroupingExpression, createToken, TokenKind, isCommentStatement } from 'brighterscript';

import { BrsTranspileState } from 'brighterscript/dist/parser/BrsTranspileState';
import { SourceNode } from 'source-map';

export class FileTranspiler {
public preprocess(file: BscFile, editor: AstEditor, annotatedFunctions: Map<string, FunctionStatement>) {
if (isBrsFile(file)) {
this.walkBscExpressions(file, editor, annotatedFunctions);
}
}
private walkBscExpressions(file: BscFile, editor: AstEditor, annotatedFunctions: Map<string, FunctionStatement>) {
if (isBrsFile(file)) {
file.ast.walk(createVisitor({
CallExpression: (call) => {
const parts = bscUtil.getAllDottedGetParts(call.callee);
return (
this.inlineCall(file, editor, call, parts ?? [], annotatedFunctions)
);
}
}), {
walkMode: WalkMode.visitAllRecursive,
editor: editor
});
}
}

private inlineCall(file: BrsFile, editor: AstEditor, call: CallExpression, parts: Identifier[], annotatedFunctions: Map<string, FunctionStatement>) {
const fullFunctionName = parts?.map(x => x.text).join('.').toLowerCase();
const annotatedFunction = annotatedFunctions.get(fullFunctionName);
if (annotatedFunction) {
const parameterMap = new Map<string, Expression>();

for (let index = 0; index < annotatedFunction.func.parameters.length; index++) {
const annotatedParameter = annotatedFunction.func.parameters[index];
const callArgument = call.args[index] ?? annotatedParameter?.defaultValue;
parameterMap.set(annotatedParameter.name.text, callArgument);
}

const clonedStatement = this.cloneStatement<ReturnStatement>(annotatedFunction.func.body.statements.filter(x => !isCommentStatement(x))[0], file);
clonedStatement.walk(createVisitor({
VariableExpression: (variable, parent?, owner?, key?) => {
const arg = parameterMap.get(variable.name.text);
if (arg) {
return this.cloneExpression(arg, file);
}
}
}), {
walkMode: WalkMode.visitAllRecursive,
editor: editor
});

if (isExpressionStatement(call.parent)) {
return clonedStatement.value;
} else {
return new GroupingExpression({
left: createToken(TokenKind.LeftParen),
right: createToken(TokenKind.RightParen)
}, clonedStatement.value!);
}
}
}

private cloneStatement<T>(statement: Statement, file: BrsFile) {
return statement.clone() as T;
}

private cloneExpression<T = Expression>(expression: Expression, file: BrsFile) {
const newCode = new SourceNode(null, null, null, [
(expression.transpile(new BrsTranspileState(file)) as any)
]).toString();
return (Parser.parse(`__thing = ${newCode}`).ast.statements[0] as AssignmentStatement).value as T;
}
}

40 changes: 40 additions & 0 deletions src/FileValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { BscFile, FunctionStatement } from 'brighterscript';
import { isCommentStatement } from 'brighterscript';
import { isReturnStatement } from 'brighterscript';
import { BsDiagnostic, ParseMode } from 'brighterscript';

Check failure on line 4 in src/FileValidator.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

'BsDiagnostic' is declared but its value is never read.

Check failure on line 4 in src/FileValidator.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

'BsDiagnostic' is declared but its value is never read.
import { AstEditor, createVisitor, isBrsFile, WalkMode } from 'brighterscript';
import { diagnostics } from './diagnostics';

export class FileValidator {
private editor = new AstEditor();
public annotatedFunctions = new Map<string, FunctionStatement>();

public reset() {
this.editor.undoAll();
this.editor = new AstEditor();
this.annotatedFunctions.clear();
}

public findAnnotations(file: BscFile) {
if (isBrsFile(file)) {
file.ast.walk(createVisitor({
FunctionStatement: (statement, parent?, owner?, key?) => {
if (statement.annotations?.find(annotation => annotation.name.toLowerCase() === 'inline')) {
this.annotatedFunctions.set(statement.getName(ParseMode.BrighterScript).toLowerCase(), statement);
const bodyStatements = statement.func.body.statements.filter(x => !isCommentStatement(x));

if (bodyStatements.length !== 1 || !isReturnStatement(bodyStatements[0])) {
file.addDiagnostic(
diagnostics.badAnnotationBody(
bodyStatements[bodyStatements.length - 1]?.range ?? statement.func.range
)
);
}
}
}
}), {
walkMode: WalkMode.visitStatements
});
}
}
}
95 changes: 58 additions & 37 deletions src/Plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Program, standardizePath as s } from 'brighterscript';
import { Program, standardizePath as s, util } from 'brighterscript';
import * as fsExtra from 'fs-extra';
import { Plugin } from './Plugin';
import undent from 'undent';
Expand All @@ -25,26 +25,17 @@ describe('Plugin', () => {
fsExtra.removeSync(tempDir);
});

it('adds a leading print statement to every named function', async () => {
it('Verifies basic annotation support', async () => {
program.setFile('source/main.bs', `
sub main()
m.name = "main"
sub init()
print subtract(5,3)
end sub
function temp()
m.name = "temp"
@inline
function subtract(a, b) as integer
return a - b
end function
`);
program.setFile('components/CustomButton.xml', `
<component name="CustomButton" extends="Button">
<script type="text/brightscript" uri="CustomButton.brs" />
</component>
`);
program.setFile('components/CustomButton.brs', `
sub init()
m.name = "init"
end sub
`);

//make sure there are no diagnostics
program.validate();
Expand All @@ -56,38 +47,68 @@ describe('Plugin', () => {
expect(
(await program.getTranspiledFileContents('source/main.bs')).code
).to.eql(undent`
sub main()
print "hello from main"
m.name = "main"
sub init()
print (5 - 3)
end sub
function temp()
print "hello from temp"
m.name = "temp"
function subtract(a, b) as integer
return a - b
end function
`);
});

it('Verifies inlineable function body must be a single return statement', () => {
program.setFile('source/main.bs', `
sub init()
print subtract(5,3)
end sub

expect(
undent((await program.getTranspiledFileContents('components/CustomButton.xml')).code)
).to.eql(undent`
<component name="CustomButton" extends="Button">
<script type="text/brightscript" uri="CustomButton.brs" />
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
<children>
<label text="Hello from CustomButton" />
</children>
</component>
@inline
function subtract(a, b) as integer
value = a - b
return value
end function
`);

//make sure there are no diagnostics
program.validate();
expect(
(await program.getTranspiledFileContents('components/CustomButton.brs')).code
).to.eql(undent`
program.getDiagnostics().map(x => {
return {
message: x.message,
range: x.range
};
})
).to.eql([{
message: 'Inlineable function body must be a single return statement',
range: util.createRange(8, 16, 8, 28)
}]);
});

it('vvv', () => {
program.setFile('source/main.bs', `
sub init()
print "hello from init"
m.name = "init"
print subtract(5,3)
end sub
@inline
function subtract(a, b) as integer
value = a - b
end function
`);

//make sure there are no diagnostics
program.validate();
expect(
program.getDiagnostics().map(x => {
return {
message: x.message,
range: x.range
};
})
).to.eql([{
message: 'Inlineable function body must be a single return statement',
range: util.createRange(7, 16, 7, 29)
}]);
});
});
55 changes: 17 additions & 38 deletions src/Plugin.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,24 @@
import type { BeforeFileTranspileEvent, CompilerPlugin } from 'brighterscript';
import { Parser, WalkMode, createVisitor, isBrsFile, isXmlFile } from 'brighterscript';
import SGParser from 'brighterscript/dist/parser/SGParser';
import { SGChildren } from 'brighterscript/dist/parser/SGTypes';
import type { AstEditor, CompilerPlugin, Program, TranspileObj } from 'brighterscript';
import { FileValidator } from './FileValidator';
import { FileTranspiler } from './FileTranspilers';

export class Plugin implements CompilerPlugin {
name = 'bsc-plugin-awesome';
name = 'bsc-plugin-inline-annotation';

beforeFileTranspile(event: BeforeFileTranspileEvent) {
if (isBrsFile(event.file)) {
event.file.ast.walk(createVisitor({
FunctionStatement: (functionStatement) => {
const printStatement = Parser.parse(`print "hello from ${functionStatement.name.text}"`).statements[0];
private fileValidator = new FileValidator();
private fileTranspiler = new FileTranspiler();
afterProgramValidate(program: Program) {
this.fileValidator.reset();

//prepend a print statement to the top of every function body
event.editor.arrayUnshift(functionStatement.func.body.statements, printStatement);
}
}), {
walkMode: WalkMode.visitAllRecursive
});

} else if (isXmlFile(event.file)) {
//prepend a label to every xml file (why? just for fun....)
const parser = new SGParser();
parser.parse('generated.xml', `
<component name="Generated">
<children>
<label text="Hello from ${event.file.ast.component!.name}" />
</children>
</component>
`);

const label = parser.ast.component!.children.children[0];
// Get a map of all annotated functions
for (const file of Object.values(program.files)) {
this.fileValidator.findAnnotations(file);
}
}

//ensure the <children> component exists
if (!event.file.ast.component!.children) {
event.file.ast.component!.children = new SGChildren();
}
event.editor.arrayUnshift(
event.file.ast.component!.children.children,
label
);
beforeProgramTranspile(program: Program, entries: TranspileObj[], editor: AstEditor) {
for (const entry of entries) {
this.fileTranspiler.preprocess(entry.file, editor, this.fileValidator.annotatedFunctions);
}
}
}
}
11 changes: 11 additions & 0 deletions src/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Range } from 'brighterscript';
import { DiagnosticSeverity } from 'brighterscript';

export const diagnostics = {
badAnnotationBody: (range: Range) => ({
message: 'Inlineable function body must be a single return statement',
code: 'bad-annotation-body',
severity: DiagnosticSeverity.Error,
range: range
})
};

0 comments on commit 8362b18

Please sign in to comment.