Skip to content
Draft
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
9 changes: 6 additions & 3 deletions src/astUtils/creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export function createToken<T extends TokenKind>(kind: T, text?: string, range?:
text: text ?? tokenDefaults[kind as string] ?? kind.toString().toLowerCase(),
isReserved: !text || text === kind.toString(),
range: range,
leadingWhitespace: ''
leadingWhitespace: '',
leadingTrivia: []
};
}

Expand All @@ -102,7 +103,8 @@ export function createIdentifier(name: string, range?: Range): Identifier {
text: name,
isReserved: false,
range: range,
leadingWhitespace: ''
leadingWhitespace: '',
leadingTrivia: []
};
}

Expand Down Expand Up @@ -172,7 +174,8 @@ export function createCall(callee: Expression, args?: Expression[]) {
callee,
createToken(TokenKind.LeftParen, '('),
createToken(TokenKind.RightParen, ')'),
args || []
args || [],
[]
);
}

Expand Down
17 changes: 9 additions & 8 deletions src/astUtils/reflection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ describe('reflection', () => {
const fors = new ForStatement(token, assignment, token, expr, block, token, token, expr);
const foreach = new ForEachStatement({ forEach: token, in: token, endFor: token }, token, expr, block);
const whiles = new WhileStatement({ while: token, endWhile: token }, expr, block);
const dottedSet = new DottedSetStatement(expr, ident, expr);
const indexedSet = new IndexedSetStatement(expr, expr, expr, token, token);
const dottedSet = new DottedSetStatement(expr, ident, expr, token, token);
const indexedSet = new IndexedSetStatement(expr, expr, expr, token, token, [], token);
const library = new LibraryStatement({ library: token, filePath: token });
const namespace = new NamespaceStatement(token, new NamespacedVariableNameExpression(createVariableExpression('a')), body, token);
const cls = new ClassStatement(token, ident, [], token);
Expand Down Expand Up @@ -193,28 +193,29 @@ describe('reflection', () => {
range: undefined,
isReserved: false,
charCode: 0,
leadingWhitespace: ''
leadingWhitespace: '',
leadingTrivia: []
};
const nsVar = new NamespacedVariableNameExpression(createVariableExpression('a'));
const binary = new BinaryExpression(expr, token, expr);
const call = new CallExpression(expr, token, token, []);
const call = new CallExpression(expr, token, token, [], []);
const fun = new FunctionExpression([], block, token, token, token, token);
const dottedGet = new DottedGetExpression(expr, ident, token);
const xmlAttrGet = new XmlAttributeGetExpression(expr, ident, token);
const indexedGet = new IndexedGetExpression(expr, expr, token, token);
const grouping = new GroupingExpression({ left: token, right: token }, expr);
const literal = createStringLiteral('test');
const escapedCarCode = new EscapedCharCodeLiteralExpression(charCode);
const arrayLit = new ArrayLiteralExpression([], token, token);
const arrayLit = new ArrayLiteralExpression([], token, token, []);
const aaLit = new AALiteralExpression([], token, token);
const unary = new UnaryExpression(token, expr);
const variable = new VariableExpression(ident);
const sourceLit = new SourceLiteralExpression(token);
const newx = new NewExpression(token, call);
const callfunc = new CallfuncExpression(expr, token, ident, token, [], token);
const callfunc = new CallfuncExpression(expr, token, ident, token, [], token, []);
const tplQuasi = new TemplateStringQuasiExpression([expr]);
const tplString = new TemplateStringExpression(token, [tplQuasi], [], token);
const taggedTpl = new TaggedTemplateStringExpression(ident, token, [tplQuasi], [], token);
const tplString = new TemplateStringExpression(token, [tplQuasi], [], token, [], []);
const taggedTpl = new TaggedTemplateStringExpression(ident, token, [tplQuasi], [], token, [], []);
const annotation = new AnnotationExpression(token, token);

it('isExpression', () => {
Expand Down
5 changes: 3 additions & 2 deletions src/astUtils/reflection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { DynamicType } from '../types/DynamicType';
import type { InterfaceType } from '../types/InterfaceType';
import type { ObjectType } from '../types/ObjectType';
import type { AstNode, Expression, Statement } from '../parser/AstNode';
import type { Token } from '../lexer/Token';

// File reflection

Expand Down Expand Up @@ -198,9 +199,9 @@ export function isAliasStatement(element: AstNode | undefined): element is Alias
* this will work for StringLiteralExpression -> Expression,
* but will not work CustomStringLiteralExpression -> StringLiteralExpression -> Expression
*/
export function isExpression(element: AstNode | undefined): element is Expression {
export function isExpression(element: AstNode | Token | undefined): element is Expression {
// eslint-disable-next-line no-bitwise
return !!(element && element.visitMode & InternalWalkMode.visitExpressions);
return !!(element && (element as any).visitMode & InternalWalkMode.visitExpressions);
}

export function isBinaryExpression(element: AstNode | undefined): element is BinaryExpression {
Expand Down
61 changes: 59 additions & 2 deletions src/lexer/Lexer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint no-template-curly-in-string: 0 */
import { expect } from '../chai-config.spec';

import { TokenKind } from './TokenKind';
import { Lexer } from './Lexer';
import { Lexer, triviaKinds } from './Lexer';
import type { Token } from './Token';
import { isToken } from './Token';
import { rangeToArray } from '../parser/Parser.spec';
import { Range } from 'vscode-languageserver';
Expand Down Expand Up @@ -1452,6 +1452,63 @@ describe('lexer', () => {
TokenKind.Eof
]);
});

describe('trivia', () => {
function stringify(tokens: Token[]) {
return tokens
//exclude the explicit triva tokens since they'll be included in the leading/trailing arrays
.filter(x => !triviaKinds.includes(x.kind))
.flatMap(x => [...x.leadingTrivia, x])
.map(x => x.text)
.join('');
}

it('combining token text and trivia can reproduce full input', () => {
const input = `
function test( )
'comment
print alpha ' blabla
end function 'trailing
'trailing2
`;
expect(
stringify(
Lexer.scan(input).tokens
)
).to.eql(input);
});

function expectTrivia(text: string, expected: Array<{ text: string; leadingTrivia?: string[]; trailingTrivia?: string[] }>) {
const tokens = Lexer.scan(text).tokens.filter(x => !triviaKinds.includes(x.kind));
expect(
tokens.map(x => {
return {
text: x.text,
leadingTrivia: x.leadingTrivia.map(x => x.text)
};
})
).to.eql(
expected.map(x => ({
leadingTrivia: [],
...x
}))
);
}

it('associates trailing items on same line with the preceeding token', () => {
expectTrivia(
`'leading\n` +
`alpha = true 'trueComment\n` +
`'eof`
, [
{ leadingTrivia: [`'leading`, `\n`], text: `alpha` },
{ leadingTrivia: [` `], text: `=` },
{ leadingTrivia: [` `], text: `true` },
//EOF
{ leadingTrivia: [` `, `'trueComment`, `\n`, `'eof`], text: `` }
]);
});
});
});

function expectKinds(text: string, tokenKinds: TokenKind[]) {
Expand Down
35 changes: 33 additions & 2 deletions src/lexer/Lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import type { Range, Diagnostic } from 'vscode-languageserver';
import { DiagnosticMessages } from '../DiagnosticMessages';
import util from '../util';

export const triviaKinds: ReadonlyArray<TokenKind> = [
TokenKind.Newline,
TokenKind.Whitespace,
TokenKind.Comment,
TokenKind.Colon
];

/**
* Numeric type designators can only be one of these characters
*/
Expand Down Expand Up @@ -110,12 +117,22 @@ export class Lexer {
range: this.options.trackLocations
? util.createRange(this.lineBegin, this.columnBegin, this.lineEnd, this.columnEnd + 1)
: undefined,
leadingWhitespace: this.leadingWhitespace
leadingWhitespace: this.leadingWhitespace,
leadingTrivia: this.leadingTrivia
});
this.leadingWhitespace = '';
return this;
}

private leadingTrivia: Token[] = [];

/**
* Pushes a token into the leadingTrivia list
*/
private pushTrivia(token: Token) {
this.leadingTrivia.push(token);
}

/**
* Fill in missing/invalid options with defaults
*/
Expand Down Expand Up @@ -1050,6 +1067,13 @@ export class Lexer {
return false;
}

/**
* Determine if this token is a trivia token
*/
private isTrivia(token: Token) {
return triviaKinds.includes(token.kind);
}

/**
* Creates a `Token` and adds it to the `tokens` array.
* @param kind the type of token to produce.
Expand All @@ -1061,8 +1085,15 @@ export class Lexer {
text: text,
isReserved: ReservedWords.has(text.toLowerCase()),
range: this.rangeOf(),
leadingWhitespace: this.leadingWhitespace
leadingWhitespace: this.leadingWhitespace,
leadingTrivia: []
};
if (this.isTrivia(token)) {
this.pushTrivia(token);
} else {
token.leadingTrivia.push(...this.leadingTrivia);
this.leadingTrivia = [];
}
this.leadingWhitespace = '';
this.tokens.push(token);
this.sync();
Expand Down
4 changes: 4 additions & 0 deletions src/lexer/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export interface Token {
* Any leading whitespace found prior to this token. Excludes newline characters.
*/
leadingWhitespace?: string;
/**
* Any tokens starting on the next line of the previous token, up to the start of this token
*/
leadingTrivia: Token[];
}

/**
Expand Down
43 changes: 42 additions & 1 deletion src/parser/AstNode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { expect } from '../chai-config.spec';
import type { AALiteralExpression, AAMemberExpression, ArrayLiteralExpression, BinaryExpression, CallExpression, CallfuncExpression, DottedGetExpression, FunctionExpression, GroupingExpression, IndexedGetExpression, NewExpression, NullCoalescingExpression, TaggedTemplateStringExpression, TemplateStringExpression, TemplateStringQuasiExpression, TernaryExpression, TypeCastExpression, UnaryExpression, XmlAttributeGetExpression } from './Expression';
import { expectZeroDiagnostics } from '../testHelpers.spec';
import { tempDir, rootDir, stagingDir } from '../testHelpers.spec';
import { ParseMode, Parser } from './Parser';
import { isAALiteralExpression, isAAMemberExpression, isAnnotationExpression, isArrayLiteralExpression, isAssignmentStatement, isBinaryExpression, isBlock, isCallExpression, isCallfuncExpression, isCatchStatement, isClassStatement, isCommentStatement, isConstStatement, isDimStatement, isDottedGetExpression, isDottedSetStatement, isEnumMemberStatement, isEnumStatement, isExpressionStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isGroupingExpression, isIfStatement, isIncrementStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInterfaceStatement, isLibraryStatement, isMethodStatement, isNamespaceStatement, isNewExpression, isNullCoalescingExpression, isPrintStatement, isReturnStatement, isTaggedTemplateStringExpression, isTemplateStringExpression, isTemplateStringQuasiExpression, isTernaryExpression, isThrowStatement, isTryCatchStatement, isTypeCastExpression, isUnaryExpression, isWhileStatement, isXmlAttributeGetExpression } from '../astUtils/reflection';
import type { ClassStatement, FunctionStatement, InterfaceFieldStatement, InterfaceMethodStatement, MethodStatement, InterfaceStatement, CatchStatement, ThrowStatement, EnumStatement, EnumMemberStatement, ConstStatement, Block, CommentStatement, PrintStatement, DimStatement, ForStatement, WhileStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, TryCatchStatement, DottedSetStatement } from './Statement';
import { AssignmentStatement, EmptyStatement } from './Statement';
import { ParseMode, Parser } from './Parser';
import type { AstNode } from './AstNode';

type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
Expand Down Expand Up @@ -1670,4 +1670,45 @@ describe('AstNode', () => {
testClone(original);
});
});

describe('toString', () => {
function testToString(text: string) {
expect(
Parser.parse(text).ast.toString()
).to.eql(
text
);
}
it('retains full fidelity', () => {
testToString(`
thing = true

if true
thing = true
end if

if true
thing = true
else
thing = true
end if

if true
thing = true
else if true
thing = true
else
thing = true
end if

for i = 0 to 10 step 1
print true,false;3
end for

for each item in thing
print 1
end for
`);
});
});
});
16 changes: 16 additions & 0 deletions src/parser/AstNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { BrsTranspileState } from './BrsTranspileState';
import type { TranspileResult } from '../interfaces';
import type { AnnotationExpression } from './Expression';
import util from '../util';
import type { SourceNode } from 'source-map';
import { TranspileState } from './TranspileState';

/**
* A BrightScript AST node
Expand Down Expand Up @@ -129,6 +131,20 @@ export abstract class AstNode {
});
}

/**
* Return the string value of this AstNode
*/
public toString() {
return this
.toSourceNode(new TranspileState('', {}))
.toString();
}

/**
* Generate a SourceNode that represents the stringified value of this node (used to generate sourcemaps and transpile the code
*/
public abstract toSourceNode(state: TranspileState): SourceNode;

/**
* Clone this node and all of its children. This creates a completely detached and identical copy of the AST.
* All tokens, statements, expressions, range, and location are cloned.
Expand Down
Loading
Loading