From 67a9ec48de583f2cff22a52fbb5ef8c236943923 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 27 Sep 2022 11:47:33 -0400 Subject: [PATCH 1/2] Add parse and transpile support for continue --- src/DiagnosticMessages.ts | 9 +- src/astUtils/reflection.ts | 5 +- src/astUtils/visitors.ts | 3 +- src/bscPlugin/validation/BrsFileValidator.ts | 43 ++++++++- src/lexer/Lexer.spec.ts | 9 ++ src/lexer/TokenKind.ts | 8 +- src/parser/Parser.ts | 18 ++++ src/parser/Statement.ts | 26 ++++++ src/parser/tests/statement/Continue.spec.ts | 98 ++++++++++++++++++++ 9 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 src/parser/tests/statement/Continue.spec.ts diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index f87cea00c..e5eed8ece 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -572,8 +572,8 @@ export let DiagnosticMessages = { code: 1108, severity: DiagnosticSeverity.Error }), - expectedToken: (tokenKind: string) => ({ - message: `Expected '${tokenKind}'`, + expectedToken: (...tokenKinds: string[]) => ({ + message: `Expected token '${tokenKinds.join(`' or '`)}'`, code: 1109, severity: DiagnosticSeverity.Error }), @@ -699,6 +699,11 @@ export let DiagnosticMessages = { message: `Expected directory depth no larger than 7, but found ${numberOfParentDirectories}.`, code: 1134, severity: DiagnosticSeverity.Error + }), + illegalContinueStatement: () => ({ + message: `Continue statement must be contained within a loop statement`, + code: 1135, + severity: DiagnosticSeverity.Error }) }; diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 00794af4a..48ec3dc0d 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -1,4 +1,4 @@ -import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassFieldStatement, ClassMethodStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, MethodStatement, FieldStatement, ConstStatement } from '../parser/Statement'; +import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassFieldStatement, ClassMethodStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement } from '../parser/Statement'; import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, NamespacedVariableNameExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression } from '../parser/Expression'; import type { BrsFile } from '../files/BrsFile'; import type { XmlFile } from '../files/XmlFile'; @@ -162,6 +162,9 @@ export function isEnumMemberStatement(element: AstNode | undefined): element is export function isConstStatement(element: AstNode | undefined): element is ConstStatement { return element?.constructor.name === 'ConstStatement'; } +export function isContinueStatement(element: AstNode | undefined): element is ContinueStatement { + return element?.constructor.name === 'ContinueStatement'; +} export function isTryCatchStatement(element: AstNode | undefined): element is TryCatchStatement { return element?.constructor.name === 'TryCatchStatement'; } diff --git a/src/astUtils/visitors.ts b/src/astUtils/visitors.ts index 4b0768cd8..a1dea6ca2 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, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, ClassMethodStatement, ClassFieldStatement, EnumStatement, EnumMemberStatement, DimStatement, TryCatchStatement, CatchStatement, ThrowStatement, InterfaceStatement, InterfaceFieldStatement, InterfaceMethodStatement, FieldStatement, MethodStatement, ConstStatement } from '../parser/Statement'; +import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, ClassMethodStatement, ClassFieldStatement, EnumStatement, EnumMemberStatement, DimStatement, TryCatchStatement, CatchStatement, ThrowStatement, InterfaceStatement, InterfaceFieldStatement, InterfaceMethodStatement, FieldStatement, MethodStatement, ConstStatement, ContinueStatement } from '../parser/Statement'; import type { AALiteralExpression, AAMemberExpression, AnnotationExpression, ArrayLiteralExpression, BinaryExpression, CallExpression, CallfuncExpression, DottedGetExpression, EscapedCharCodeLiteralExpression, FunctionExpression, FunctionParameterExpression, GroupingExpression, IndexedGetExpression, LiteralExpression, NamespacedVariableNameExpression, NewExpression, NullCoalescingExpression, RegexLiteralExpression, SourceLiteralExpression, TaggedTemplateStringExpression, TemplateStringExpression, TemplateStringQuasiExpression, TernaryExpression, UnaryExpression, VariableExpression, XmlAttributeGetExpression } from '../parser/Expression'; import { isExpression, isStatement } from './reflection'; import type { AstEditor } from './AstEditor'; @@ -127,6 +127,7 @@ export function createVisitor( * @deprecated use `FieldStatement` */ ClassFieldStatement?: (statement: ClassFieldStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; + ContinueStatement?: (statement: ContinueStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; MethodStatement?: (statement: MethodStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; FieldStatement?: (statement: FieldStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; TryCatchStatement?: (statement: TryCatchStatement, parent?: Statement, owner?: any, key?: any) => Statement | void; diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index b9ead4060..b828ccf97 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -1,13 +1,14 @@ -import { isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isEnumStatement, isFunctionStatement, isImportStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespacedVariableNameExpression, isNamespaceStatement } from '../../astUtils/reflection'; +import { isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isEnumStatement, isForEachStatement, isForStatement, isFunctionStatement, isImportStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespacedVariableNameExpression, isNamespaceStatement, isWhileStatement } from '../../astUtils/reflection'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import { DiagnosticMessages } from '../../DiagnosticMessages'; import type { BrsFile } from '../../files/BrsFile'; import type { OnFileValidateEvent } from '../../interfaces'; +import { Token } from '../../lexer/Token'; import { TokenKind } from '../../lexer/TokenKind'; import type { AstNode } from '../../parser/AstNode'; import type { LiteralExpression } from '../../parser/Expression'; import { ParseMode } from '../../parser/Parser'; -import type { EnumMemberStatement, EnumStatement, ImportStatement, LibraryStatement } from '../../parser/Statement'; +import { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, WhileStatement } from '../../parser/Statement'; import { DynamicType } from '../../types/DynamicType'; import util from '../../util'; @@ -146,6 +147,9 @@ export class BrsFileValidator { if (node.identifier) { node.parent.getSymbolTable().addSymbol(node.identifier.text, node.identifier.range, DynamicType.instance); } + }, + ContinueStatement: (node) => { + this.validateContinueStatement(node); } }); @@ -299,4 +303,39 @@ export class BrsFileValidator { } } } + + private validateContinueStatement(statement: ContinueStatement) { + const validateLoopTypeMatch = (loopType: TokenKind) => { + //coerce ForEach to For + loopType = loopType === TokenKind.ForEach ? TokenKind.For : loopType; + + if (loopType?.toLowerCase() !== statement.tokens.loopType.text?.toLowerCase()) { + this.event.file.addDiagnostic({ + range: statement.tokens.loopType.range, + ...DiagnosticMessages.expectedToken(loopType) + }); + } + }; + + //find the parent loop statement + const parent = statement.findAncestor((node) => { + if (isWhileStatement(node)) { + validateLoopTypeMatch(node.tokens.while.kind); + return true; + } else if (isForStatement(node)) { + validateLoopTypeMatch(node.forToken.kind); + return true; + } else if (isForEachStatement(node)) { + validateLoopTypeMatch(node.tokens.forEach.kind); + return true; + } + }); + //flag continue statements found outside of a loop + if (!parent) { + this.event.file.addDiagnostic({ + range: statement.range, + ...DiagnosticMessages.illegalContinueStatement() + }); + } + } } diff --git a/src/lexer/Lexer.spec.ts b/src/lexer/Lexer.spec.ts index 7e7c2686c..33e525d7f 100644 --- a/src/lexer/Lexer.spec.ts +++ b/src/lexer/Lexer.spec.ts @@ -1368,6 +1368,15 @@ describe('lexer', () => { ); }); }); + + it('detects "continue" as a keyword', () => { + expect( + Lexer.scan('continue').tokens.map(x => x.kind) + ).to.eql([ + TokenKind.Continue, + TokenKind.Eof + ]); + }); }); function expectKinds(text: string, tokenKinds: TokenKind[]) { diff --git a/src/lexer/TokenKind.ts b/src/lexer/TokenKind.ts index b036885ef..c6ea51517 100644 --- a/src/lexer/TokenKind.ts +++ b/src/lexer/TokenKind.ts @@ -161,6 +161,7 @@ export enum TokenKind { Import = 'Import', EndInterface = 'EndInterface', Const = 'Const', + Continue = 'Continue', //brighterscript source literals LineNumLiteral = 'LineNumLiteral', @@ -238,6 +239,7 @@ export const ReservedWords = new Set([ export const Keywords: Record = { as: TokenKind.As, and: TokenKind.And, + continue: TokenKind.Continue, dim: TokenKind.Dim, end: TokenKind.End, then: TokenKind.Then, @@ -441,7 +443,8 @@ export const AllowedProperties = [ TokenKind.EndTry, TokenKind.Throw, TokenKind.EndInterface, - TokenKind.Const + TokenKind.Const, + TokenKind.Continue ]; /** List of TokenKind that are allowed as local var identifiers. */ @@ -474,7 +477,8 @@ export const AllowedLocalIdentifiers = [ TokenKind.Try, TokenKind.Catch, TokenKind.EndTry, - TokenKind.Const + TokenKind.Const, + TokenKind.Continue ]; export const BrighterScriptSourceLiterals = [ diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 0d0007173..41915686f 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -21,6 +21,7 @@ import { Block, Body, CatchStatement, + ContinueStatement, ClassStatement, ConstStatement, CommentStatement, @@ -1109,6 +1110,10 @@ export class Parser { return this.gotoStatement(); } + if (this.check(TokenKind.Continue)) { + return this.continueStatement(); + } + //does this line look like a label? (i.e. `someIdentifier:` ) if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) { try { @@ -2097,6 +2102,19 @@ export class Parser { return new LabelStatement(tokens); } + /** + * Parses a `continue` statement + */ + private continueStatement() { + return new ContinueStatement({ + continue: this.advance(), + loopType: this.tryConsume( + DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For), + TokenKind.While, TokenKind.For + ) + }); + } + /** * Parses a `goto` statement * @returns an AST representation of an `goto` statement. diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 4133ef2d9..7ad82b9a7 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -2511,3 +2511,29 @@ export class ConstStatement extends Statement implements TypedefProvider { } } } + +export class ContinueStatement extends Statement { + constructor( + public tokens: { + continue: Token; + loopType: Token; + } + ) { + super(); + } + + public get range() { + return this.tokens.continue.range; + } + + transpile(state: BrsTranspileState) { + return [ + state.sourceNode(this.tokens.continue, this.tokens.continue?.text ?? 'continue'), + this.tokens.loopType?.leadingWhitespace ?? ' ', + state.sourceNode(this.tokens.continue, this.tokens.loopType?.text) + ]; + } + walk(visitor: WalkVisitor, options: WalkOptions) { + //nothing to walk + } +} diff --git a/src/parser/tests/statement/Continue.spec.ts b/src/parser/tests/statement/Continue.spec.ts new file mode 100644 index 000000000..f75cb5696 --- /dev/null +++ b/src/parser/tests/statement/Continue.spec.ts @@ -0,0 +1,98 @@ +import { expect } from 'chai'; +import { createSandbox } from 'sinon'; +import { isContinueStatement } from '../../../astUtils/reflection'; +import { DiagnosticMessages } from '../../../DiagnosticMessages'; +import { TokenKind } from '../../../lexer/TokenKind'; +import { Program } from '../../../Program'; +import { expectDiagnostics, expectZeroDiagnostics, getTestTranspile } from '../../../testHelpers.spec'; +import { standardizePath as s } from '../../../util'; +import type { BrsFile } from '../../../files/BrsFile'; +const sinon = createSandbox(); + +describe('parser continue statements', () => { + let rootDir = s`${process.cwd()}/.tmp/rootDir`; + let program: Program; + let testTranspile = getTestTranspile(() => [program, rootDir]); + + beforeEach(() => { + program = new Program({ rootDir: rootDir, sourceMap: true }); + }); + afterEach(() => { + sinon.restore(); + program.dispose(); + }); + + it('parses standalone statement properly', () => { + const file = program.setFile('source/main.bs', ` + sub main() + for i = 0 to 10 + continue for + end for + end sub + `); + expectZeroDiagnostics(program); + expect(file.ast.findChild(isContinueStatement)).to.exist; + }); + + it('flags incorrect loop type', () => { + const file = program.setFile('source/main.bs', ` + sub main() + for i = 0 to 10 + continue while + end for + for each item in [1, 2, 3] + continue while + end for + while true + continue for + end while + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.expectedToken(TokenKind.For), + DiagnosticMessages.expectedToken(TokenKind.For), + DiagnosticMessages.expectedToken(TokenKind.While) + ]); + expect(file.ast.findChild(isContinueStatement)).to.exist; + }); + + it('flags missing `for` or `while` but still creates the node', () => { + const file = program.setFile('source/main.bs', ` + sub main() + for i = 0 to 10 + continue + end for + end sub + `); + expectDiagnostics(program, [ + DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For) + ]); + expect(file.ast.findChild(isContinueStatement)).to.exist; + }); + + it('detects `continue` used outside of a loop', () => { + program.setFile('source/main.bs', ` + sub main() + continue for + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.illegalContinueStatement().message + ]); + }); + + it('transpiles properly', () => { + testTranspile(` + sub main() + while true + continue while + end while + for i = 0 to 10 + continue for + end for + end sub + `); + }); +}); From 3ed8e15929c8aed4287a17aea167907eac252bb2 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Tue, 27 Sep 2022 11:49:42 -0400 Subject: [PATCH 2/2] fix lint issues --- src/bscPlugin/validation/BrsFileValidator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index b828ccf97..1be5816d4 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -3,12 +3,11 @@ import { createVisitor, WalkMode } from '../../astUtils/visitors'; import { DiagnosticMessages } from '../../DiagnosticMessages'; import type { BrsFile } from '../../files/BrsFile'; import type { OnFileValidateEvent } from '../../interfaces'; -import { Token } from '../../lexer/Token'; import { TokenKind } from '../../lexer/TokenKind'; import type { AstNode } from '../../parser/AstNode'; import type { LiteralExpression } from '../../parser/Expression'; import { ParseMode } from '../../parser/Parser'; -import { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, WhileStatement } from '../../parser/Statement'; +import type { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, WhileStatement } from '../../parser/Statement'; import { DynamicType } from '../../types/DynamicType'; import util from '../../util';