Skip to content

Commit

Permalink
support adding/deleting to ast arrays in visitor
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed May 25, 2022
1 parent df7ef9c commit 7fc58b6
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 53 deletions.
67 changes: 64 additions & 3 deletions src/astUtils/visitors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { expect } from 'chai';
import * as sinon from 'sinon';
import { Program } from '../Program';
import type { BrsFile } from '../files/BrsFile';
import type { Statement } from '../parser/Statement';
import { PrintStatement, Block, ReturnStatement } from '../parser/Statement';
import type { FunctionStatement, Statement } from '../parser/Statement';
import { PrintStatement, Block, ReturnStatement, ExpressionStatement } from '../parser/Statement';
import type { Expression } from '../parser/Expression';
import { TokenKind } from '../lexer/TokenKind';
import { createVisitor, WalkMode, walkStatements } from './visitors';
import { isPrintStatement } from './reflection';
import { createToken } from './creators';
import { createCall, createToken, createVariableExpression } from './creators';
import { createStackedVisitor } from './stackedVisitor';
import { AstEditor } from './AstEditor';
import { Parser } from '../parser/Parser';
Expand Down Expand Up @@ -944,5 +944,66 @@ describe('astUtils visitors', () => {
});
expect(items).to.be.length(17);
});

it('can be used to delete statements', () => {
const { ast } = Parser.parse(`
sub main()
print 1
print 2
print 3
end sub
`);
let callCount = 0;
ast.walk((astNode, parent, owningObject: Statement[], key) => {
if (isPrintStatement(astNode)) {
callCount++;
//delete the print statement (we know owningObject is an array based on this specific test)
owningObject.splice(key, 1);
}
}, {
walkMode: WalkMode.visitAllRecursive
});
//the visitor should have been called for every statement
expect(callCount).to.eql(3);
expect(
(ast.statements[0] as FunctionStatement).func.body.statements
).to.be.lengthOf(0);
});

it('can be used to insert statements', () => {
const { ast } = Parser.parse(`
sub main()
print 1
print 2
print 3
end sub
`);
let printStatementCount = 0;
let callExpressionCount = 0;
const calls = [];
ast.walk(createVisitor({
PrintStatement: (astNode, parent, owningObject: Statement[], key) => {
printStatementCount++;
//add another expression to the list every time. This should result in 1 the first time, 2 the second, 3 the third.
calls.push(new ExpressionStatement(
createCall(
createVariableExpression('doSomethingBeforePrint')
)
));
owningObject.splice(key, 0, ...calls);
},
CallExpression: () => {
callExpressionCount++;
}
}), {
walkMode: WalkMode.visitAllRecursive
});
//the visitor should have been called for every statement
expect(printStatementCount).to.eql(3);
expect(callExpressionCount).to.eql(0);
expect(
(ast.statements[0] as FunctionStatement).func.body.statements
).to.be.lengthOf(9);
});
});
});
19 changes: 17 additions & 2 deletions src/astUtils/visitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ export function walk<T>(owningObject: T, key: keyof T, visitor: WalkVisitor, opt
element.walk(visitor, options);
}

/**
* Helper for AST elements to walk arrays when visitors might change the array size (to delete/insert items).
* @param filter a function used to filter items from the array. return true if that item should be walked
*/
export function walkArray<T>(array: Array<T>, visitor: WalkVisitor, options: WalkOptions, parent?: Expression | Statement, filter?: <T>(element: T) => boolean) {
for (let i = 0; i < array.length; i++) {
if (!filter || filter(array[i])) {
const startLength = array.length;
walk(array, i, visitor, options, parent);
//compensate for deleted or added items.
i += array.length - startLength;
}
}
}

