From 43cbf8b72fa39ddd518cd46bba498a507db1a78e Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Wed, 21 Aug 2024 11:18:57 -0300 Subject: [PATCH] Merges `ExitForStatement` and `ExitWhileStatement` (#1289) * Merges exitFor and exitWhile statements * Properly use original text for exitwhile (one word) * Updates the tokens in parser.tokens to reflect the created while token --- src/astUtils/reflection.spec.ts | 17 +++--- src/astUtils/reflection.ts | 9 ++-- src/astUtils/visitors.ts | 5 +- src/files/BrsFile.spec.ts | 49 +++++++++++++++++ src/lexer/Lexer.spec.ts | 9 ++-- src/lexer/TokenKind.ts | 5 -- src/parser/AstNode.ts | 3 +- src/parser/Parser.spec.ts | 71 ++++++++++++++++++++++++- src/parser/Parser.ts | 56 +++++++++++++------ src/parser/Statement.spec.ts | 6 +-- src/parser/Statement.ts | 60 +++++++-------------- src/parser/tests/statement/Misc.spec.ts | 1 - src/testHelpers.spec.ts | 12 +++-- 13 files changed, 203 insertions(+), 100 deletions(-) diff --git a/src/astUtils/reflection.spec.ts b/src/astUtils/reflection.spec.ts index 104570d5b..eb5b8f6b5 100644 --- a/src/astUtils/reflection.spec.ts +++ b/src/astUtils/reflection.spec.ts @@ -1,10 +1,10 @@ /* eslint-disable no-multi-spaces */ import { expect } from '../chai-config.spec'; -import { PrintStatement, Block, Body, AssignmentStatement, ExitForStatement, ExitWhileStatement, ExpressionStatement, FunctionStatement, IfStatement, IncrementStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, EmptyStatement, TryCatchStatement, CatchStatement, ThrowStatement } from '../parser/Statement'; +import { PrintStatement, Block, Body, AssignmentStatement, ExpressionStatement, FunctionStatement, IfStatement, IncrementStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, EmptyStatement, TryCatchStatement, CatchStatement, ThrowStatement, ExitStatement } from '../parser/Statement'; import { FunctionExpression, BinaryExpression, CallExpression, DottedGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, XmlAttributeGetExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression } from '../parser/Expression'; import type { Token } from '../lexer/Token'; import { TokenKind } from '../lexer/TokenKind'; -import { isPrintStatement, isIfStatement, isBody, isAssignmentStatement, isBlock, isExpressionStatement, isExitForStatement, isExitWhileStatement, isFunctionStatement, isIncrementStatement, isGotoStatement, isLabelStatement, isReturnStatement, isEndStatement, isStopStatement, isForStatement, isForEachStatement, isWhileStatement, isDottedSetStatement, isIndexedSetStatement, isLibraryStatement, isNamespaceStatement, isImportStatement, isExpression, isBinaryExpression, isCallExpression, isFunctionExpression, isDottedGetExpression, isXmlAttributeGetExpression, isIndexedGetExpression, isGroupingExpression, isLiteralExpression, isEscapedCharCodeLiteralExpression, isArrayLiteralExpression, isAALiteralExpression, isUnaryExpression, isVariableExpression, isSourceLiteralExpression, isNewExpression, isCallfuncExpression, isTemplateStringQuasiExpression, isTemplateStringExpression, isTaggedTemplateStringExpression, isBrsFile, isXmlFile, isClassStatement, isStatement, isAnnotationExpression, isTryCatchStatement, isCatchStatement, isThrowStatement, isLiteralInvalid, isLiteralBoolean, isLiteralNumber, isLiteralInteger, isLiteralLongInteger, isLiteralFloat, isLiteralDouble } from './reflection'; +import { isPrintStatement, isIfStatement, isBody, isAssignmentStatement, isBlock, isExpressionStatement, isFunctionStatement, isIncrementStatement, isGotoStatement, isLabelStatement, isReturnStatement, isEndStatement, isStopStatement, isForStatement, isForEachStatement, isWhileStatement, isDottedSetStatement, isIndexedSetStatement, isLibraryStatement, isNamespaceStatement, isImportStatement, isExpression, isBinaryExpression, isCallExpression, isFunctionExpression, isDottedGetExpression, isXmlAttributeGetExpression, isIndexedGetExpression, isGroupingExpression, isLiteralExpression, isEscapedCharCodeLiteralExpression, isArrayLiteralExpression, isAALiteralExpression, isUnaryExpression, isVariableExpression, isSourceLiteralExpression, isNewExpression, isCallfuncExpression, isTemplateStringQuasiExpression, isTemplateStringExpression, isTaggedTemplateStringExpression, isBrsFile, isXmlFile, isClassStatement, isStatement, isAnnotationExpression, isTryCatchStatement, isCatchStatement, isThrowStatement, isLiteralInvalid, isLiteralBoolean, isLiteralNumber, isLiteralInteger, isLiteralLongInteger, isLiteralFloat, isLiteralDouble, isExitStatement } from './reflection'; import { createToken, createStringLiteral, createInvalidLiteral, createBooleanLiteral, createIntegerLiteral, createVariableExpression, createFloatLiteral, createDoubleLiteral, createLongIntegerLiteral } from './creators'; import { Program } from '../Program'; import { BrsFile } from '../files/BrsFile'; @@ -31,8 +31,7 @@ describe('reflection', () => { const assignment = new AssignmentStatement({ equals: undefined, name: ident, value: expr }); const block = new Block({ statements: [] }); const expression = new ExpressionStatement({ expression: expr }); - const exitFor = new ExitForStatement({ exitFor: token }); - const exitWhile = new ExitWhileStatement({ exitWhile: token }); + const exit = new ExitStatement({ exit: token, loopType: token }); const funs = new FunctionStatement({ name: ident, func: new FunctionExpression({ @@ -92,13 +91,9 @@ describe('reflection', () => { expect(isExpressionStatement(expression)).to.be.true; expect(isExpressionStatement(body)).to.be.false; }); - it('isExitForStatement', () => { - expect(isExitForStatement(exitFor)).to.be.true; - expect(isExitForStatement(body)).to.be.false; - }); - it('isExitWhileStatement', () => { - expect(isExitWhileStatement(exitWhile)).to.be.true; - expect(isExitWhileStatement(body)).to.be.false; + it('isExitStatement', () => { + expect(isExitStatement(exit)).to.be.true; + expect(isExitStatement(body)).to.be.false; }); it('isFunctionStatement', () => { expect(isFunctionStatement(funs)).to.be.true; diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 7de729371..11abe6301 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -1,4 +1,4 @@ -import type { Body, AssignmentStatement, Block, ExpressionStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement, TypecastStatement, AliasStatement, ConditionalCompileStatement, ConditionalCompileConstStatement, ConditionalCompileErrorStatement, AugmentedAssignmentStatement } from '../parser/Statement'; +import type { Body, AssignmentStatement, Block, ExpressionStatement, ExitStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement, TypecastStatement, AliasStatement, ConditionalCompileStatement, ConditionalCompileConstStatement, ConditionalCompileErrorStatement, AugmentedAssignmentStatement } from '../parser/Statement'; import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TypecastExpression, TypeExpression, TypedArrayExpression } from '../parser/Expression'; import type { BrsFile } from '../files/BrsFile'; import type { XmlFile } from '../files/XmlFile'; @@ -85,11 +85,8 @@ export function isBlock(element: AstNode | undefined): element is Block { export function isExpressionStatement(element: AstNode | undefined): element is ExpressionStatement { return element?.kind === AstNodeKind.ExpressionStatement; } -export function isExitForStatement(element: AstNode | undefined): element is ExitForStatement { - return element?.kind === AstNodeKind.ExitForStatement; -} -export function isExitWhileStatement(element: AstNode | undefined): element is ExitWhileStatement { - return element?.kind === AstNodeKind.ExitWhileStatement; +export function isExitStatement(element: AstNode | undefined): element is ExitStatement { + return element?.kind === AstNodeKind.ExitStatement; } export function isFunctionStatement(element: AstNode | undefined): element is FunctionStatement { return element?.kind === AstNodeKind.FunctionStatement; diff --git a/src/astUtils/visitors.ts b/src/astUtils/visitors.ts index c4e38d2a7..af92d64c4 100644 --- a/src/astUtils/visitors.ts +++ b/src/astUtils/visitors.ts @@ -1,6 +1,6 @@ /* eslint-disable no-bitwise */ import type { CancellationToken } from 'vscode-languageserver'; -import type { Body, AssignmentStatement, Block, ExpressionStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, EnumStatement, EnumMemberStatement, DimStatement, TryCatchStatement, CatchStatement, ThrowStatement, InterfaceStatement, InterfaceFieldStatement, InterfaceMethodStatement, FieldStatement, MethodStatement, ConstStatement, ContinueStatement, TypecastStatement, AliasStatement, ConditionalCompileStatement, ConditionalCompileErrorStatement, ConditionalCompileConstStatement, AugmentedAssignmentStatement } from '../parser/Statement'; +import type { Body, AssignmentStatement, Block, ExpressionStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, EnumStatement, EnumMemberStatement, DimStatement, TryCatchStatement, CatchStatement, ThrowStatement, InterfaceStatement, InterfaceFieldStatement, InterfaceMethodStatement, FieldStatement, MethodStatement, ConstStatement, ContinueStatement, TypecastStatement, AliasStatement, ConditionalCompileStatement, ConditionalCompileErrorStatement, ConditionalCompileConstStatement, AugmentedAssignmentStatement, ExitStatement } from '../parser/Statement'; import type { AALiteralExpression, AAMemberExpression, AnnotationExpression, ArrayLiteralExpression, BinaryExpression, CallExpression, CallfuncExpression, DottedGetExpression, EscapedCharCodeLiteralExpression, FunctionExpression, FunctionParameterExpression, GroupingExpression, IndexedGetExpression, LiteralExpression, NewExpression, NullCoalescingExpression, RegexLiteralExpression, SourceLiteralExpression, TaggedTemplateStringExpression, TemplateStringExpression, TemplateStringQuasiExpression, TernaryExpression, TypecastExpression, TypeExpression, UnaryExpression, VariableExpression, XmlAttributeGetExpression } from '../parser/Expression'; import { isExpression, isStatement } from './reflection'; import type { Editor } from './Editor'; @@ -116,8 +116,7 @@ export function createVisitor( AssignmentStatement?: (statement: AssignmentStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; Block?: (statement: Block, parent?: Statement, owner?: any, key?: any) => Statement | void; ExpressionStatement?: (statement: ExpressionStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; - ExitForStatement?: (statement: ExitForStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; - ExitWhileStatement?: (statement: ExitWhileStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; + ExitStatement?: (statement: ExitStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; FunctionStatement?: (statement: FunctionStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; IfStatement?: (statement: IfStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; IncrementStatement?: (statement: IncrementStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 5cb1365ed..b1db5d7f6 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -2953,6 +2953,55 @@ describe('BrsFile', () => { expect(location.column).eql(4); }); + describe('jump statements', () => { + it('handles exit for', async () => { + await testTranspile(` + sub main() + for i = 1 to 10 + exit for + end for + end sub + `); + }); + + it('handles exit while', async () => { + await testTranspile(` + sub main() + while true + exit while + end while + end sub + `); + }); + + it('handles exitWhile (one word)', async () => { + await testTranspile(` + sub main() + while true + exitWhile + end while + end sub + `); + }); + + it('transpiles case correctly', async () => { + await testTranspile(` + sub main() + for i = 1 to 10 + eXiT fOr + end for + while true + exIt whILE + end while + while true + eXitWhile + end while + end sub + `); + }); + }); + + describe('sourcemap validation', () => { it('computes correct source and position in sourcemap', async () => { program.options.sourceMap = true; diff --git a/src/lexer/Lexer.spec.ts b/src/lexer/Lexer.spec.ts index 6d86e34f9..f83e36765 100644 --- a/src/lexer/Lexer.spec.ts +++ b/src/lexer/Lexer.spec.ts @@ -932,7 +932,7 @@ describe('lexer', () => { it('matches single-word keywords', () => { // test just a sample of single-word reserved words for now. // if we find any that we've missed - let { tokens } = Lexer.scan('and then or if else endif return true false line_num'); + let { tokens } = Lexer.scan('and then or if else endif return true false line_num exitwhile'); expect(tokens.map(w => w.kind)).to.deep.equal([ TokenKind.And, TokenKind.Then, @@ -944,6 +944,7 @@ describe('lexer', () => { TokenKind.True, TokenKind.False, TokenKind.LineNumLiteral, + TokenKind.ExitWhile, TokenKind.Eof ]); }); @@ -955,7 +956,8 @@ describe('lexer', () => { TokenKind.EndWhile, TokenKind.EndSub, TokenKind.EndFunction, - TokenKind.ExitWhile, + TokenKind.Exit, + TokenKind.While, TokenKind.Eof ]); }); @@ -963,7 +965,8 @@ describe('lexer', () => { it('accepts \'exit for\' but not \'exitfor\'', () => { let { tokens } = Lexer.scan('exit for exitfor'); expect(tokens.map(w => w.kind)).to.deep.equal([ - TokenKind.ExitFor, + TokenKind.Exit, + TokenKind.For, TokenKind.Identifier, TokenKind.Eof ]); diff --git a/src/lexer/TokenKind.ts b/src/lexer/TokenKind.ts index 03256ee04..7976604ba 100644 --- a/src/lexer/TokenKind.ts +++ b/src/lexer/TokenKind.ts @@ -108,7 +108,6 @@ export enum TokenKind { EndWhile = 'EndWhile', Eval = 'Eval', Exit = 'Exit', - ExitFor = 'ExitFor', // not technically a reserved word, but definitely a tokenKind ExitWhile = 'ExitWhile', False = 'False', For = 'For', @@ -269,9 +268,7 @@ export const Keywords: Record = { endwhile: TokenKind.EndWhile, 'end while': TokenKind.EndWhile, exit: TokenKind.Exit, - 'exit for': TokenKind.ExitFor, // note: 'exitfor' (no space) is *not* a keyword exitwhile: TokenKind.ExitWhile, - 'exit while': TokenKind.ExitWhile, false: TokenKind.False, for: TokenKind.For, 'for each': TokenKind.ForEach, // note: 'foreach' (no space) is *not* a keyword @@ -400,7 +397,6 @@ export const AllowedProperties = [ TokenKind.EndWhile, TokenKind.Eval, TokenKind.Exit, - TokenKind.ExitFor, TokenKind.ExitWhile, TokenKind.False, TokenKind.For, @@ -476,7 +472,6 @@ export const AllowedProperties = [ /** List of TokenKind that are allowed as local var identifiers. */ export const AllowedLocalIdentifiers = [ TokenKind.EndFor, - TokenKind.ExitFor, TokenKind.ForEach, TokenKind.Void, TokenKind.Boolean, diff --git a/src/parser/AstNode.ts b/src/parser/AstNode.ts index 2a89b50a7..0b7dd04ae 100644 --- a/src/parser/AstNode.ts +++ b/src/parser/AstNode.ts @@ -247,8 +247,7 @@ export enum AstNodeKind { EmptyStatement = 'EmptyStatement', AssignmentStatement = 'AssignmentStatement', ExpressionStatement = 'ExpressionStatement', - ExitForStatement = 'ExitForStatement', - ExitWhileStatement = 'ExitWhileStatement', + ExitStatement = 'ExitStatement', FunctionStatement = 'FunctionStatement', IfStatement = 'IfStatement', IncrementStatement = 'IncrementStatement', diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index 320cc7d40..f75c915e7 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -4,11 +4,11 @@ import { ReservedWords, TokenKind } from '../lexer/TokenKind'; import type { AAMemberExpression, BinaryExpression, LiteralExpression, TypecastExpression, UnaryExpression } from './Expression'; import { TernaryExpression, NewExpression, IndexedGetExpression, DottedGetExpression, XmlAttributeGetExpression, CallfuncExpression, AnnotationExpression, CallExpression, FunctionExpression, VariableExpression } from './Expression'; import { Parser, ParseMode } from './Parser'; -import type { AliasStatement, AssignmentStatement, Block, ClassStatement, ConditionalCompileConstStatement, ConditionalCompileErrorStatement, ConditionalCompileStatement, InterfaceStatement, ReturnStatement, TypecastStatement } from './Statement'; +import type { AliasStatement, AssignmentStatement, Block, ClassStatement, ConditionalCompileConstStatement, ConditionalCompileErrorStatement, ConditionalCompileStatement, ExitStatement, ForStatement, InterfaceStatement, ReturnStatement, TypecastStatement } from './Statement'; import { PrintStatement, FunctionStatement, NamespaceStatement, ImportStatement } from './Statement'; import { Range } from 'vscode-languageserver'; import { DiagnosticMessages } from '../DiagnosticMessages'; -import { isAliasStatement, isAssignmentStatement, isBinaryExpression, isBlock, isBody, isCallExpression, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isDottedGetExpression, isExpression, isExpressionStatement, isFunctionStatement, isGroupingExpression, isIfStatement, isIndexedGetExpression, isInterfaceStatement, isLiteralExpression, isNamespaceStatement, isPrintStatement, isTypecastExpression, isTypecastStatement, isUnaryExpression, isVariableExpression } from '../astUtils/reflection'; +import { isAliasStatement, isAssignmentStatement, isBinaryExpression, isBlock, isBody, isCallExpression, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isDottedGetExpression, isExitStatement, isExpression, isExpressionStatement, isFunctionStatement, isGroupingExpression, isIfStatement, isIndexedGetExpression, isInterfaceStatement, isLiteralExpression, isNamespaceStatement, isPrintStatement, isTypecastExpression, isTypecastStatement, isUnaryExpression, isVariableExpression } from '../astUtils/reflection'; import { expectDiagnostics, expectDiagnosticsIncludes, expectTypeToBe, expectZeroDiagnostics, rootDir } from '../testHelpers.spec'; import { createVisitor, WalkMode } from '../astUtils/visitors'; import type { Expression, Statement } from './AstNode'; @@ -2292,6 +2292,73 @@ describe('parser', () => { expect(((ast.statements[0] as FunctionStatement).func.body.statements[0] as AssignmentStatement).tokens.name.text).to.eq('alias'); }); }); + + describe('jump statements', () => { + it('should recognize `exit for`', () => { + let { ast, diagnostics } = parse(` + sub main() + for i = 1 to 10 + exit for + end for + end sub + `); + expectZeroDiagnostics(diagnostics); + let loop = (ast.statements[0] as FunctionStatement).func.body.statements[0] as ForStatement; + let exitStmt = loop.body.statements[0] as ExitStatement; + expect(isExitStatement(exitStmt)).to.be.true; + expect(exitStmt.tokens.loopType.text).to.eq('for'); + }); + + it('should recognize `exit while`', () => { + let { ast, diagnostics } = parse(` + sub main(i) + while i < 10 + exit while + i++ + end while + end sub + `); + expectZeroDiagnostics(diagnostics); + let loop = (ast.statements[0] as FunctionStatement).func.body.statements[0] as ForStatement; + let exitStmt = loop.body.statements[0] as ExitStatement; + expect(isExitStatement(exitStmt)).to.be.true; + expect(exitStmt.tokens.loopType.text).to.eq('while'); + }); + + it('should recognize `exitwhile` (one word)', () => { + let { ast, diagnostics } = parse(` + sub main(i) + while i < 10 + exitwhile + i++ + end while + end sub + `); + expectZeroDiagnostics(diagnostics); + let loop = (ast.statements[0] as FunctionStatement).func.body.statements[0] as ForStatement; + let exitStmt = loop.body.statements[0] as ExitStatement; + expect(isExitStatement(exitStmt)).to.be.true; + expect(exitStmt.tokens.loopType.text).to.eq('while'); + }); + + it('should allow identifiers named `exitfor` (one word)', () => { + let { ast, diagnostics } = parse(` + sub main() + for i = 1 to 10 + exitfor = 1 + exit for + end for + end sub + `); + expectZeroDiagnostics(diagnostics); + let loop = (ast.statements[0] as FunctionStatement).func.body.statements[0] as ForStatement; + let assignment = loop.body.statements[0] as AssignmentStatement; + expect(assignment.tokens.name.text).to.eq('exitfor'); + let exitStmt = loop.body.statements[1] as ExitStatement; + expect(isExitStatement(exitStmt)).to.be.true; + expect(exitStmt.tokens.loopType.text).to.eq('for'); + }); + }); }); export function parse(text: string, mode?: ParseMode, bsConsts: Record = {}) { diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index aad056dbb..f657fca71 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -35,8 +35,7 @@ import { EndStatement, EnumMemberStatement, EnumStatement, - ExitForStatement, - ExitWhileStatement, + ExitStatement, ExpressionStatement, ForEachStatement, FieldStatement, @@ -1108,8 +1107,8 @@ export class Parser { return this.whileStatement(); } - if (this.check(TokenKind.ExitWhile)) { - return this.exitWhile(); + if (this.checkAny(TokenKind.Exit, TokenKind.ExitWhile)) { + return this.exitStatement(); } if (this.check(TokenKind.For)) { @@ -1120,10 +1119,6 @@ export class Parser { return this.forEachStatement(); } - if (this.check(TokenKind.ExitFor)) { - return this.exitFor(); - } - if (this.check(TokenKind.End)) { return this.endStatement(); } @@ -1235,10 +1230,43 @@ export class Parser { }); } - private exitWhile(): ExitWhileStatement { - let keyword = this.advance(); + private exitStatement(): ExitStatement { + let exitToken = this.advance(); + if (exitToken.kind === TokenKind.ExitWhile) { + // `exitwhile` is allowed in code, and means `exit while` + // use an ExitStatement that is nicer to work with by breaking the `exit` and `while` tokens apart + + const exitText = exitToken.text.substring(0, 4); + const whileText = exitToken.text.substring(4); + const originalRange = exitToken.location.range; + const originalStart = originalRange.start; + + const exitRange = util.createRange( + originalStart.line, + originalStart.character, + originalStart.line, + originalStart.character + 4); + const whileRange = util.createRange( + originalStart.line, + originalStart.character + 4, + originalStart.line, + originalStart.character + exitToken.text.length); + + exitToken = createToken(TokenKind.Exit, exitText, util.createLocationFromRange(exitToken.location.uri, exitRange)); + this.tokens[this.current - 1] = exitToken; + const newLoopToken = createToken(TokenKind.While, whileText, util.createLocationFromRange(exitToken.location.uri, whileRange)); + this.tokens.splice(this.current, 0, newLoopToken); + } + + const loopTypeToken = this.tryConsume( + DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For), + TokenKind.While, TokenKind.For + ); - return new ExitWhileStatement({ exitWhile: keyword }); + return new ExitStatement({ + exit: exitToken, + loopType: loopTypeToken + }); } private forStatement(): ForStatement { @@ -1337,12 +1365,6 @@ export class Parser { }); } - private exitFor(): ExitForStatement { - let keyword = this.advance(); - - return new ExitForStatement({ exitFor: keyword }); - } - private namespaceStatement(): NamespaceStatement | undefined { this.warnIfNotBrighterScriptMode('namespace'); let keyword = this.advance(); diff --git a/src/parser/Statement.spec.ts b/src/parser/Statement.spec.ts index a4218b36c..0edb52ddb 100644 --- a/src/parser/Statement.spec.ts +++ b/src/parser/Statement.spec.ts @@ -1,6 +1,6 @@ import { expect } from '../chai-config.spec'; import { NamespaceStatement, ClassStatement } from './Statement'; -import { AssignmentStatement, Block, Body, CatchStatement, DottedSetStatement, EmptyStatement, EndStatement, ExitForStatement, ExitWhileStatement, ExpressionStatement, ForEachStatement, ForStatement, FunctionStatement, GotoStatement, IfStatement, ImportStatement, IncrementStatement, IndexedSetStatement, LabelStatement, LibraryStatement, PrintStatement, ReturnStatement, StopStatement, ThrowStatement, TryCatchStatement, WhileStatement } from './Statement'; +import { AssignmentStatement, Block, Body, CatchStatement, DottedSetStatement, EmptyStatement, EndStatement, ExitStatement, ExpressionStatement, ForEachStatement, ForStatement, FunctionStatement, GotoStatement, IfStatement, ImportStatement, IncrementStatement, IndexedSetStatement, LabelStatement, LibraryStatement, PrintStatement, ReturnStatement, StopStatement, ThrowStatement, TryCatchStatement, WhileStatement } from './Statement'; import { ParseMode, Parser } from './Parser'; import { WalkMode } from '../astUtils/visitors'; import { isClassStatement, isNamespaceStatement } from '../astUtils/reflection'; @@ -128,8 +128,8 @@ describe('Statement', () => { const assignment = new AssignmentStatement({ equals: undefined, name: ident, value: expr }); const block = new Block({ statements: [] }); const expression = new ExpressionStatement({ expression: expr }); - const exitFor = new ExitForStatement({ exitFor: token }); - const exitWhile = new ExitWhileStatement({ exitWhile: token }); + const exitFor = new ExitStatement({ exit: token, loopType: token }); + const exitWhile = new ExitStatement({ exit: token, loopType: token }); const funs = new FunctionStatement({ name: ident, func: new FunctionExpression({ diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 4305fa825..c828974e5 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -436,61 +436,37 @@ export class ExpressionStatement extends Statement { } } - -export class ExitForStatement extends Statement { - constructor(options?: { - exitFor?: Token; - }) { - super(); - this.tokens = { - exitFor: options?.exitFor - }; - this.location = this.tokens.exitFor?.location; - } - - public readonly tokens: { - readonly exitFor?: Token; - }; - - public readonly kind = AstNodeKind.ExitForStatement; - - public readonly location?: Location; - - transpile(state: BrsTranspileState) { - return this.tokens.exitFor ? state.transpileToken(this.tokens.exitFor) : ['exit for']; - } - - walk(visitor: WalkVisitor, options: WalkOptions) { - //nothing to walk - } - - get leadingTrivia(): Token[] { - return this.tokens.exitFor?.leadingTrivia; - } - -} - -export class ExitWhileStatement extends Statement { +export class ExitStatement extends Statement { constructor(options?: { - exitWhile?: Token; + exit?: Token; + loopType: Token; }) { super(); this.tokens = { - exitWhile: options?.exitWhile + exit: options?.exit, + loopType: options.loopType }; - this.location = this.tokens.exitWhile?.location; + this.location = util.createBoundingLocation( + this.tokens.exit, + this.tokens.loopType + ); } public readonly tokens: { - readonly exitWhile?: Token; + readonly exit: Token; + readonly loopType?: Token; }; - public readonly kind = AstNodeKind.ExitWhileStatement; + public readonly kind = AstNodeKind.ExitStatement; public readonly location?: Location; transpile(state: BrsTranspileState) { - return this.tokens.exitWhile ? state.transpileToken(this.tokens.exitWhile) : ['exit while']; + return [ + state.transpileToken(this.tokens.exit, 'exit'), + this.tokens.loopType?.leadingWhitespace ?? ' ', + state.transpileToken(this.tokens.loopType) + ]; } walk(visitor: WalkVisitor, options: WalkOptions) { @@ -498,7 +474,7 @@ export class ExitWhileStatement extends Statement { } get leadingTrivia(): Token[] { - return this.tokens.exitWhile?.leadingTrivia; + return this.tokens.exit?.leadingTrivia; } } diff --git a/src/parser/tests/statement/Misc.spec.ts b/src/parser/tests/statement/Misc.spec.ts index b46b94f00..d8a64576c 100644 --- a/src/parser/tests/statement/Misc.spec.ts +++ b/src/parser/tests/statement/Misc.spec.ts @@ -118,7 +118,6 @@ describe('parser', () => { TokenKind.EndWhile, TokenKind.Eval, TokenKind.Exit, - TokenKind.ExitFor, TokenKind.ExitWhile, TokenKind.False, TokenKind.For, diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts index cfbfecce3..2bfe14a37 100644 --- a/src/testHelpers.spec.ts +++ b/src/testHelpers.spec.ts @@ -7,7 +7,7 @@ import { expect } from './chai-config.spec'; import type { CodeActionShorthand } from './CodeActionUtil'; import { codeActionUtil } from './CodeActionUtil'; import type { BrsFile } from './files/BrsFile'; -import type { Program } from './Program'; +import { Program } from './Program'; import { standardizePath as s } from './util'; import { getDiagnosticLine } from './diagnosticUtils'; import { firstBy } from 'thenby'; @@ -175,11 +175,13 @@ export function expectZeroDiagnostics(arg: DiagnosticCollection) { diagnostic.message = diagnostic.message.replace(/\r/g, '\\r').replace(/\n/g, '\\n'); message += `\n • bs${diagnostic.code} "${diagnostic.message}" at ${diagnostic.location?.uri ?? ''}#(${diagnostic.location.range?.start.line}:${diagnostic.location.range?.start.character})-(${diagnostic.location.range?.end.line}:${diagnostic.location.range?.end.character})`; //print the line containing the error (if we can find it)srcPath - const file = (arg as any).getFile(diagnostic.location.uri); + if (arg instanceof Program) { + const file = arg.getFile(diagnostic.location.uri); - const line = (file as BrsFile)?.fileContents?.split(/\r?\n/g)?.[diagnostic.location.range?.start.line]; - if (line) { - message += '\n' + getDiagnosticLine(diagnostic, line, chalk.red); + const line = (file as BrsFile)?.fileContents?.split(/\r?\n/g)?.[diagnostic.location.range?.start.line]; + if (line) { + message += '\n' + getDiagnosticLine(diagnostic, line, chalk.red); + } } } assert.fail(message);