/**
* Creates an optimized visitor function.
* Conventional visitors will need to inspect each incoming Statement/Expression, leading to many if statements.
Expand Down Expand Up @@ -154,8 +169,8 @@ export function createVisitor(
if (visitor.ClassMethodStatement) {
visitor.MethodStatement = visitor.ClassMethodStatement;
}
return <WalkVisitor>((statement: Statement, parent?: Statement): Statement | void => {
return visitor[statement.constructor.name]?.(statement, parent);
return <WalkVisitor>((statement: Statement, parent?: Statement, owningObject?: any, key?: any): Statement | void => {
return visitor[statement.constructor.name]?.(statement, parent, owningObject, key);
});
}

Expand Down
30 changes: 7 additions & 23 deletions src/parser/Expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { BrsTranspileState } from './BrsTranspileState';
import { ParseMode } from './Parser';
import * as fileUrl from 'file-url';
import type { WalkOptions, WalkVisitor } from '../astUtils/visitors';
import { walk, InternalWalkMode } from '../astUtils/visitors';
import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors';
import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isStringType, isUnaryExpression, isVariableExpression } from '../astUtils/reflection';
import type { TranspileResult, TypedefProvider } from '../interfaces';
import { VoidType } from '../types/VoidType';
Expand Down Expand Up @@ -116,9 +116,7 @@ export class CallExpression extends Expression {
walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkExpressions) {
walk(this, 'callee', visitor, options);
for (let i = 0; i < this.args.length; i++) {
walk(this.args, i, visitor, options, this);
}
walkArray(this.args, visitor, options, this);
}
}
}
Expand Down Expand Up @@ -244,9 +242,7 @@ export class FunctionExpression extends Expression implements TypedefProvider {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkExpressions) {
for (let i = 0; i < this.parameters.length; i++) {
walk(this.parameters, i, visitor, options, this);
}
walkArray(this.parameters, visitor, options, this);

//This is the core of full-program walking...it allows us to step into sub functions
if (options.walkMode & InternalWalkMode.recurseChildFunctions) {
Expand Down Expand Up @@ -625,9 +621,7 @@ export class ArrayLiteralExpression extends Expression {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkExpressions) {
for (let i = 0; i < this.elements.length; i++) {
walk(this.elements, i, visitor, options, this);
}
walkArray(this.elements, visitor, options, this);
}
}
}
Expand Down Expand Up @@ -739,13 +733,7 @@ export class AALiteralExpression extends Expression {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkExpressions) {
for (let i = 0; i < this.elements.length; i++) {
if (isCommentStatement(this.elements[i])) {
walk(this.elements, i, visitor, options, this);
} else {
walk(this.elements, i, visitor, options, this);
}
}
walkArray(this.elements, visitor, options, this);
}
}
}
Expand Down Expand Up @@ -990,9 +978,7 @@ export class CallfuncExpression extends Expression {
walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkExpressions) {
walk(this, 'callee', visitor, options);
for (let i = 0; i < this.args.length; i++) {
walk(this.args, i, visitor, options, this);
}
walkArray(this.args, visitor, options, this);
}
}
}
Expand Down Expand Up @@ -1033,9 +1019,7 @@ export class TemplateStringQuasiExpression extends Expression {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkExpressions) {
for (let i = 0; i < this.expressions.length; i++) {
walk(this.expressions, i, visitor, options, this);
}
walkArray(this.expressions, visitor, options, this);
}
}
}
Expand Down
37 changes: 12 additions & 25 deletions src/parser/Statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Position } from 'vscode-languageserver';
import type { BrsTranspileState } from './BrsTranspileState';
import { ParseMode } from './Parser';
import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
import { InternalWalkMode, walk, createVisitor, WalkMode } from '../astUtils/visitors';
import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors';
import { isCallExpression, isClassFieldStatement, isClassMethodStatement, isCommentStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isTypedefProvider, isVoidType } from '../astUtils/reflection';
import type { TranspileResult, TypedefProvider } from '../interfaces';
import { createInvalidLiteral, createMethodStatement, createToken, interpolatedRange } from '../astUtils/creators';
Expand Down Expand Up @@ -128,9 +128,7 @@ export class Body extends Statement implements TypedefProvider {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkStatements) {
for (let i = 0; i < this.statements.length; i++) {
walk(this.statements, i, visitor, options, this);
}
walkArray(this.statements, visitor, options, this);
}
}
}
Expand Down Expand Up @@ -221,9 +219,7 @@ export class Block extends Statement {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkStatements) {
for (let i = 0; i < this.statements.length; i++) {
walk(this.statements, i, visitor, options, this);
}
walkArray(this.statements, visitor, options, this);
}
}
}
Expand Down Expand Up @@ -598,12 +594,8 @@ export class PrintStatement extends Statement {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkExpressions) {
for (let i = 0; i < this.expressions.length; i++) {
//sometimes we have semicolon `Token`s in the expressions list (should probably fix that...), so only emit the actual expressions
if (isExpression(this.expressions[i] as any)) {
walk(this.expressions, i, visitor, options, this);
}
}
//sometimes we have semicolon Tokens in the expressions list (should probably fix that...), so only walk the actual expressions
walkArray(this.expressions, visitor, options, this, (item) => isExpression(item as any));
}
}
}
Expand Down Expand Up @@ -645,9 +637,8 @@ export class DimStatement extends Statement {

public walk(visitor: WalkVisitor, options: WalkOptions) {
if (this.dimensions?.length > 0 && options.walkMode & InternalWalkMode.walkExpressions) {
for (let i = 0; i < this.dimensions.length; i++) {
walk(this.dimensions, i, visitor, options, this);
}
walkArray(this.dimensions, visitor, options, this);

}
}
}
Expand Down Expand Up @@ -1348,9 +1339,8 @@ export class InterfaceStatement extends Statement implements TypedefProvider {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkStatements) {
for (let i = 0; i < this.body.length; i++) {
walk(this.body, i, visitor, options, this);
}
walkArray(this.body, visitor, options, this);

}
}
}
Expand Down Expand Up @@ -1866,9 +1856,7 @@ export class ClassStatement extends Statement implements TypedefProvider {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkStatements) {
for (let i = 0; i < this.body.length; i++) {
walk(this.body, i, visitor, options, this);
}
walkArray(this.body, visitor, options, this);
}
}
}
Expand Down Expand Up @@ -2388,9 +2376,8 @@ export class EnumStatement extends Statement implements TypedefProvider {

walk(visitor: WalkVisitor, options: WalkOptions) {
if (options.walkMode & InternalWalkMode.walkStatements) {
for (let i = 0; i < this.body.length; i++) {
walk(this.body, i, visitor, options, this);
}
walkArray(this.body, visitor, options, this);

}
}
}
Expand Down

0 comments on commit 7fc58b6

Please sign in to comment.