├── .tslintignore ├── docs ├── tests.png └── tokei.png ├── test ├── assets │ ├── library.brs │ ├── functions.brs │ ├── docblock.brs │ ├── syntax.brs │ ├── rodash.brs │ └── weird.brs ├── .eslintrc ├── assets.spec.ts ├── diagram.spec.ts ├── errors.spec.ts ├── location.spec.ts ├── helpers.ts └── ast.spec.ts ├── .gitignore ├── tsconfig.jest.json ├── tslint.json ├── .editorconfig ├── .travis.yml ├── .circleci └── config.yml ├── jest.config.js ├── tsconfig.json ├── LICENSE ├── src ├── main.ts ├── AST.d.ts ├── ASTWalker.d.ts ├── VisitorKeys.ts ├── BaseVisitor.ts ├── BaseASTWalker.ts ├── Tokens.ts ├── Parser.ts └── ASTVisitor.ts ├── package.json └── README.md /.tslintignore: -------------------------------------------------------------------------------- 1 | *.d.ts -------------------------------------------------------------------------------- /docs/tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RokuRoad/bright/HEAD/docs/tests.png -------------------------------------------------------------------------------- /docs/tokei.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RokuRoad/bright/HEAD/docs/tokei.png -------------------------------------------------------------------------------- /test/assets/library.brs: -------------------------------------------------------------------------------- 1 | ' Loading libraries 2 | Library "lib1" 3 | library "lib1" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __snapshots__ 2 | coverage 3 | node_modules 4 | .vscode 5 | .DS_Store 6 | dist 7 | assets 8 | -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "linterOptions": {}, 4 | "rules": { 5 | "interface-name": [true, "never-prefix"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:roku/recommended", 3 | "rules": { 4 | "roku/no-print": "warn", 5 | "roku/no-stop": "warn", 6 | "roku/sub-to-function": "off", 7 | "roku/function-no-return": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | charset = utf-8 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [Makefile] 17 | indent_style = tab -------------------------------------------------------------------------------- /test/assets.spec.ts: -------------------------------------------------------------------------------- 1 | import { basename } from 'path' 2 | import { files, scanFile } from './helpers' 3 | 4 | describe.skip('Asset tests', () => { 5 | files(__dirname + '/assets/**/*.brs').forEach((path: string) => { 6 | test(basename(path, '.brs'), () => { 7 | expect(scanFile(path)).toBeTruthy() 8 | }) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10" 5 | 6 | sudo: false 7 | 8 | before_install: 9 | - curl -o- -L https://yarnpkg.com/install.sh | bash 10 | - export PATH="$HOME/.yarn/bin:$PATH" 11 | 12 | cache: 13 | yarn: true 14 | directories: 15 | - ".eslintcache" 16 | - "node_modules" 17 | 18 | script: 19 | - npm install codecov -g 20 | - yarn run coverage 21 | - yarn run build 22 | 23 | after_success: 24 | - codecov 25 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/node:10 7 | working_directory: ~/repo 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | key: v1-dependencies-{{ checksum "yarn.lock" }} 12 | - run: yarn 13 | - save_cache: 14 | key: v1-dependencies-{{ checksum "yarn.lock" }} 15 | paths: 16 | - node_modules 17 | - run: yarn build 18 | - run: yarn coverage 19 | - run: yarn release 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | testRegex: '(/test/.*|(\\.|/)(test|spec))\\.tsx?$', 8 | moduleFileExtensions: [ 'ts', 'tsx', 'js', 'jsx', 'json', 'node' ], 9 | verbose: true, 10 | bail: true, 11 | collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts' ], 12 | globals: { 13 | 'ts-jest': { 14 | tsConfigFile: path.resolve(path.join(__dirname, 'tsconfig.jest.json')), 15 | skipBabel: true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/assets/functions.brs: -------------------------------------------------------------------------------- 1 | function f1() 2 | 'empty function 3 | end function 4 | 5 | function f2() 6 | 'empty function 7 | end function 8 | 9 | 10 | function f3() as void 11 | 'empty function 12 | end function 13 | 14 | function f4() as dynamic 15 | return {} 16 | end function 17 | 18 | function f5() as integer 19 | return 5 20 | end function 21 | 22 | function f6(p1 as integer) as integer 23 | return 5 + p1 24 | end function 25 | 26 | function f6(p1 = 5 as integer) as integer 27 | return 5 + p1 28 | end function -------------------------------------------------------------------------------- /test/assets/docblock.brs: -------------------------------------------------------------------------------- 1 | ' /** 2 | ' * @member get 3 | ' * @memberof module:rodash 4 | ' * @instance 5 | ' * @description 6 | ' * Resolve a nested 'dot' notation path safely. This is primarily allow things like 7 | ' * "myValue = a.b.c.d.e.f" to run without crashing the VM when an intermediate value is invalid. 8 | ' * 9 | ' * @example 10 | ' * 11 | ' * data = { a: 1 b: { c: 2 d: { e: 3 f: [4, 5] } } } 12 | ' * value = _.get(data, ["b","d","f"]) 13 | ' * ' => [4, 5] 14 | ' * 15 | ' * value = _.get(data, "b.d.f[0]") 16 | ' * ' => 4 17 | ' * 18 | ' * value = _.get(data, "a[0]") 19 | ' * ' => invalid 20 | ' * 21 | ' */ -------------------------------------------------------------------------------- /test/diagram.spec.ts: -------------------------------------------------------------------------------- 1 | import { createSyntaxDiagramsCode } from 'chevrotain' 2 | import { writeFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | 5 | import { parserInstance } from '../src/Parser' 6 | 7 | describe('Diagrams', () => { 8 | test('Generate AST Productions', () => { 9 | // extract the serialized parser. 10 | const serializedGrammar = parserInstance.getSerializedGastProductions() 11 | 12 | // create the HTML Text 13 | const htmlText = createSyntaxDiagramsCode(serializedGrammar) 14 | 15 | // Write the HTML file to disk 16 | const outPath = resolve(__dirname, '../diagram') 17 | 18 | writeFileSync(outPath + '/index.html', htmlText) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "resolveJsonModule": true, 5 | "suppressImplicitAnyIndexErrors": true, 6 | "allowUnreachableCode": false, 7 | "declaration": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "lib": [ 11 | "dom", 12 | "es6", 13 | "es2015" 14 | ], 15 | "module": "commonjs", 16 | "outDir": "dist", 17 | "moduleResolution": "node", 18 | "noImplicitAny": false, 19 | "noImplicitReturns": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "pretty": true, 23 | "removeComments": true, 24 | "sourceMap": false, 25 | "target": "es6", 26 | "typeRoots": [ 27 | "node_modules/@types" 28 | ] 29 | }, 30 | "typeAcquisition": { 31 | "enable": true 32 | }, 33 | "include": [ 34 | "src" 35 | ] 36 | } -------------------------------------------------------------------------------- /test/errors.spec.ts: -------------------------------------------------------------------------------- 1 | import { ast, parse } from '../src/main' 2 | 3 | import { error } from './helpers' 4 | 5 | describe('Some Parsing errors', () => { 6 | it('Should be able to find parser errors in program', () => { 7 | const { parseErrors } = parse('Lbrry "ads"') 8 | 9 | expect(parseErrors.length).toEqual(1) 10 | expect(error(parseErrors[0], 'Lbrry "ads"')).toBeTruthy() 11 | }) 12 | 13 | it('Should be able to find parser errors in function', () => { 14 | const { parseErrors } = parse(`function a end function`) 15 | 16 | expect(parseErrors.length).toEqual(1) 17 | expect(error(parseErrors[0], `function a end function`)).toBeTruthy() 18 | }) 19 | 20 | it('Should throw an exception', () => { 21 | expect(() => { 22 | const { parseErrors } = ast(`function a end function`) 23 | 24 | expect(parseErrors.length).toEqual(1) 25 | expect(error(parseErrors[0], `function a end function`)).toBeTruthy() 26 | }).toThrow() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/location.spec.ts: -------------------------------------------------------------------------------- 1 | import { sourceAST } from './helpers' 2 | 3 | describe('Location tests', () => { 4 | test('Should identify location of statement', () => { 5 | expect(() => { 6 | const ast = sourceAST(`print a, b, c`, 'BlockStatement') 7 | const ast2 = sourceAST(`? a, b, c`, 'BlockStatement') 8 | 9 | expect(ast.range).toEqual([0, 13]) 10 | expect(ast2.range).toEqual([0, 9]) 11 | }).not.toThrow() 12 | }) 13 | 14 | test('Should identify location of multiline statement', () => { 15 | expect(() => { 16 | const ast = sourceAST(['function gotoStatement()', ' mylabel:', ' print "Anthony was here!"', '', ' goto mylabel', 'end function', ''].join('\n')) 17 | expect(ast.range).toEqual([0, 92]) 18 | }).not.toThrow() 19 | }) 20 | 21 | test('Should find chains', () => { 22 | expect(() => { 23 | const ast = sourceAST('logUtil().debug("Writing hideScores:{0} to registry", v)', 'BlockStatement') 24 | expect(ast.range).toEqual([0, 56]) 25 | }).not.toThrow() 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Igor Alpert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ASTVisitor } from './ASTVisitor' 2 | import { BaseASTWalker } from './BaseASTWalker' 3 | import { parse, RokuBRSParser } from './Parser' 4 | import { ALL_TOKENS } from './Tokens' 5 | 6 | import { visitorKeys } from './VisitorKeys' 7 | 8 | const visitor = new ASTVisitor() 9 | 10 | /** 11 | * Build an AST from source 12 | * @param source BrightScript sources 13 | * @param type Node to start with 14 | */ 15 | const ast = (source: string, type = 'Program') => { 16 | const { value, tokens, parseErrors, lexErrors } = parse(source, type) 17 | const props = { tokens, parseErrors, lexErrors } 18 | 19 | const tree = visitor.visit(value, props) 20 | 21 | if (parseErrors.length) { 22 | const { message, location } = parseErrors[0] 23 | 24 | const column = location.start.column 25 | const line = location.start.line 26 | 27 | const err = new SyntaxError(`${message} at ${line}:${column}`) 28 | 29 | err.lineNumber = line 30 | err.column = column 31 | 32 | throw err 33 | } 34 | 35 | return tree 36 | } 37 | 38 | export { RokuBRSParser, ASTVisitor, ALL_TOKENS, parse, ast, visitorKeys, BaseASTWalker } 39 | -------------------------------------------------------------------------------- /src/AST.d.ts: -------------------------------------------------------------------------------- 1 | export interface ASTNode extends Location { 2 | type: string 3 | trailing?: ASTNode[] | ASTNode | string 4 | 5 | [key: string]: any 6 | } 7 | 8 | export interface BinaryASTNode extends ASTNode { 9 | left: ASTNode 10 | right: ASTNode 11 | operator: ASTNode 12 | } 13 | 14 | export interface ContextProps { 15 | tokens: Token[] 16 | } 17 | 18 | export interface Token extends Location { 19 | type: string 20 | value: number | string 21 | } 22 | 23 | export interface NodeContext { 24 | [key: string]: any 25 | } 26 | 27 | export interface ProgramNode extends NodeContext { 28 | Declaration: ASTNode[] 29 | Empty: ASTNode[] 30 | } 31 | 32 | export interface TokenContext { 33 | startLine: number 34 | startColumn: number 35 | image: string 36 | endLine: number 37 | endColumn: number 38 | startOffset: number 39 | endOffset: number 40 | tokenType: { 41 | tokenName: string 42 | } 43 | } 44 | 45 | export interface Location { 46 | loc: { 47 | start: Position 48 | end: Position 49 | source?: string 50 | } 51 | range?: [number, number] 52 | } 53 | export interface Position { 54 | line: number 55 | column: number 56 | } 57 | 58 | declare global { 59 | interface SyntaxError { 60 | lineNumber: number 61 | column: number 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@roku-road/bright", 3 | "version": "1.3.1", 4 | "public": true, 5 | "description": "Blazing fast parser for BrightScript that gives you ESTree like AST", 6 | "repository": "git@github.com:RokuRoad/bright.git", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "tsc", 10 | "lint": "tslint -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", 11 | "test": "jest", 12 | "release": "tsc && semantic-release --no-ci", 13 | "prepublishOnly": "tsc", 14 | "coverage": "jest --coverage" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "keywords": [ 20 | "parser", 21 | "brs", 22 | "brightscript", 23 | "ast", 24 | "cst", 25 | "syntax", 26 | "roku-development", 27 | "grammar", 28 | "roku", 29 | "language" 30 | ], 31 | "main": "dist/main.js", 32 | "files": [ 33 | "dist/*.js" 34 | ], 35 | "devDependencies": { 36 | "@types/jest": "23.3.2", 37 | "@types/lodash": "4.14.112", 38 | "@types/node": "^11.9.0", 39 | "babel-code-frame": "6.26.0", 40 | "glob-all": "3.1.0", 41 | "jest": "23.1.0", 42 | "jest-mock": "23.1.0", 43 | "semantic-release": "15.7.2", 44 | "traverse": "0.6.6", 45 | "ts-jest": "23.1.4", 46 | "tslib": "^1.9.3", 47 | "tslint": "^5.10.0", 48 | "tslint-config-prettier": "^1.13.0", 49 | "typescript": "2.9.2" 50 | }, 51 | "dependencies": { 52 | "chevrotain": "3.7.2", 53 | "lodash": "4.17.12" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ASTWalker.d.ts: -------------------------------------------------------------------------------- 1 | export interface ASTWalker { 2 | Program(ctx): ASTNode 3 | LibraryStatement(ctx): ASTNode 4 | UnTypedIdentifier({ name }): ASTNode 5 | STRING_LITERAL(ctx): ASTNode 6 | ParameterList(ctx): ASTNode 7 | AdditionExpression(ctx): ASTNode 8 | AssignmentExpression(ctx): ASTNode 9 | MultiplicationExpression(ctx): ASTNode 10 | LogicExpression(ctx): ASTNode 11 | ParenthesisExpression(ctx): ASTNode 12 | ArrayElement(ctx): ASTNode 13 | ArrayExpression(ctx): ASTNode 14 | CallExpression(ctx): ASTNode 15 | ConditionalConst(ctx): ASTNode 16 | ConditionalElseIfStatement(ctx): ASTNode 17 | ConditionalElseStatement(ctx): ASTNode 18 | ConditionalError(ctx): ASTNode 19 | ConditionalIfStatement(ctx): ASTNode 20 | DimStatement(ctx): ASTNode 21 | DotMemberExpression(ctx): ASTNode 22 | ElseIfStatement(ctx): ASTNode 23 | ElseStatement(ctx): ASTNode 24 | EmptyStatement(ctx): ASTNode 25 | Comment({ value, trailingComments }): ASTNode 26 | ForEachStatement(ctx): ASTNode 27 | ForStatement(ctx): ASTNode 28 | FunctionDeclaration(ctx): ASTNode 29 | FunctionExpression(ctx): ASTNode 30 | GoToStatement(ctx): ASTNode 31 | IfStatement(ctx): ASTNode 32 | Literal({ value }): ASTNode 33 | MemberExpression(ctx): ASTNode 34 | NextStatement(ctx): ASTNode 35 | PostfixExpression(ctx): ASTNode 36 | PrintStatement(ctx): ASTNode 37 | RelationExpression(ctx): ASTNode 38 | ReturnStatement(ctx): ASTNode 39 | StopStatement(ctx): ASTNode 40 | SubDeclaration(ctx): ASTNode 41 | SubExpression(ctx): ASTNode 42 | UnaryExpression(ctx): ASTNode 43 | WhileStatement(ctx): ASTNode 44 | BlockStatement(ctx): ASTNode 45 | TypeAnnotation({ value }): ASTNode 46 | ObjectExpression(ctx): ASTNode 47 | Parameter(ctx): ASTNode 48 | Identifier(ctx): ASTNode 49 | Property(ctx): ASTNode 50 | Arguments(ctx): ASTNode 51 | visit(ctx): { [key: string]: ASTNode | ASTNode[] } 52 | asArray(value: ASTNode[] | ASTNode): ASTNode[] 53 | binary(ctx): ASTNode[] 54 | mergeOperands(from: ASTNode[], to: ASTNode[], dividers: ASTNode[]): ASTNode[] 55 | } 56 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { codeFrameColumns } from '@babel/code-frame' 2 | import { readFileSync } from 'fs' 3 | import { sync } from 'glob' 4 | import { inspect } from 'util' 5 | import { ast, parse } from '../src/main' 6 | /** 7 | * Read source code and produce parsed information 8 | * 9 | * @param source Brightscript code 10 | * @param type Entry point for the parser. Default is Program that means .brs file 11 | * 12 | * @returns object with value, tokens and errors 13 | */ 14 | export const scanSource = (source: string, type = 'Program') => { 15 | const { value, lexErrors, tokens, parseErrors } = parse(source, type) 16 | 17 | expect(lexErrors.length).toEqual(0) 18 | expect(parseErrors.length).toEqual(0) 19 | 20 | expect(tokens).toBeTruthy() 21 | expect(value).toBeTruthy() 22 | 23 | return { value, lexErrors, tokens, parseErrors } 24 | } 25 | 26 | /** 27 | * Build AST tree from the file 28 | * @param path Path to BrightScript file 29 | */ 30 | export const fileAST = (path: string, type = 'Program') => { 31 | const source = readFileSync(path, 'utf8') 32 | return sourceAST(source, type) 33 | } 34 | 35 | /** 36 | * Build AST tree from the source 37 | * @param path Path to BrightScript file 38 | */ 39 | export const sourceAST = (source: string, type = 'Program') => { 40 | return ast(source, type) 41 | } 42 | 43 | /** 44 | * Read source code file and pass source to scanSource() 45 | * 46 | * @param path File path 47 | * 48 | * @returns object with value, tokens and errors 49 | */ 50 | export const scanFile = (path: string) => { 51 | const source = readFileSync(path, 'utf8') 52 | 53 | return scanSource(source) 54 | } 55 | 56 | /** 57 | * Returns list of files using pattern 58 | * @param path Pattern for glob 59 | */ 60 | export const files = (path: string) => sync(path) 61 | 62 | /** 63 | * Renders code frame with error location 64 | * @param err Error object in form of Parser error 65 | * @param location 66 | */ 67 | export const error = ({ message, location }, source: string): string => { 68 | return codeFrameColumns(source, location, { highlightCode: true, message }) 69 | } 70 | 71 | export const print = (value: any) => { 72 | // tslint:disable-next-line:no-console 73 | console.log(inspect(value, false, null, true)) 74 | } 75 | 76 | describe('Testing utils', () => { 77 | test('Helpers exists', () => { 78 | expect(scanFile).toBeTruthy() 79 | expect(scanSource).toBeTruthy() 80 | expect(files).toBeTruthy() 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/VisitorKeys.ts: -------------------------------------------------------------------------------- 1 | import { mapValues } from "lodash"; 2 | import * as TOKENS from "./Tokens"; 3 | 4 | const IGNORED_TOKENS = mapValues(TOKENS, () => []); 5 | 6 | const visitorKeys = { 7 | ...IGNORED_TOKENS, 8 | AdditionExpression: ["left", "right", "operator", "trailingComments"], 9 | Arguments: ["param"], 10 | ArrayElement: ["value", "trailingComments"], 11 | ArrayExpression: ["elements", "trailingComments"], 12 | AssignmentExpression: ["left", "right", "operator"], 13 | BlockStatement: ["body"], 14 | CallExpression: ["args", "callee"], 15 | Comment: [], 16 | ConditionalConst: ["operator", "left", "right"], 17 | ConditionalElseIfStatement: ["alternate", "body", "test"], 18 | ConditionalElseStatement: ["body"], 19 | ConditionalError: ["error"], 20 | ConditionalIfStatement: ["alternate", "body", "test", "trailingComments"], 21 | DimStatement: ["id", "ArrayExpression"], 22 | DotMemberExpression: ["operator", "right"], 23 | ElseIfStatement: ["test", "body", "trailingComments"], 24 | ElseStatement: ["body", "trailingComments"], 25 | EmptyStatement: ["trailingComments"], 26 | ForEachStatement: ["countExpression", "counter", "body", "trailingComments"], 27 | ForStatement: ["init", "test", "update", "body", "trailingComments"], 28 | FunctionDeclaration: [ 29 | "id", 30 | "ReturnType", 31 | "params", 32 | "body", 33 | "trailingComments", 34 | ], 35 | FunctionExpression: ["body", "params", "ReturnType", "trailingComments"], 36 | GoToStatement: ["id"], 37 | Identifier: ["asType", "name"], 38 | IfStatement: ["test", "consequent", "alternate", "trailingComments"], 39 | LibraryStatement: ["path"], 40 | Literal: ["raw", "value"], 41 | LogicExpression: ["operator", "left", "right"], 42 | MemberExpression: ["computed", "object", "properties"], 43 | MultiplicationExpression: ["operator", "left", "right"], 44 | NextStatement: ["trailingComments"], 45 | ObjectExpression: ["properties", "trailingComments"], 46 | Parameter: ["name", "TypeAnnotation", "value"], 47 | ParameterList: ["args"], 48 | ParenthesisExpression: ["expression"], 49 | PostfixExpression: ["operator", "argument"], 50 | PrintStatement: ["value", "trailingComments"], 51 | Program: ["body"], 52 | Property: ["key", "value"], 53 | RelationExpression: ["left", "right", "operator"], 54 | ReturnStatement: ["argument", "trailingComments"], 55 | StopStatement: ["trailingComments"], 56 | SubDeclaration: ["id", "params", "body", "ReturnType", "trailingComments"], 57 | SubExpression: ["body", "params", "trailingComments"], 58 | TypeAnnotation: [], 59 | UnTypedIdentifier: ["name"], 60 | UnaryExpression: ["operator", "argument"], 61 | WhileStatement: ["test", "body"], 62 | RokuTryStatement: ["body", "trailingComments", "exception", "onError"], 63 | }; 64 | 65 | export { visitorKeys }; 66 | -------------------------------------------------------------------------------- /test/ast.spec.ts: -------------------------------------------------------------------------------- 1 | import { fileAST, sourceAST /* , print */ } from "./helpers"; 2 | 3 | describe("AST", () => { 4 | test("Should be able to Parse AST", () => { 5 | expect(() => { 6 | fileAST(__dirname + "/assets/rodash.brs"); 7 | }).not.toThrow(); 8 | }); 9 | 10 | test("Should be able to walk AST", () => { 11 | expect(() => { 12 | fileAST(__dirname + "/assets/syntax.brs"); 13 | }).not.toThrow(); 14 | }); 15 | 16 | test("Should be able parse inline AST #1", () => { 17 | expect(() => { 18 | sourceAST("c[x, y, z] = k", "BlockStatement"); 19 | }).not.toThrow(); 20 | }); 21 | 22 | test("Should be able parse inline AST #2", () => { 23 | expect(() => { 24 | sourceAST('Library "lib1"'); 25 | }).not.toThrow(); 26 | }); 27 | 28 | test("Should be able parse inline AST with comments", () => { 29 | expect(() => { 30 | sourceAST(` 31 | ' /** 32 | ' * @member intersection 33 | ' * @memberof module:rodash 34 | ' * @instance 35 | ' * @description Return a new array of items from the first which are also in the second. 36 | ' * @param {Array} first 37 | ' * @param {Array} second 38 | ' * @example 39 | ' * 40 | ' * intersection = _.intersection([1,2], [2]) 41 | ' * ' => [2] 42 | ' */ 43 | Function rodash_intersection_(first, second) '1 44 | result = [] '2 45 | 46 | a = { 47 | c: 4 ' 55 48 | e: "dd" ' 66 49 | } 50 | 51 | for each f in first '3 52 | for each s in second '4 53 | if m.equal(s,f) then result.push(f) '5 54 | end for '6 55 | end for '7 56 | return result '8 57 | End Function '9 58 | `); 59 | 60 | // console.log(inspect(ast, false, null, true)) 61 | }).not.toThrow(); 62 | }); 63 | 64 | test("Should be able parse inline AST #3", () => { 65 | expect(() => { 66 | /* const ast = */ sourceAST('m.top.findNode("label")', "BlockStatement"); 67 | 68 | // print(ast) 69 | }).not.toThrow(); 70 | }); 71 | 72 | test("Should be able parse reserved words", () => { 73 | expect(() => { 74 | sourceAST( 75 | ` 76 | m.stop.findNode("label") 77 | 78 | a = { 79 | in:5 80 | stop:10 81 | mod: 5 82 | next: function(mod) 83 | print b 84 | end function 85 | } 86 | 87 | mod = "stop" 88 | 89 | `, 90 | "BlockStatement" 91 | ); 92 | }).not.toThrow(); 93 | }); 94 | test("Should be able parse try catch", () => { 95 | expect(() => { 96 | sourceAST( 97 | `try 98 | m.stop.findNode("label") 99 | 100 | a = { 101 | in:5 102 | stop:10 103 | mod: 5 104 | next: function(mod) 105 | print b 106 | end function 107 | } 108 | 109 | mod = "stop" 110 | catch e 111 | end try`, 112 | "RokuTryStatement" 113 | ); 114 | }).not.toThrow(); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/assets/syntax.brs: -------------------------------------------------------------------------------- 1 | function a() 2 | a$ = "a rose is a rose" 3 | b1 = 1.23 4 | x = x - z1 5 | 6 | aa = { joe: 10, fred: 11, sue: 9 } 7 | 8 | for each n In aa 9 | print n;aa[n] 10 | aa.delete(n) 11 | end for 12 | end function 13 | 14 | 15 | function gotoStatement() 16 | mylabel: 17 | print "Anthony was here!" 18 | goto mylabel 19 | end function 20 | 21 | function whileStatement() 22 | k = 0 23 | while k = 0 24 | k = 1 25 | print "loop once" 26 | end while 27 | 28 | while true 29 | print "loop once" 30 | if k <> 0 then exit while 31 | end while 32 | end function 33 | 34 | function lineIf() 35 | if x > 127 then print "out of range" 36 | if caveman = "fred" then print "flintstone" else print "rubble" 37 | end function 38 | 39 | 40 | function blockIf() 41 | msg = wait(0, p) 42 | if type(msg) = "roVideoPlayerEvent" then 43 | if debug then print "video event" 44 | if msg.isFullResult() 45 | if debug then print "video finished" 46 | return 9 47 | end if 48 | else if type(msg) = "roUniversalControlEvent" then 49 | if debug then print "button press "; msg.GetInt() 50 | HandleButton(msg.GetInt()) 51 | else if msg = invalid then 52 | if debug print "timeout" 53 | return 6 54 | else 55 | 56 | end if 57 | end function 58 | 59 | 60 | 61 | function posSt() 62 | print tab(40) pos(0) 'prints 40 at position 40 63 | print "these" tab(pos(0) + 5)"words" tab(pos(0) + 5)"are";print tab(pos(0) + 5)"evenly" tab(pos(0) + 5)"spaced" 64 | end function 65 | 66 | 67 | function cat(a, b) 68 | return a + b 'a, b could be numbers or strings 69 | end function 70 | 71 | function five() as integer 72 | return 5 73 | end function 74 | 75 | function add(a as integer, b as integer) as integer 76 | return a + b 77 | end function 78 | 79 | 80 | function add2(a as integer, b = 5 as integer) as integer 81 | return a + b 82 | end function 83 | 84 | 85 | function add3(a as integer, b = a + 5 as integer) as integer 86 | return a + b 87 | end function 88 | 89 | 90 | 91 | sub main() 92 | obj = { 93 | add: add 94 | a: 5 95 | b: 10 96 | } 97 | 98 | obj.add() 99 | obj.add().more(5, 4, 3, 3) 100 | 101 | print obj.result 102 | end sub 103 | 104 | 105 | function add() as void 106 | m.result = m.a + m.b 107 | end function 108 | 109 | 110 | function cond() 111 | if(a = 5) then 112 | else if(a = 4) then 113 | else 114 | end if 115 | end function 116 | 117 | 118 | REM anonymous functions 119 | function anon() as void 120 | myfunc = function(a, b) 121 | return a + b 122 | end function 123 | 124 | #const a = 5 125 | 126 | mysub = sub (a, b) 127 | return a + b 128 | end sub 129 | 130 | #if (a = 5) 131 | print myfunc(1, 2) 132 | #else if (a < 4) 133 | next 134 | #else 135 | print mysub(1, 2) 136 | #end if 137 | 138 | end function 139 | 140 | function math() 141 | #error "Conditional error" 142 | x = 1 * 10 143 | x += 1 144 | ' x = 2 145 | x += 2 146 | ' x = 4 147 | x -= 1 148 | ' x = 3 149 | x /= 2 150 | 151 | m = a.subNode.top() 152 | 153 | x = 9-- 154 | x \= 2 155 | ' x = 4 (integer divide) 156 | x *= 3 157 | ' x = 12 158 | 159 | x =++1 160 | x <<= 8 161 | ' x = 256 162 | x -= 1 163 | ' x = 255 164 | x >>= 4 165 | ' x = 15 166 | end function 167 | 168 | 169 | function dimStatement() 170 | dim c[5, 4, 6] 171 | try 172 | for x = 1 to 5 173 | for y = 1 to 4 174 | for z = 1 to 6 175 | c[x, y, z] = k 176 | k = k + 1 177 | end for 178 | end for 179 | end for 180 | catch e 181 | end try 182 | 183 | 184 | k = 0 185 | for x = 1 to 5 186 | for y = 1 to 4 187 | for z = 1 to 6 188 | if c[x, y, z] <> k then print"error" : stop 189 | if c[x][y][z] <> k then print "error": stop 190 | k = k + 1 191 | end for 192 | end for 193 | end for 194 | end function 195 | -------------------------------------------------------------------------------- /src/BaseVisitor.ts: -------------------------------------------------------------------------------- 1 | import { filter, first, isArray, isUndefined, keys, map, mapKeys, NumericDictionary, values } from 'lodash' 2 | import { ASTNode, BinaryASTNode, ContextProps, Location, NodeContext, Position, TokenContext } from './AST' 3 | import { RokuBRSParser } from './Parser' 4 | 5 | const parser = new RokuBRSParser([]) 6 | const Visitor = parser.getBaseCstVisitorConstructor() 7 | 8 | export class BaseVisitor extends Visitor { 9 | constructor() { 10 | super() 11 | } 12 | 13 | public visit(cstNode: NodeContext, param?: ContextProps) { 14 | if (isArray(cstNode)) { 15 | cstNode = cstNode[0] 16 | } 17 | 18 | if (isUndefined(cstNode)) { 19 | return undefined 20 | } 21 | 22 | return this[cstNode.fullName || cstNode.name](cstNode.children, param) 23 | } 24 | 25 | protected byLine(elements: ASTNode[] = []): NumericDictionary { 26 | return mapKeys( 27 | this.asArray(elements), 28 | (el: ASTNode): number => { 29 | return el.loc && el.loc.start.line 30 | } 31 | ) 32 | } 33 | 34 | protected asArray(value: ASTNode | ASTNode[] = []): ASTNode[] { 35 | if (!value) { 36 | return [] 37 | } 38 | return isArray(value) ? filter(value) : [value] 39 | } 40 | 41 | protected mergeTrailing(elements: ASTNode[] = [], trailing: ASTNode[] = []): ASTNode[] { 42 | const linedTrailing = this.byLine(trailing) 43 | const merger = (el: ASTNode): ASTNode => { 44 | const line = el.loc && el.loc.start.line 45 | if (linedTrailing[line]) { 46 | el.trailing = linedTrailing[line] 47 | } 48 | return el 49 | } 50 | 51 | return map(elements, merger) as any 52 | } 53 | 54 | protected Location(head: ASTNode, tail: ASTNode): Location { 55 | if (!head || !tail) { 56 | return { loc: null, range: null } 57 | } 58 | 59 | const range: [number, number] = [null, null] 60 | const loc = { start: { line: null, column: null }, end: { line: null, column: null } } 61 | 62 | if (head.loc !== null) { 63 | loc.start = head.loc.start 64 | } 65 | 66 | if (tail.loc !== null) { 67 | loc.end = tail.loc.end 68 | } 69 | 70 | if (head.range !== null) { 71 | range[0] = head.range[0] 72 | } 73 | 74 | if (tail.range !== null) { 75 | range[1] = tail.range[1] 76 | } 77 | 78 | return { loc, range } 79 | } 80 | 81 | protected RenderNode(node): ASTNode { 82 | const visitor = subNode => { 83 | return this.visit(subNode) 84 | } 85 | const mapped = filter(map(node, subNode => visitor(subNode))) 86 | 87 | return mapped.length === 1 ? mapped[0] : mapped 88 | } 89 | 90 | protected mergeOperands(from = [], to = [], dividers = []): ASTNode[] { 91 | while (from.length) { 92 | to.push(dividers.shift()) 93 | to.push(from.shift()) 94 | } 95 | 96 | return filter(to) 97 | } 98 | 99 | protected flatListExpression(type: string, operator: ASTNode, left: ASTNode, right: ASTNode): ASTNode { 100 | const head = isArray(left) ? first(left) : left 101 | const tail = isArray(right) ? first(right) : right 102 | 103 | const binary: BinaryASTNode = { type, operator, left, right, ...this.Location(head, tail) } 104 | 105 | return this.asNode(binary, {}) 106 | } 107 | 108 | protected mapArguments(ctx: NodeContext, cb: (_: { [key: string]: ASTNode }) => ASTNode): ASTNode | null { 109 | const _ = {} 110 | 111 | map(ctx, (node, key: string) => { 112 | this.isToken(node) ? (_[key] = this.RenderToken(node)) : (_[key] = this.RenderNode(node)) 113 | }) 114 | 115 | if (cb) { 116 | return cb(_) 117 | } 118 | 119 | return null 120 | } 121 | 122 | protected isToken(node: ASTNode): boolean { 123 | return node && node[0] && node[0].tokenType 124 | } 125 | 126 | protected singleNode(ctx: NodeContext): ASTNode { 127 | const data = this.singleArgument(ctx) 128 | return this.asNode(data as ASTNode, ctx) 129 | } 130 | 131 | protected singleArgument(ctx: NodeContext): ASTNode | false { 132 | const names = keys(ctx) 133 | 134 | if (names.length !== 1) { 135 | return false 136 | } 137 | 138 | return this.mapArguments(ctx, nodes => first(values(nodes))) 139 | } 140 | 141 | protected Position(line: number, column: number): Position { 142 | return { line, column } 143 | } 144 | 145 | protected RenderToken(node): ASTNode | ASTNode[] { 146 | const mapper = ({ startLine, startColumn, image, startOffset, tokenType }: TokenContext): ASTNode => { 147 | const length = image.length 148 | const start = this.Position(startLine, startColumn) 149 | const end = this.Position(startLine, startColumn + length) 150 | 151 | return { 152 | loc: { source: image, start, end }, 153 | range: [startOffset, startOffset + length], 154 | type: tokenType.tokenName 155 | } 156 | } 157 | 158 | const mapped = map(node, mapper) 159 | 160 | return mapped.length === 1 ? mapped[0] : mapped 161 | } 162 | 163 | protected asNode(data: ASTNode, _: NodeContext): ASTNode { 164 | return data 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bright 2 | 3 | [![npm version](https://img.shields.io/npm/v/@roku-road/bright.svg)](https://www.npmjs.com/package/@roku-road/bright) 4 | [![Downloads/month](https://img.shields.io/npm/dm/@roku-road/bright.svg)](http://www.npmtrends.com/@roku-road/bright) 5 | [![Build Status](https://travis-ci.com/RokuRoad/bright.svg?branch=master)](https://travis-ci.com/RokuRoad/bright) 6 | [![CircleCI](https://img.shields.io/circleci/project/github/RokuRoad/bright.svg?style=for-the-badge)](https://circleci.com/gh/RokuRoad/bright) 7 | [![Coverage Status](https://codecov.io/gh/RokuRoad/bright/branch/master/graph/badge.svg)](https://codecov.io/gh/RokuRoad/bright) 8 | [![Dependency Status](https://david-dm.org/RokuRoad/bright.svg)](https://david-dm.org/RokuRoad/bright) [![Greenkeeper badge](https://badges.greenkeeper.io/RokuRoad/bright.svg)](https://greenkeeper.io/) 9 | [![CodeFactor](https://www.codefactor.io/repository/github/RokuRoad/bright/badge)](https://www.codefactor.io/repository/github/RokuRoad/bright) 10 | ## What is Bright? 11 | 12 | Blazing fast parser for BrightScript that gives you ESTree like AST. Sort of. The motivation behind this project is to build solid platform and base for other development tools in Roku world. This parser takes .brs files and builds AST in form of ESTree like structure that could be used with ESLint, Prettier or other tools for linting, refactoring and formatting. 13 | 14 | This project uses awesome https://github.com/SAP/chevrotain parser engine, performance tests : https://sap.github.io/chevrotain/performance/ 15 | 16 | 17 | ### Performance 18 | 19 | While there is no official metrics yet, during development it continuesly got tested on ~800 random open source project files from Github. 20 | 21 | ![Tokei](https://github.com/RokuRoad/bright/blob/master/docs/tokei.png) 22 | 23 | Thanks, [Tokei](https://github.com/Aaronepower/tokei) 24 | 25 | ![Tests](https://github.com/RokuRoad/bright/blob/master/docs/tests.png) 26 | 27 | Project is written in TypeScript and compiles to JS 28 | 29 | ## Installation 30 | 31 | ```yarn``` 32 | 33 | 34 | ## Usage 35 | 36 | ```typescript 37 | import { ast, parse } from '@roku-road/bright' 38 | 39 | export const scanSource = (source: string, type = 'Program') => { 40 | const { value, lexErrors, tokens, parseErrors } = parse(source, type) 41 | 42 | //... 43 | 44 | return ast(value) 45 | } 46 | ``` 47 | 48 | 49 | ## Example 50 | ``` brightscript 51 | Library "ads" 52 | ``` 53 | 54 | Will produce 55 | 56 | #### Tokens 57 | ``` javascript 58 | [ { loc: { start: { column: 1, line: 1 }, end: { column: 7, line: 1 } }, 59 | range: [ 0, 6 ], 60 | type: 'LIBRARY', 61 | value: 'Library' }, 62 | { loc: { start: { column: 9, line: 1 }, end: { column: 13, line: 1 } }, 63 | range: [ 8, 12 ], 64 | type: 'STRING_LITERAL', 65 | value: '"ads"' } ] 66 | ``` 67 | and value 68 | ```javascript 69 | { value: 70 | { name: 'Program', 71 | children: 72 | { Declaration: 73 | [ { name: 'LibraryStatement', 74 | children: 75 | { LIBRARY: 76 | [ { image: 'Library', 77 | startOffset: 0, 78 | endOffset: 6, 79 | startLine: 1, 80 | endLine: 1, 81 | startColumn: 1, 82 | endColumn: 7, 83 | tokenTypeIdx: 39 } ], 84 | path: 85 | [ { image: '"ads"', 86 | startOffset: 8, 87 | endOffset: 12, 88 | startLine: 1, 89 | endLine: 1, 90 | startColumn: 9, 91 | endColumn: 13, 92 | tokenTypeIdx: 79 93 | ], 94 | tokenTypeIdx: 79, 95 | categoryMatches: [], 96 | categoryMatchesMap: {}, 97 | tokenName: 'STRING_LITERAL', 98 | isParent: false } } ] } } ], 99 | EOF: 100 | [ { image: '', 101 | startOffset: NaN, 102 | endOffset: NaN, 103 | startLine: NaN, 104 | endLine: NaN, 105 | startColumn: NaN, 106 | endColumn: NaN, 107 | tokenTypeIdx: 1, 108 | } ] } } } 109 | ``` 110 | 111 | ### Errors 112 | Lets say we forget to put a new line after function signature declaration 113 | 114 | ```brightscript 115 | function a end function 116 | ``` 117 | 118 | ```javascript 119 | [ Error { 120 | name: 'MismatchedTokenException', 121 | message: 'Expecting token of type --> TERMINATOR <-- but found --> \'end function\' <--', 122 | token: 123 | { image: 'end function', 124 | startOffset: 11, 125 | endOffset: 22, 126 | startLine: 1, 127 | endLine: 1, 128 | startColumn: 12, 129 | endColumn: 23, 130 | tokenTypeIdx: 29, 131 | tokenType: 132 | { PATTERN: [Function: pattern], 133 | tokenTypeIdx: 29, 134 | CATEGORIES: [], 135 | categoryMatches: [], 136 | categoryMatchesMap: {}, 137 | tokenName: 'END_FUNCTION', 138 | isParent: false, 139 | LONGER_ALT: 140 | { PATTERN: /([A-Za-z_]+[A-Za-z0-9_]*)/, 141 | tokenTypeIdx: 3, 142 | CATEGORIES: [], 143 | categoryMatches: [], 144 | categoryMatchesMap: {}, 145 | tokenName: 'IDENTIFIER', 146 | isParent: false }, 147 | START_CHARS_HINT: [ 'E', 'e' ] } }, 148 | resyncedTokens: [], 149 | context: 150 | { ruleStack: [ 'Program', 'FunctionDeclaration', 'EndOfStatement' ], 151 | ruleOccurrenceStack: [ 0, 0, 0 ] } } ] 152 | ``` 153 | 154 | ##### Rendered as 155 | 156 | ``` 157 | > 1 | function a end function 158 | | ^^^^^^^^^^^ Expecting token of type --> TERMINATOR <-- but found --> 'end function' <-- 159 | ``` 160 | 161 | 162 | Bright consists of Tokens, Parser and Visitors. Please see *Chevrotain* project for details 163 | 164 | | Element | Description | 165 | | --- | --- | 166 | | RokuBRSParser | Heart of the project, defined structure of nodes | 167 | | ASTVisitor | Visitor that walks parsed CST and produce AST for other tools | 168 | | ALL_TOKENS | Map of the tokens, literals, punctuation etc | 169 | | parse | API function to get parsed value, lexer and parser errors | 170 | | ast | API function to get AST tree value, lexer and parser errors | 171 | | visitorKeys | Map for walking though the tree (to avoid blind iteration) | 172 | 173 | ## Grammar 174 | Please check generated https://github.com/RokuRoad/bright/blob/master/diagram/index.html for details 175 | -------------------------------------------------------------------------------- /src/BaseASTWalker.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode } from "./AST"; 2 | import { ASTWalker } from "./ASTWalker"; 3 | 4 | import { isArray, map, zipObject } from "lodash"; 5 | import { visitorKeys } from "./VisitorKeys"; 6 | 7 | /** 8 | * Checks if method should be represented as visitor 9 | * @param method 10 | */ 11 | const shouldBeVisitor = (method: string) => { 12 | return visitorKeys[method] && method.toUpperCase() !== method; 13 | }; 14 | 15 | /** 16 | * 17 | * @export 18 | * @class BaseASTWalker 19 | * @implements {(ASTWalker)} 20 | */ 21 | export class BaseASTWalker implements ASTWalker { 22 | public Program(): ASTNode | ASTNode[] { 23 | throw new Error("Method not implemented."); 24 | } 25 | public LibraryStatement(): ASTNode | ASTNode[] { 26 | throw new Error("Method not implemented."); 27 | } 28 | public UnTypedIdentifier(): ASTNode | ASTNode[] { 29 | throw new Error("Method not implemented."); 30 | } 31 | public ParameterList(): ASTNode | ASTNode[] { 32 | throw new Error("Method not implemented."); 33 | } 34 | public AdditionExpression(): ASTNode | ASTNode[] { 35 | throw new Error("Method not implemented."); 36 | } 37 | public AssignmentExpression(): ASTNode | ASTNode[] { 38 | throw new Error("Method not implemented."); 39 | } 40 | public MultiplicationExpression(): ASTNode | ASTNode[] { 41 | throw new Error("Method not implemented."); 42 | } 43 | public LogicExpression(): ASTNode | ASTNode[] { 44 | throw new Error("Method not implemented."); 45 | } 46 | public ParenthesisExpression(): ASTNode | ASTNode[] { 47 | throw new Error("Method not implemented."); 48 | } 49 | public ArrayElement(): ASTNode | ASTNode[] { 50 | throw new Error("Method not implemented."); 51 | } 52 | public ArrayExpression(): ASTNode | ASTNode[] { 53 | throw new Error("Method not implemented."); 54 | } 55 | public CallExpression(): ASTNode | ASTNode[] { 56 | throw new Error("Method not implemented."); 57 | } 58 | public ConditionalConst(): ASTNode | ASTNode[] { 59 | throw new Error("Method not implemented."); 60 | } 61 | public ConditionalElseIfStatement(): ASTNode | ASTNode[] { 62 | throw new Error("Method not implemented."); 63 | } 64 | public ConditionalElseStatement(): ASTNode | ASTNode[] { 65 | throw new Error("Method not implemented."); 66 | } 67 | public ConditionalError(): ASTNode | ASTNode[] { 68 | throw new Error("Method not implemented."); 69 | } 70 | public ConditionalIfStatement(): ASTNode | ASTNode[] { 71 | throw new Error("Method not implemented."); 72 | } 73 | public DimStatement(): ASTNode | ASTNode[] { 74 | throw new Error("Method not implemented."); 75 | } 76 | public DotMemberExpression(): ASTNode | ASTNode[] { 77 | throw new Error("Method not implemented."); 78 | } 79 | public ElseIfStatement(): ASTNode | ASTNode[] { 80 | throw new Error("Method not implemented."); 81 | } 82 | public ElseStatement(): ASTNode | ASTNode[] { 83 | throw new Error("Method not implemented."); 84 | } 85 | public EmptyStatement(): ASTNode | ASTNode[] { 86 | throw new Error("Method not implemented."); 87 | } 88 | public Comment(): ASTNode | ASTNode[] { 89 | throw new Error("Method not implemented."); 90 | } 91 | public ForEachStatement(): ASTNode | ASTNode[] { 92 | throw new Error("Method not implemented."); 93 | } 94 | public ForStatement(): ASTNode | ASTNode[] { 95 | throw new Error("Method not implemented."); 96 | } 97 | public FunctionDeclaration(): ASTNode | ASTNode[] { 98 | throw new Error("Method not implemented."); 99 | } 100 | public FunctionExpression(): ASTNode | ASTNode[] { 101 | throw new Error("Method not implemented."); 102 | } 103 | public GoToStatement(): ASTNode | ASTNode[] { 104 | throw new Error("Method not implemented."); 105 | } 106 | public IfStatement(): ASTNode | ASTNode[] { 107 | throw new Error("Method not implemented."); 108 | } 109 | public Literal(): ASTNode | ASTNode[] { 110 | throw new Error("Method not implemented."); 111 | } 112 | public MemberExpression(): ASTNode | ASTNode[] { 113 | throw new Error("Method not implemented."); 114 | } 115 | public NextStatement(): ASTNode | ASTNode[] { 116 | throw new Error("Method not implemented."); 117 | } 118 | public PostfixExpression(): ASTNode | ASTNode[] { 119 | throw new Error("Method not implemented."); 120 | } 121 | public PrintStatement(): ASTNode | ASTNode[] { 122 | throw new Error("Method not implemented."); 123 | } 124 | public RelationExpression(): ASTNode | ASTNode[] { 125 | throw new Error("Method not implemented."); 126 | } 127 | public ReturnStatement(): ASTNode | ASTNode[] { 128 | throw new Error("Method not implemented."); 129 | } 130 | public StopStatement(): ASTNode | ASTNode[] { 131 | throw new Error("Method not implemented."); 132 | } 133 | public SubDeclaration(): ASTNode | ASTNode[] { 134 | throw new Error("Method not implemented."); 135 | } 136 | public SubExpression(): ASTNode | ASTNode[] { 137 | throw new Error("Method not implemented."); 138 | } 139 | public UnaryExpression(): ASTNode | ASTNode[] { 140 | throw new Error("Method not implemented."); 141 | } 142 | public WhileStatement(): ASTNode | ASTNode[] { 143 | throw new Error("Method not implemented."); 144 | } 145 | public RokuTryStatement(): ASTNode | ASTNode[] { 146 | throw new Error("Method not implemented."); 147 | } 148 | public BlockStatement(): ASTNode | ASTNode[] { 149 | throw new Error("Method not implemented."); 150 | } 151 | public TypeAnnotation(): ASTNode | ASTNode[] { 152 | throw new Error("Method not implemented."); 153 | } 154 | public ObjectExpression(): ASTNode | ASTNode[] { 155 | throw new Error("Method not implemented."); 156 | } 157 | public Parameter(): ASTNode | ASTNode[] { 158 | throw new Error("Method not implemented."); 159 | } 160 | public Identifier(): ASTNode | ASTNode[] { 161 | throw new Error("Method not implemented."); 162 | } 163 | public Property(): ASTNode | ASTNode[] { 164 | throw new Error("Method not implemented."); 165 | } 166 | public Arguments(): ASTNode | ASTNode[] { 167 | throw new Error("Method not implemented."); 168 | } 169 | public STRING_LITERAL(ctx): ASTNode { 170 | return ctx.loc.source; 171 | } 172 | 173 | /** 174 | * Renders ASTNode using this class methods 175 | * @param {ASTNode} ctx 176 | * @returns {({ [key: string]: ASTNode | ASTNode[] })} 177 | * 178 | * @memberOf BaseASTWalker 179 | */ 180 | public visit(ctx: ASTNode): { [key: string]: ASTNode | ASTNode[] } { 181 | if (!ctx.type) { 182 | return null; 183 | } 184 | 185 | const keysToVisit = visitorKeys[ctx.type]; 186 | const mapped = map(keysToVisit, (key) => { 187 | if (!ctx[key]) { 188 | return ctx[key]; 189 | } 190 | 191 | if (ctx[key].type) { 192 | const method = ctx[key].type; 193 | 194 | if (!this[method]) { 195 | if (shouldBeVisitor(method)) { 196 | // tslint:disable-next-line:no-console 197 | console.warn(method); 198 | } else { 199 | return this.STRING_LITERAL(ctx[key]); 200 | } 201 | } else { 202 | return this[method](ctx[key]); 203 | } 204 | } else { 205 | if (isArray(ctx[key])) { 206 | return map(ctx[key], (element) => { 207 | const method = element.type; 208 | 209 | if (!this[method]) { 210 | if (shouldBeVisitor(method)) { 211 | // tslint:disable-next-line:no-console 212 | console.warn(method); 213 | } else { 214 | return this.STRING_LITERAL(element); 215 | } 216 | } else { 217 | return this[method](element); 218 | } 219 | }); 220 | } 221 | } 222 | }); 223 | 224 | return zipObject(keysToVisit, mapped); 225 | } 226 | 227 | /** 228 | * Converts value to array if it is a single element 229 | * 230 | * @protected 231 | * @param {(ASTNode[] | ASTNode)} value 232 | * @returns {ASTNode[]} 233 | * 234 | * @memberOf BaseASTWalker 235 | */ 236 | public asArray(value: ASTNode[] | ASTNode): ASTNode[] { 237 | if (!value) { 238 | return []; 239 | } 240 | return isArray(value) ? value : [value]; 241 | } 242 | 243 | /** 244 | * Build expression from left, right and operator. 245 | * Converts to array as necessary 246 | * 247 | * @protected 248 | * @param {left, right, operator} ctx 249 | * @returns Array of elements 250 | * 251 | * @memberOf BaseASTWalker 252 | */ 253 | public binary(ctx: any) { 254 | let { left, right, operator } = this.visit(ctx); 255 | 256 | left = this.asArray(left); 257 | right = this.asArray(right); 258 | operator = this.asArray(operator); 259 | 260 | return this.mergeOperands(right, left, operator); 261 | } 262 | 263 | /** 264 | * Merge left, right and operator arrays. 265 | * Useful in situation when 266 | * 5 + 4 + 8 is represented as 267 | * left [5] 268 | * right [4, 8] 269 | * operators [+, +] 270 | * 271 | * @protected 272 | * @param {ASTNode[]} [from=[]] 273 | * @param {ASTNode[]} [to=[]] 274 | * @param {ASTNode[]} [dividers=[]] 275 | * @returns 276 | * 277 | * @memberOf BaseASTWalker 278 | */ 279 | public mergeOperands( 280 | from: ASTNode[] = [], 281 | to: ASTNode[] = [], 282 | dividers: ASTNode[] = [] 283 | ) { 284 | while (from.length) { 285 | to.push(dividers.shift()); 286 | to.push(from.shift()); 287 | } 288 | 289 | return to; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/Tokens.ts: -------------------------------------------------------------------------------- 1 | import { createToken, Lexer } from "chevrotain"; 2 | 3 | export const WS = createToken({ 4 | group: Lexer.SKIPPED, 5 | name: "WS", 6 | pattern: /[\s\t]+/, 7 | }); 8 | 9 | export const IDENTIFIER = createToken({ 10 | name: "IDENTIFIER", 11 | pattern: /([A-Za-z_]+[A-Za-z0-9_]*)/, 12 | }); 13 | 14 | const keyword = (words: string | string[], opts = {}) => { 15 | if (!Array.isArray(words)) { 16 | words = [words]; 17 | } 18 | 19 | const term = words[0].toLowerCase(); 20 | 21 | const name = words.map((word) => word.toUpperCase()).join("_"); 22 | const re = new RegExp(`\\b${words.join("[ \\t]*")}\\b`, "iy"); 23 | 24 | const pattern = (text: string, startOffset: number) => { 25 | re.lastIndex = startOffset; 26 | return re.exec(text); 27 | }; 28 | 29 | const hint = term.substr(0, 1); 30 | const startHint = 31 | hint.toUpperCase() === hint.toLowerCase() 32 | ? [hint.toUpperCase()] 33 | : [hint.toUpperCase(), hint.toLowerCase()]; 34 | 35 | return createToken({ 36 | name, 37 | pattern, 38 | longer_alt: IDENTIFIER, 39 | start_chars_hint: startHint, 40 | line_breaks: false, 41 | ...opts, 42 | }); 43 | }; 44 | 45 | export const BASE_TYPE = createToken({ name: "BASE_TYPE", pattern: Lexer.NA }); 46 | export const LITERAL = createToken({ name: "LITERAL", pattern: Lexer.NA }); 47 | export const RELATIONAL_OPERATOR = createToken({ 48 | name: "RELATIONAL_OPERATOR", 49 | pattern: Lexer.NA, 50 | }); 51 | export const EQUALITY_OPERATOR = createToken({ 52 | name: "EQUALITY_OPERATOR", 53 | pattern: Lexer.NA, 54 | }); 55 | export const PRINT = createToken({ name: "PRINT", pattern: Lexer.NA }); 56 | 57 | export const PUNCTUATION = createToken({ 58 | name: "PUNCTUATION", 59 | pattern: Lexer.NA, 60 | }); 61 | 62 | export const LOGIC_OPERATOR = createToken({ 63 | name: "LOGIC_OPERATOR", 64 | pattern: Lexer.NA, 65 | categories: PUNCTUATION, 66 | }); 67 | export const SHIFT_OPERATOR = createToken({ 68 | name: "SHIFT_OPERATOR", 69 | pattern: Lexer.NA, 70 | categories: PUNCTUATION, 71 | }); 72 | export const MULTI_OPERATOR = createToken({ 73 | name: "MULTI_OPERATOR", 74 | pattern: Lexer.NA, 75 | categories: PUNCTUATION, 76 | }); 77 | 78 | export const TERMINATOR = createToken({ 79 | name: "TERMINATOR", 80 | pattern: Lexer.NA, 81 | }); 82 | 83 | export const UNARY = createToken({ 84 | name: "UNARY", 85 | pattern: Lexer.NA, 86 | categories: PUNCTUATION, 87 | }); 88 | export const POSTFIX = createToken({ 89 | name: "POSTFIX", 90 | pattern: Lexer.NA, 91 | categories: PUNCTUATION, 92 | }); 93 | export const ADDICTIVE_OPERATOR = createToken({ 94 | categories: PUNCTUATION, 95 | name: "ADDICTIVE_OPERATOR", 96 | pattern: Lexer.NA, 97 | }); 98 | 99 | export const ATTRIBUTE = createToken({ 100 | name: "ATTRIBUTE", 101 | pattern: "@", 102 | line_breaks: false, 103 | }); 104 | export const BACK_SLASH = createToken({ 105 | name: "BACK_SLASH", 106 | pattern: "\\", 107 | categories: MULTI_OPERATOR, 108 | line_breaks: false, 109 | }); 110 | 111 | export const COMMENT_QUOTE = createToken({ 112 | name: "COMMENT_QUOTE", 113 | pattern: /'[^\n\r]*/, 114 | line_breaks: false, 115 | }); 116 | export const COMMENT_REM = createToken({ 117 | name: "COMMENT_REM", 118 | pattern: /\b(rem|REM|Rem)\b[^\n\r]*/i, 119 | line_breaks: false, 120 | }); 121 | 122 | export const CONDITIONAL_IF = createToken({ 123 | name: "CONDITIONAL_IF", 124 | pattern: /#\bif\b/, 125 | line_breaks: false, 126 | }); 127 | 128 | export const CONDITIONAL_ELSE = createToken({ 129 | name: "CONDITIONAL_ELSE", 130 | pattern: /#\else\b/, 131 | line_breaks: false, 132 | }); 133 | export const CONDITIONAL_ELSE_IF = createToken({ 134 | name: "CONDITIONAL_ELSE_IF", 135 | pattern: /#\belse[ ]*if\b/, 136 | }); 137 | export const CONDITIONAL_END_IF = createToken({ 138 | name: "CONDITIONAL_END_IF", 139 | pattern: /#\bend[ ]*if\b/, 140 | }); 141 | export const CONDITIONAL_ERROR = createToken({ 142 | name: "CONDITIONAL_ERROR", 143 | pattern: /#\berror\b[^\n\r]+/, 144 | }); 145 | export const CONDITIONAL_CONST = createToken({ 146 | name: "CONDITIONAL_CONST", 147 | pattern: /#\bconst\b/, 148 | }); 149 | 150 | export const ELSE_IF = keyword(["Else", "If"]); 151 | 152 | export const END_FOR = keyword(["End", "For"]); 153 | export const END_FUNCTION = keyword(["End", "Function"]); 154 | export const END_IF = keyword(["End", "If"]); 155 | export const END_SUB = keyword(["End", "Sub"]); 156 | export const END_WHILE = keyword(["End", "While"]); 157 | 158 | export const TRY = keyword("Try"); 159 | export const CATCH = keyword("Catch"); 160 | export const END_TRY = keyword(["End", "Try"]); 161 | export const AS = keyword("As"); 162 | export const EVAL = keyword("Eval"); 163 | export const EXIT = keyword("Exit"); 164 | export const FOR = keyword("For"); 165 | export const GOTO = keyword("Goto"); 166 | export const LET = keyword("Let"); 167 | export const LIBRARY = keyword("Library"); 168 | export const LINE_NUM = keyword("LINE_NUM"); 169 | export const ELSE = keyword("Else"); 170 | export const END = keyword("End"); 171 | export const DIM = keyword("Dim"); 172 | export const EACH = keyword("Each"); 173 | export const IF = keyword("If"); 174 | export const IN = keyword("In"); 175 | export const NEXT = keyword("Next"); 176 | 177 | export const EXIT_FOR = keyword(["Exit", "For"]); 178 | export const EXIT_WHILE = keyword(["Exit", "While"]); 179 | 180 | export const TYPE_DECLARATION = createToken({ 181 | name: "TYPE_DECLARATION", 182 | pattern: /[\$%!#&]/, 183 | }); 184 | 185 | export const OPEN_BRACKET = createToken({ 186 | name: "OPEN_BRACKET", 187 | pattern: "[", 188 | line_breaks: false, 189 | }); 190 | export const OPEN_CURLY_BRACE = createToken({ 191 | name: "OPEN_CURLY_BRACE", 192 | pattern: "{", 193 | line_breaks: false, 194 | }); 195 | export const OPEN_PAREN = createToken({ 196 | name: "OPEN_PAREN", 197 | pattern: "(", 198 | line_breaks: false, 199 | }); 200 | 201 | export const CLOSE_BRACKET = createToken({ 202 | name: "CLOSE_BRACKET", 203 | pattern: "]", 204 | line_breaks: false, 205 | }); 206 | export const CLOSE_CURLY_BRACE = createToken({ 207 | name: "CLOSE_CURLY_BRACE", 208 | pattern: "}", 209 | line_breaks: false, 210 | }); 211 | export const CLOSE_PAREN = createToken({ 212 | name: "CLOSE_PAREN", 213 | pattern: ")", 214 | line_breaks: false, 215 | }); 216 | 217 | export const PERIOD = createToken({ name: "PERIOD", pattern: "." }); 218 | 219 | export const FULL_PRINT = keyword("Print", { 220 | name: "FULL_PRINT", 221 | categories: PRINT, 222 | line_breaks: false, 223 | }); 224 | export const SHORT_PRINT = createToken({ 225 | pattern: "?", 226 | name: "SHORT_PRINT", 227 | categories: PRINT, 228 | line_breaks: false, 229 | }); 230 | 231 | export const RETURN = keyword("return"); 232 | export const STEP = keyword("step"); 233 | export const STOP = keyword("stop"); 234 | 235 | export const BOOLEAN = keyword("Boolean", { categories: BASE_TYPE }); 236 | export const INTEGER = keyword("Integer", { categories: BASE_TYPE }); 237 | export const LONGINTEGER = keyword("LongInteger", { categories: BASE_TYPE }); 238 | export const FLOAT = keyword("Float", { categories: BASE_TYPE }); 239 | export const DOUBLE = keyword("Double", { categories: BASE_TYPE }); 240 | export const STRING = keyword("String", { categories: BASE_TYPE }); 241 | export const OBJECT = keyword("Object", { categories: BASE_TYPE }); 242 | export const FUNCTION = keyword("Function", { categories: BASE_TYPE }); 243 | export const INTERFACE = keyword("Interface", { categories: BASE_TYPE }); 244 | export const INVALID = keyword("Invalid", { categories: [BASE_TYPE, LITERAL] }); 245 | export const DYNAMIC = keyword("Dynamic", { categories: BASE_TYPE }); 246 | export const VOID = keyword("Void", { categories: BASE_TYPE }); 247 | 248 | export const SUB = keyword("Sub"); 249 | 250 | export const THEN = keyword("Then"); 251 | export const TO = keyword("To"); 252 | export const WHILE = keyword("While"); 253 | 254 | export const STRING_LITERAL = createToken({ 255 | categories: LITERAL, 256 | name: "STRING_LITERAL", 257 | pattern: /"([^"]|"")*"/, 258 | }); 259 | 260 | export const BOOLEAN_LITERAL = createToken({ 261 | name: "BOOLEAN_LITERAL", 262 | pattern: Lexer.NA, 263 | categories: LITERAL, 264 | }); 265 | export const TRUE = keyword("true", { categories: BOOLEAN_LITERAL }); 266 | export const FALSE = keyword("false", { categories: BOOLEAN_LITERAL }); 267 | 268 | export const NUMBER_LITERAL = createToken({ 269 | categories: LITERAL, 270 | name: "NUMBER_LITERAL", 271 | pattern: /(?:\d*\.?\d+|\d+\.?\d*)(?:[eEdD][-+]?\d+)?/, 272 | }); 273 | 274 | export const HEX_LITERAL = createToken({ 275 | categories: LITERAL, 276 | name: "HEX_LITERAL", 277 | pattern: /&[hHFf0-9EeDdCcBbAa]+&?/, 278 | }); 279 | 280 | export const GREATER_THAN = createToken({ 281 | name: "GREATER_THAN", 282 | pattern: ">", 283 | categories: RELATIONAL_OPERATOR, 284 | }); 285 | export const GREATER_THAN_EQUAL = createToken({ 286 | categories: RELATIONAL_OPERATOR, 287 | name: "GREATER_THAN_EQUAL", 288 | pattern: ">=", 289 | }); 290 | export const LESS_THAN = createToken({ 291 | name: "LESS_THAN", 292 | pattern: "<", 293 | categories: RELATIONAL_OPERATOR, 294 | }); 295 | export const LESS_THAN_EQUAL = createToken({ 296 | name: "LESS_THAN_EQUAL", 297 | pattern: "<=", 298 | categories: RELATIONAL_OPERATOR, 299 | }); 300 | export const NOT_EQUAL = createToken({ 301 | name: "NOT_EQUAL", 302 | pattern: "<>", 303 | categories: RELATIONAL_OPERATOR, 304 | }); 305 | 306 | export const EQUAL = createToken({ 307 | name: "EQUAL", 308 | pattern: "=", 309 | categories: EQUALITY_OPERATOR, 310 | }); 311 | export const OP_ASSIGNMENT_ADD = createToken({ 312 | categories: EQUALITY_OPERATOR, 313 | name: "OP_ASSIGNMENT_ADD", 314 | pattern: "+=", 315 | }); 316 | export const OP_ASSIGNMENT_BITSHIFT_LEFT = createToken({ 317 | categories: EQUALITY_OPERATOR, 318 | name: "OP_ASSIGNMENT_BITSHIFT_LEFT", 319 | pattern: "<<=", 320 | }); 321 | export const OP_ASSIGNMENT_BITSHIFT_RIGHT = createToken({ 322 | categories: EQUALITY_OPERATOR, 323 | name: "OP_ASSIGNMENT_BITSHIFT_RIGHT", 324 | pattern: ">>=", 325 | }); 326 | 327 | export const OP_ASSIGNMENT_DIVISION = createToken({ 328 | categories: EQUALITY_OPERATOR, 329 | name: "OP_ASSIGNMENT_DIVISION", 330 | pattern: "/=", 331 | }); 332 | 333 | // prettier-ignore 334 | export const OP_ASSIGNMENT_INTEGER_DIVISION = createToken({ name: 'OP_ASSIGNMENT_INTEGER_DIVISION', pattern: '\\=', categories: EQUALITY_OPERATOR }) 335 | 336 | export const OP_ASSIGNMENT_MULTIPLY = createToken({ 337 | categories: EQUALITY_OPERATOR, 338 | name: "OP_ASSIGNMENT_MULTIPLY", 339 | pattern: "*=", 340 | }); 341 | export const OP_ASSIGNMENT_SUBTRACT = createToken({ 342 | categories: EQUALITY_OPERATOR, 343 | name: "OP_ASSIGNMENT_SUBTRACT", 344 | pattern: "-=", 345 | }); 346 | 347 | export const COMMA = createToken({ name: "COMMA", pattern: "," }); 348 | 349 | export const COLON = createToken({ 350 | name: "COLON", 351 | pattern: ":", 352 | categories: TERMINATOR, 353 | }); 354 | export const NEWLINE = createToken({ 355 | categories: TERMINATOR, 356 | line_breaks: true, 357 | name: "NEWLINE", 358 | pattern: /\s*[\n\r]+/, 359 | }); 360 | 361 | export const MOD = keyword("Mod", { categories: LOGIC_OPERATOR }); 362 | export const OR = keyword("Or", { categories: LOGIC_OPERATOR }); 363 | export const NOT = keyword("Not", { categories: [LOGIC_OPERATOR, UNARY] }); 364 | export const AND = keyword("And", { categories: LOGIC_OPERATOR }); 365 | 366 | export const SEMICOLON = createToken({ 367 | name: "SEMICOLON", 368 | pattern: ";", 369 | categories: TERMINATOR, 370 | }); 371 | 372 | export const DECREMENT = createToken({ 373 | name: "DECREMENT", 374 | pattern: "--", 375 | categories: [UNARY, POSTFIX], 376 | }); 377 | export const INCREMENT = createToken({ 378 | name: "INCREMENT", 379 | pattern: "++", 380 | categories: [UNARY, POSTFIX], 381 | }); 382 | export const BITSHIFT_LEFT = createToken({ 383 | name: "BITSHIFT_LEFT", 384 | pattern: "<<", 385 | categories: SHIFT_OPERATOR, 386 | }); 387 | export const BITSHIFT_RIGHT = createToken({ 388 | name: "BITSHIFT_RIGHT", 389 | pattern: ">>", 390 | categories: SHIFT_OPERATOR, 391 | }); 392 | export const OP_DIVIDE = createToken({ 393 | name: "OP_DIVIDE", 394 | pattern: "/", 395 | categories: MULTI_OPERATOR, 396 | }); 397 | export const OP_MULTIPLE = createToken({ 398 | name: "OP_MULTIPLE", 399 | pattern: "*", 400 | categories: MULTI_OPERATOR, 401 | }); 402 | export const OP_PLUS = createToken({ 403 | name: "OP_PLUS", 404 | pattern: "+", 405 | categories: [ADDICTIVE_OPERATOR, UNARY], 406 | }); 407 | export const OP_MINUS = createToken({ 408 | name: "OP_MINUS", 409 | pattern: "-", 410 | categories: [ADDICTIVE_OPERATOR, UNARY], 411 | }); 412 | export const OP_EXPONENT = createToken({ 413 | name: "OP_EXPONENT", 414 | pattern: "^", 415 | categories: MULTI_OPERATOR, 416 | }); 417 | 418 | export const ALL_TOKENS = [ 419 | // First 420 | NEWLINE, 421 | WS, 422 | COMMENT_QUOTE, 423 | COMMENT_REM, 424 | LITERAL, 425 | STRING_LITERAL, 426 | BOOLEAN_LITERAL, 427 | TRUE, 428 | FALSE, 429 | HEX_LITERAL, 430 | NUMBER_LITERAL, 431 | 432 | // 433 | 434 | AS, 435 | 436 | OP_ASSIGNMENT_ADD, 437 | OP_ASSIGNMENT_MULTIPLY, 438 | OP_ASSIGNMENT_SUBTRACT, 439 | OP_MULTIPLE, 440 | OP_ASSIGNMENT_INTEGER_DIVISION, 441 | OP_ASSIGNMENT_DIVISION, 442 | OP_ASSIGNMENT_BITSHIFT_LEFT, 443 | OP_ASSIGNMENT_BITSHIFT_RIGHT, 444 | 445 | LOGIC_OPERATOR, 446 | EQUALITY_OPERATOR, 447 | RELATIONAL_OPERATOR, 448 | SHIFT_OPERATOR, 449 | ADDICTIVE_OPERATOR, 450 | MULTI_OPERATOR, 451 | PUNCTUATION, 452 | 453 | ATTRIBUTE, 454 | BACK_SLASH, 455 | 456 | OP_EXPONENT, 457 | CLOSE_BRACKET, 458 | CLOSE_CURLY_BRACE, 459 | CLOSE_PAREN, 460 | 461 | TERMINATOR, 462 | COLON, 463 | COMMA, 464 | 465 | CONDITIONAL_CONST, 466 | CONDITIONAL_ERROR, 467 | 468 | CONDITIONAL_ELSE_IF, 469 | CONDITIONAL_END_IF, 470 | CONDITIONAL_ELSE, 471 | CONDITIONAL_IF, 472 | 473 | DIM, 474 | 475 | // DOUBLE_QUOTE, 476 | 477 | END_FOR, 478 | END_FUNCTION, 479 | END_IF, 480 | END_SUB, 481 | END_WHILE, 482 | TRY, 483 | CATCH, 484 | END_TRY, 485 | 486 | ELSE_IF, 487 | 488 | ELSE, 489 | END, 490 | EACH, 491 | 492 | NOT_EQUAL, 493 | 494 | EXIT_FOR, 495 | EXIT_WHILE, 496 | EXIT, 497 | FOR, 498 | 499 | DECREMENT, 500 | INCREMENT, 501 | 502 | OP_DIVIDE, 503 | OP_PLUS, 504 | OP_MINUS, 505 | 506 | UNARY, 507 | POSTFIX, 508 | FUNCTION, 509 | 510 | GOTO, 511 | GREATER_THAN_EQUAL, 512 | BITSHIFT_RIGHT, 513 | GREATER_THAN, 514 | 515 | LESS_THAN_EQUAL, 516 | BITSHIFT_LEFT, 517 | LESS_THAN, 518 | LET, 519 | LIBRARY, 520 | LINE_NUM, 521 | 522 | MOD, 523 | NEXT, 524 | 525 | AND, 526 | NOT, 527 | OR, 528 | 529 | BASE_TYPE, 530 | BOOLEAN, 531 | DOUBLE, 532 | STRING, 533 | INTEGER, 534 | LONGINTEGER, 535 | FLOAT, 536 | DYNAMIC, 537 | INTERFACE, 538 | VOID, 539 | OBJECT, 540 | 541 | OPEN_BRACKET, 542 | OPEN_CURLY_BRACE, 543 | OPEN_PAREN, 544 | 545 | IF, 546 | IN, 547 | 548 | PERIOD, 549 | 550 | PRINT, 551 | FULL_PRINT, 552 | SHORT_PRINT, 553 | 554 | RETURN, 555 | SEMICOLON, 556 | STEP, 557 | STOP, 558 | 559 | SUB, 560 | THEN, 561 | TO, 562 | 563 | WHILE, 564 | 565 | EQUAL, 566 | 567 | // Last 568 | IDENTIFIER, 569 | TYPE_DECLARATION, 570 | ]; 571 | -------------------------------------------------------------------------------- /test/assets/rodash.brs: -------------------------------------------------------------------------------- 1 | ' VERSION: rodash 0.2.0 2 | ' LICENSE: Permission is hereby granted, free of charge, to any person obtaining 3 | ' LICENSE: a copy of this software and associated documentation files (the 4 | ' LICENSE: "Software"), to deal in the Software without restriction, including 5 | ' LICENSE: without limitation the rights to use, copy, modify, merge, publish, 6 | ' LICENSE: distribute, sublicense, and/or sell copies of the Software, and to 7 | ' LICENSE: permit persons to whom the Software is furnished to do so, subject to 8 | ' LICENSE: the following conditions: 9 | ' LICENSE: 10 | ' LICENSE: The above copyright notice and this permission notice shall be 11 | ' LICENSE: included in all copies or substantial portions of the Software. 12 | ' LICENSE: 13 | ' LICENSE: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | ' LICENSE: EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | ' LICENSE: MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | ' LICENSE: NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | ' LICENSE: LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | ' LICENSE: OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | ' LICENSE: WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | Function rodash_cloneNode_(source) 21 | blacklistedFields = ["change", "focusedChild"] ' read-only fields 22 | destination = CreateObject("roSGNode", source.subtype()) 23 | fields = source.getFields() 24 | for each f in blacklistedFields 25 | fields.delete(f) 26 | end for 27 | destination.setFields(fields) 28 | return destination 29 | End Function 30 | Function rodash_cloneAssocArray_(source) 31 | destination = {} 32 | for each key in source 33 | destination[key] = source[key] 34 | end for 35 | return destination 36 | End Function 37 | Function rodash_cloneArrayish_(source) 38 | if type(source) = "roArray" 39 | destination = CreateObject(type(source), 0, true) 40 | else 41 | destination = CreateObject(type(source)) 42 | end if 43 | destination.Append(source) 44 | return destination 45 | End Function 46 | Function rodash_cloneStringish_(source) 47 | destination = CreateObject("roString") 48 | destination.SetString(source.GetString()) 49 | if type(source) = "roString" 50 | return destination 51 | else if type(source) = "String" 52 | return destination.GetString() 53 | end if 54 | End Function 55 | Function rodash_clone_(source) 56 | if source <> invalid 57 | if type(source) = "roSGNode" 58 | return m.cloneNode_(source) 59 | else if type(source) = "roAssociativeArray" 60 | return m.cloneAssocArray_(source) 61 | else if GetInterface(source, "ifArray") <> invalid 62 | return m.cloneArrayish_(source) 63 | else if (type(source) = "roString") or (type(source) = "String") 64 | return m.cloneStringish_(source) 65 | else 66 | if type(Box(source)) <> type(source) 67 | return source 68 | else if type(source) = "roInt" 69 | return Box(source.GetInt()) 70 | else if type(source) = "roLongInt" 71 | return Box(source.GetLongInt()) 72 | else if type(source) = "roFloat" 73 | return Box(source.GetFloat()) 74 | else if type(source) = "roDouble" 75 | return Box(source.GetDouble()) 76 | else if type(source) = "roBoolean" 77 | return Box(source.GetBoolean()) 78 | else 79 | return invalid 80 | end if 81 | end if 82 | end if 83 | return invalid 84 | End Function 85 | Function rodash_cloneDeep_(source) 86 | newTree = m.clone(source) 87 | for i=0 to source.getChildCount()-1 88 | newTree.appendChild(m.cloneDeep(source.getChild(i))) 89 | end for 90 | return newTree 91 | End Function 92 | Function rodash_cond_(expression, t, f) 93 | if expression 94 | return t 95 | else 96 | return f 97 | end if 98 | End Function 99 | Function rodash_createRequest_(url As String, options={} As Object) As Object 100 | if Left(LCase(url), 5) = "https" then 101 | https = true 102 | else 103 | https = false 104 | end if 105 | validMethods = {"GET":true, "PUT":true, "POST":true, "DELETE":true, "PATCH":true} 106 | if options <> invalid and options.method <> invalid and validMethods.DoesExist(UCase(options.method)) then 107 | method = UCase(options.method) 108 | else 109 | method = "GET" 110 | end if 111 | if options <> invalid and options.headers <> invalid and type(options.headers) = "roAssociativeArray" then 112 | headers = options.headers 113 | else 114 | headers = {} 115 | end if 116 | if options <> invalid and options.body <> invalid and (type(options.body) = "String" or type(options.body) = "roString") then 117 | body = options.body 118 | else 119 | body = "" 120 | end if 121 | return { 122 | url: url 123 | https: https 124 | headers: headers 125 | body: body 126 | method: method 127 | id: CreateObject("roDeviceInfo").GetRandomUUID() 128 | urlTransfer: invalid 129 | urlEvent: invalid ' response roUrlEvent 130 | start: rodash_request_start_ 131 | cancel: rodash_request_cancel_ 132 | handleEvent: rodash_request_handleEvent_ 133 | } 134 | End Function 135 | Function rodash_request_start_(synchronous=false As Boolean, messagePort=invalid As Object) As Object 136 | urlTransfer = CreateObject("roUrlTransfer") 137 | urlTransfer.SetUrl(m.url) 138 | if m.https then 139 | urlTransfer.SetCertificatesFile("common:/certs/ca-bundle.crt") 140 | end if 141 | urlTransfer.SetRequest(m.method) 142 | urlTransfer.EnableEncodings(true) 143 | urlTransfer.RetainBodyOnError(true) 144 | if messagePort = invalid then 145 | messagePort = CreateObject("roMessagePort") 146 | end if 147 | urlTransfer.SetMessagePort(messagePort) 148 | urlTransfer.SetHeaders(m.headers) 149 | postMethods = {"PUT":true,"POST":true,"PATCH":true} 150 | if postMethods.DoesExist(m.method) then 151 | urlTransfer.AsyncPostFromString(m.body) 152 | else 153 | urlTransfer.AsyncGetToString() 154 | end if 155 | m.urlTransfer = urlTransfer 156 | if synchronous then 157 | result = invalid 158 | while m.urlEvent = invalid 159 | message = wait(0, messagePort) 160 | result = m.handleEvent(message) 161 | end while 162 | return result 163 | else 164 | return m.urlTransfer.GetMessagePort() 165 | end if 166 | End Function 167 | Function rodash_request_cancel_() 168 | if m.urlTransfer <> invalid and type(m.urlTransfer) = "roUrlTransfer" then 169 | m.urlTransfer.AsyncCancel() 170 | m.urlTransfer = invalid 171 | end if 172 | End Function 173 | Function rodash_request_handleEvent_(message As Object) As Object 174 | if type(message) = "roUrlEvent" and m.urlTransfer.GetIdentity() = message.GetSourceIdentity() then 175 | m.urlEvent = message 176 | if message.GetResponseCode() > 0 then 177 | return message.GetString() 178 | else 179 | return invalid 180 | end if 181 | else 182 | return invalid 183 | end if 184 | End Function 185 | Function rodash_getDeviceProfile_() As Object 186 | ai = CreateObject("roAppInfo") 187 | di = CreateObject("roDeviceInfo") 188 | profile = { 189 | appInfo: { 190 | id: ai.GetID() 191 | version: ai.GetVersion() 192 | itle: ai.GetTitle() 193 | subtitle: ai.GetSubtitle() 194 | devid: ai.GetDevID() 195 | isDev: ai.IsDev() 196 | } 197 | deviceInfo: { 198 | model: di.GetModel() 199 | modelDetails: di.GetModelDetails() 200 | modelDisplayName: di.GetModelDisplayName() 201 | friendlyName: di.GetFriendlyName() 202 | version: di.GetVersion() 203 | uniqueId: di.GetDeviceUniqueId() 204 | advertisingId: di.GetAdvertisingId() 205 | adTrackingDisabled: di.IsAdIdTrackingDisabled() 206 | rackingId: di.GetClientTrackingId() 207 | imeZone: di.GetTimeZone() 208 | features: { 209 | "5.1_surround_sound": di.HasFeature("5.1_surround_sound") 210 | "can_output_5.1_surround_sound": di.HasFeature("can_output_5.1_surround_sound") 211 | "sd_only_hardware": di.HasFeature("sd_only_hardware") 212 | "usb_hardware": di.HasFeature("usb_hardware") 213 | "1080p_hardware": di.HasFeature("1080p_hardware") 214 | "sdcard_hardware": di.HasFeature("sdcard_hardware") 215 | "ethernet_hardware": di.HasFeature("ethernet_hardware") 216 | "gaming_hardware": di.HasFeature("gaming_hardware") 217 | "bluetooth_hardware": di.HasFeature("bluetooth_hardware") 218 | } 219 | locale: di.GetCurrentLocale() 220 | country: di.GetCountryCode() 221 | drm: di.GetDrmInfo() 222 | displayType: di.GetDisplayType() 223 | displayMode: di.GetDisplayMode() 224 | displayAspectRatio: di.GetDisplayAspectRatio() 225 | displaySize: di.GetDisplaySize() 226 | videoMode: di.GetVideoMode() 227 | displayProperties: di.GetDisplayProperties() 228 | supportedGraphicsResolutions: di.GetSupportedGraphicsResolutions() 229 | uiResolution: di.GetUIResolution() 230 | graphicsPlatform: di.GetGraphicsPlatform() 231 | videoDecodeInfo: di.GetVideoDecodeInfo() 232 | audioOutputChannel: di.GetAudioOutputChannel() 233 | audioDecodeInfo: di.GetAudioDecodeInfo() 234 | } 235 | } 236 | ic = CreateObject("roImageCanvas") 237 | if ic <> invalid 238 | profile.imageCanvas = { 239 | canvasRect: ic.GetCanvasRect() 240 | } 241 | end if 242 | return profile 243 | End Function 244 | Function rodash_difference_(first, second) 245 | result = [] 246 | for each f in first 247 | result.push(f) 248 | for each s in second 249 | if m.equal(s,f) then result.pop() 250 | end for 251 | end for 252 | return result 253 | End Function 254 | Function rodash_empty_(value) 255 | if value = invalid then return true 256 | if GetInterface(value, "ifAssociativeArray") <> invalid 257 | return (value.count() = 0) 258 | else if GetInterface(value, "ifArray") <> invalid 259 | return (value.count() = 0) 260 | else if (type(value) = "roString") or (type(value) = "String") 261 | return (value = "") ' works for native and object strings 262 | end if 263 | return false 264 | End Function 265 | Function rodash_equal_(a, b) 266 | compare = false 267 | result = eval("if a = b then compare = true") 268 | if not compare 269 | if type(a) = type(b) 270 | if type(a) = invalid 271 | compare = true 272 | end if 273 | end if 274 | end if 275 | return compare 276 | End Function 277 | Function rodash_get_(aa, path, default=invalid) 278 | if aa = invalid or type(aa) <> "roAssociativeArray" then return default 279 | segments = m.pathAsArray_(path) 280 | if segments = invalid then return default 281 | result = invalid 282 | while segments.count() > 0 283 | key = segments.shift() 284 | if not aa.doesExist(key) 285 | exit while 286 | end if 287 | value = aa.lookup(key) 288 | if segments.count() = 0 289 | result = value 290 | exit while 291 | end if 292 | if value = invalid or type(value) <> "roAssociativeArray" 293 | exit while 294 | end if 295 | aa = value 296 | end while 297 | if result = invalid then return default 298 | return result 299 | End Function 300 | Function rodash_indexOf_(array, value) 301 | if array = invalid or value = invalid or GetInterface(array, "ifArray") = invalid then return -1 302 | for i=0 to array.count()-1 303 | if m.equal(array[i], value) then return i 304 | end for 305 | return -1 306 | End Function 307 | Function rodash_intersection_(first, second) 308 | result = [] 309 | for each f in first 310 | for each s in second 311 | if m.equal(s,f) then result.push(f) 312 | end for 313 | end for 314 | return result 315 | End Function 316 | Function rodash_getManifest_(path="pkg:/manifest" As String) 317 | file = ReadAsciiFile(path) 318 | lines = file.split(Chr(10)) 319 | manifest = {} 320 | for each line in lines 321 | line = line.trim() 322 | if line.left(1) <> "#" and line.len() <> 0 then 323 | equal = line.instr(0, "=") 324 | if equal = -1 then equal = line.len() 325 | key = line.left(equal) 326 | value = line.mid(equal+1) 327 | manifest[key] = value 328 | end if 329 | end for 330 | return manifest 331 | End Function 332 | Function rodash_map_(object, fn) 333 | if type(fn) <> "Function" 334 | fn = Function(x): return x: End FUnction 335 | end if 336 | if GetInterface(object, "ifArray") <> invalid 337 | result = [] 338 | for each o in object 339 | result.push(fn(o)) 340 | end for 341 | return result 342 | else 343 | return [] 344 | end if 345 | End Function 346 | Function rodash_max_(a,b) 347 | max = invalid 348 | result = eval("if a >= b then: max = a: else: max = b: end if") 349 | return max 350 | End Function 351 | Function rodash_min_(a,b) 352 | min = invalid 353 | result = eval("if a <= b then: min = a: else: min = b: end if") 354 | return min 355 | End Function 356 | Function rodash_pathAsArray_(path) 357 | pathRE = CreateObject("roRegex", "\[([0-9]+)\]", "i") 358 | segments = [] 359 | if type(path) = "String" or type(path) = "roString" 360 | dottedPath = pathRE.replaceAll(path, ".\1") 361 | stringSegments = dottedPath.tokenize(".") 362 | for each s in stringSegments 363 | if (Asc(s) >= 48) and (Asc(s) <= 57) 364 | segments.push(s.toInt()) 365 | else 366 | segments.push(s) 367 | end if 368 | end for 369 | else if type(path) = "roList" or type(path) = "roArray" 370 | stringPath = "" 371 | for each s in path 372 | stringPath = stringPath + "." + Box(s).toStr() 373 | end for 374 | segments = m.pathAsArray_(stringPath) 375 | else 376 | segments = invalid 377 | end if 378 | return segments 379 | End Function 380 | Function rodash_regRead_(sectionName As String, key As String) As Dynamic 381 | sectionName = LCase(sectionName) 382 | key = LCase(key) 383 | registry = CreateObject("roRegistry") 384 | section = CreateObject("roRegistrySection", sectionName) 385 | if section.Exists(key) then 386 | return ParseJson(section.Read(key)) 387 | else 388 | return invalid 389 | end if 390 | End Function 391 | Function rodash_regWrite_(sectionName As String, key As String, value As Dynamic) As Void 392 | sectionName = LCase(sectionName) 393 | key = LCase(key) 394 | registry = CreateObject("roRegistry") 395 | section = CreateObject("roRegistrySection", sectionName) 396 | section.Write(key, FormatJson(value)) 397 | section.Flush() 398 | registry.Flush() 399 | End Function 400 | Function rodash_regDelete_(sectionName As String, key As String) As Void 401 | sectionName = LCase(sectionName) 402 | key = LCase(key) 403 | registry = CreateObject("roRegistry") 404 | section = CreateObject("roRegistrySection", sectionName) 405 | if section.Exists(key) then section.Delete(key) 406 | section.Flush() 407 | registry.Flush() 408 | End Function 409 | Function rodash_regReadAll_() As Object 410 | registry = CreateObject("roRegistry") 411 | sections = registry.GetSectionList() 412 | data = {} 413 | for each sectionName in sections 414 | section = CreateObject("roRegistrySection", sectionName) 415 | keys = section.GetKeyList() 416 | sectionData = {} 417 | for each k in keys 418 | sectionData[k] = ParseJson(section.Read(k)) 419 | end for 420 | data[sectionName] = sectionData 421 | end for 422 | return data 423 | End Function 424 | Function rodash_regWriteAll_(data As Object) As Void 425 | registry = CreateObject("roRegistry") 426 | if data <> invalid and type(data) = "roAssociativeArray" then 427 | for each sectionName in data 428 | sectionData = data[sectionName] 429 | sectionName = LCase(Box(sectionName.toStr())) ' force it to a roString 430 | section = CreateObject("roRegistrySection", sectionName) 431 | if sectionData <> invalid and type(sectionData) = "roAssociativeArray" then 432 | for each key in sectionData 433 | value = sectionData[key] 434 | key = LCase(Box(key.toStr())) 435 | section.Write(key, FormatJson(value)) 436 | end for 437 | end if 438 | section.Flush() 439 | end for 440 | end if 441 | registry.Flush() 442 | End Function 443 | Function rodash_regDeleteAll_() As Void 444 | registry = CreateObject("roRegistry") 445 | sections = registry.GetSectionList() 446 | for each sectionName in sections 447 | registry.Delete(sectionName) 448 | end for 449 | registry.Flush() 450 | End Function 451 | Function rodash() 452 | return { 453 | intersection: rodash_intersection_ 454 | difference: rodash_difference_ 455 | equal: rodash_equal_ 456 | get: rodash_get_ 457 | set: rodash_set_ 458 | getManifest: rodash_getManifest_ 459 | regRead: rodash_regRead_ 460 | regWrite: rodash_regWrite_ 461 | regDelete: rodash_regDelete_ 462 | regReadAll: rodash_regReadAll_ 463 | regWriteAll: rodash_regWriteAll_ 464 | regDeleteAll: rodash_regDeleteAll_ 465 | createRequest: rodash_createRequest_ 466 | uriEncodeParams: rodash_uri_encodeParams_ 467 | uriParse: rodash_uri_parse_ 468 | empty: rodash_empty_ 469 | clone: rodash_clone_ 470 | cloneDeep: rodash_cloneDeep_ 471 | cond: rodash_cond_ 472 | map: rodash_map_ 473 | indexOf: rodash_indexOf_ 474 | min: rodash_min_ 475 | max: rodash_max_ 476 | getDeviceProfile: rodash_getDeviceProfile_ 477 | pathAsArray_: rodash_pathAsArray_ 478 | cloneNode_: rodash_cloneNode_ 479 | cloneAssocArray_: rodash_cloneAssocArray_ 480 | cloneArrayish_: rodash_cloneArrayish_ 481 | cloneStringish_: rodash_cloneStringish_ 482 | uriSimpleParse_: rodash_uri_simpleParse_ 483 | } 484 | End Function 485 | Function rodash_set_(aa, path, value) 486 | if aa = invalid or type(aa) <> "roAssociativeArray" then return aa 487 | segments = m.pathAsArray_(path) 488 | if segments = invalid then return aa 489 | walk = aa 490 | while segments.count() > 0 491 | key = segments.shift() 492 | if segments.count() = 0 493 | walk.addReplace(key, value) 494 | exit while 495 | end if 496 | lookup = walk.lookup(key) 497 | if lookup = invalid or type(lookup) <> "roAssociativeArray" 498 | walk.addReplace(key, {}) 499 | end if 500 | walk = walk.lookup(key) 501 | end while 502 | return aa 503 | End Function 504 | Function rodash_uri_encodeParams_(params As Object) As String 505 | encoded = "" 506 | if params <> invalid and type(params) = "roAssociativeArray" then 507 | for each param in params.Keys() 508 | value = params[param] 509 | if value = invalid then 510 | value = "" 511 | else 512 | value = value.toStr() ' force to roString 513 | if FindMemberFunction(value, "EncodeUriComponent") <> invalid then 514 | value = value.EncodeUriComponent() 515 | else 516 | ransferEncoder = CreateObject("roUrlTransfer") 517 | value = transferEncoder.Escape(value) 518 | end if 519 | end if 520 | encoded = encoded + param.toStr() + "=" + value + "&" 521 | end for 522 | if Right(encoded, 1) = "&" then 523 | encoded = Left(encoded, Len(encoded)-1) 524 | end if 525 | end if 526 | return encoded 527 | End Function 528 | Function rodash_uri_parse_(input) 529 | return m.uriSimpleParse_(input) 530 | End Function 531 | Function rodash_uri_simpleParse_(input As String) 532 | result = { 533 | scheme: "" 534 | port: "" 535 | hostname: "" 536 | pathname: "" 537 | search: "" 538 | hash: "" 539 | } 540 | scheme_end = input.instr("//") 541 | if scheme_end = -1 then scheme_end = input.len() 542 | result.scheme = input.left(scheme_end) 543 | input = input.mid(scheme_end + 2) 544 | hostname_end = input.instr(":") 545 | if hostname_end = -1 then 546 | hostname_end = input.instr("/") 547 | if hostname_end = -1 then hostname_end = input.len() 548 | end if 549 | result.hostname = input.left(hostname_end) 550 | input = input.mid(hostname_end) 551 | if input.Left(1) = ":" then 552 | port_end = input.instr("/") 553 | if port_end = -1 then port_end = input.len() 554 | result.port = input.mid(1, port_end-1) ' take the ':' prefix off 555 | input = input.mid(port_end) 556 | end if 557 | path_end = input.instr("?") 558 | if path_end = -1 then 559 | path_end = input.instr("#") 560 | if path_end = -1 then path_end = input.len() 561 | end if 562 | result.pathname = input.left(path_end) 563 | input = input.mid(path_end) 564 | if input.left(1) = "?" then 565 | search_end = input.instr("#") 566 | if search_end = -1 then search_end = input.len() 567 | result.search = input.left(search_end) 568 | input = input.mid(search_end) 569 | end if 570 | if input.left(1) = "#" then 571 | hash_end = input.len() 572 | result.hash = input.left(hash_end) 573 | end if 574 | return result 575 | End Function -------------------------------------------------------------------------------- /src/Parser.ts: -------------------------------------------------------------------------------- 1 | import { EOF, IToken, Lexer, Parser } from "chevrotain"; 2 | import { map } from "lodash"; 3 | import { Token } from "./AST"; 4 | 5 | import { 6 | ADDICTIVE_OPERATOR, 7 | ALL_TOKENS, 8 | AS, 9 | ATTRIBUTE, 10 | BASE_TYPE, 11 | BOOLEAN, 12 | CLOSE_BRACKET, 13 | CLOSE_CURLY_BRACE, 14 | CLOSE_PAREN, 15 | COLON, 16 | COMMA, 17 | COMMENT_QUOTE, 18 | COMMENT_REM, 19 | CONDITIONAL_CONST, 20 | CONDITIONAL_ELSE, 21 | CONDITIONAL_ELSE_IF, 22 | CONDITIONAL_END_IF, 23 | CONDITIONAL_ERROR, 24 | CONDITIONAL_IF, 25 | DIM, 26 | EACH, 27 | ELSE, 28 | ELSE_IF, 29 | END, 30 | END_FOR, 31 | END_FUNCTION, 32 | END_IF, 33 | END_SUB, 34 | END_WHILE, 35 | EQUAL, 36 | EQUALITY_OPERATOR, 37 | EXIT_FOR, 38 | EXIT_WHILE, 39 | FOR, 40 | FUNCTION, 41 | GOTO, 42 | IDENTIFIER, 43 | IF, 44 | IN, 45 | INTEGER, 46 | LIBRARY, 47 | LITERAL, 48 | LOGIC_OPERATOR, 49 | LONGINTEGER, 50 | MOD, 51 | MULTI_OPERATOR, 52 | NEWLINE, 53 | NEXT, 54 | NUMBER_LITERAL, 55 | OBJECT, 56 | OPEN_BRACKET, 57 | OPEN_CURLY_BRACE, 58 | OPEN_PAREN, 59 | PERIOD, 60 | POSTFIX, 61 | PRINT, 62 | RELATIONAL_OPERATOR, 63 | RETURN, 64 | SEMICOLON, 65 | SHIFT_OPERATOR, 66 | STEP, 67 | STOP, 68 | STRING, 69 | STRING_LITERAL, 70 | SUB, 71 | TERMINATOR, 72 | THEN, 73 | TO, 74 | TYPE_DECLARATION, 75 | UNARY, 76 | WHILE, 77 | TRY, 78 | CATCH, 79 | END_TRY, 80 | } from "./Tokens"; 81 | 82 | const BRSLexer = new Lexer(ALL_TOKENS, { 83 | deferDefinitionErrorsHandling: true, 84 | ensureOptimizations: true, 85 | positionTracking: "onlyStart", 86 | }); 87 | 88 | const operator = { LABEL: "operator" }; 89 | const right = { LABEL: "right" }; 90 | const left = { LABEL: "left" }; 91 | const body = { LABEL: "body" }; 92 | const Declaration = { LABEL: "Declaration" }; 93 | const Empty = { LABEL: "Empty" }; 94 | 95 | const Statement = { LABEL: "Statement" }; 96 | 97 | export class RokuBRSParser extends Parser { 98 | public Program = this.RULE("Program", () => { 99 | this.MANY(() => { 100 | this.OR([ 101 | { ALT: () => this.SUBRULE(this.FunctionDeclaration, Declaration) }, 102 | { ALT: () => this.SUBRULE(this.LibraryStatement, Declaration) }, 103 | { ALT: () => this.SUBRULE(this.SubDeclaration, Declaration) }, 104 | { 105 | ALT: () => 106 | this.SUBRULE(this.ConditionalCompilationStatement, Declaration), 107 | }, 108 | { ALT: () => this.SUBRULE(this.EmptyStatement, Declaration) }, 109 | { ALT: () => this.SUBRULE(this.Comment, Declaration) }, 110 | ]); 111 | }); 112 | 113 | this.OPTION(() => { 114 | this.CONSUME(EOF); 115 | }); 116 | }); 117 | 118 | protected BlockStatement = this.RULE("BlockStatement", () => { 119 | this.MANY(() => { 120 | this.OR([ 121 | { ALT: () => this.SUBRULE(this.Statement, body) }, 122 | { ALT: () => this.CONSUME(TERMINATOR) }, 123 | ]); 124 | }); 125 | }); 126 | 127 | protected Statement = this.RULE("Statement", () => { 128 | this.OR2( 129 | this.cacheStatement || 130 | (this.cacheStatement = [ 131 | { ALT: () => this.SUBRULE(this.EmptyStatement, Empty) }, 132 | { ALT: () => this.SUBRULE(this.ForStatement, Statement) }, 133 | { ALT: () => this.SUBRULE(this.IfStatement, Statement) }, 134 | { ALT: () => this.SUBRULE(this.ForEachStatement, Statement) }, 135 | { ALT: () => this.SUBRULE(this.WhileStatement, Statement) }, 136 | { ALT: () => this.SUBRULE(this.RokuTryStatement, Statement) }, 137 | { ALT: () => this.SUBRULE(this.PrintStatement, Statement) }, 138 | { ALT: () => this.SUBRULE(this.ReturnStatement, Statement) }, 139 | { ALT: () => this.SUBRULE(this.StopStatement, Statement) }, 140 | { ALT: () => this.SUBRULE(this.GoToStatement, Statement) }, 141 | { ALT: () => this.SUBRULE(this.LabeledStatement, Statement) }, 142 | 143 | { 144 | ALT: () => 145 | this.SUBRULE(this.ConditionalCompilationStatement, Statement), 146 | }, 147 | { ALT: () => this.SUBRULE(this.DimStatement, Statement) }, 148 | { ALT: () => this.SUBRULE(this.NextStatement, Statement) }, 149 | { ALT: () => this.SUBRULE(this.ExitStatement, Statement) }, 150 | 151 | { ALT: () => this.SUBRULE(this.ExpressionStatement, Statement) }, 152 | ]) 153 | ); 154 | 155 | this.OPTION(() => { 156 | this.SUBRULE2(this.Comment, { LABEL: "trailingComments" }); 157 | }); 158 | }); 159 | 160 | protected ArrayExpression = this.RULE("ArrayExpression", () => { 161 | this.CONSUME(OPEN_BRACKET); 162 | this.MANY(() => { 163 | this.OR([ 164 | { ALT: () => this.SUBRULE(this.ArrayElement, { LABEL: "elements" }) }, 165 | { ALT: () => this.CONSUME(COMMA) }, 166 | { 167 | ALT: () => 168 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }), 169 | }, 170 | // 171 | ]); 172 | }); 173 | this.CONSUME(CLOSE_BRACKET); 174 | }); 175 | 176 | protected ObjectExpression = this.RULE("ObjectExpression", () => { 177 | this.CONSUME(OPEN_CURLY_BRACE); 178 | this.MANY(() => { 179 | this.OR([ 180 | { ALT: () => this.SUBRULE(this.Property, { LABEL: "properties" }) }, 181 | { ALT: () => this.CONSUME(COMMA) }, 182 | { 183 | ALT: () => 184 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }), 185 | }, 186 | // 187 | ]); 188 | }); 189 | this.CONSUME(CLOSE_CURLY_BRACE); 190 | }); 191 | 192 | protected Property = this.RULE("Property", () => { 193 | this.SUBRULE(this.PropertyName, { LABEL: "key" }); 194 | this.CONSUME(COLON); 195 | this.SUBRULE(this.AssignmentExpression, { LABEL: "value" }); 196 | }); 197 | 198 | protected ArrayElement = this.RULE("ArrayElement", () => { 199 | this.SUBRULE(this.AssignmentExpression, { LABEL: "value" }); 200 | this.OPTION(() => { 201 | this.CONSUME(COMMA); 202 | }); 203 | }); 204 | 205 | protected PropertyName = this.RULE("PropertyName", () => { 206 | this.OR([ 207 | { ALT: () => this.SUBRULE(this.Identifier) }, 208 | { ALT: () => this.SUBRULE(this.ReservedWord) }, 209 | { ALT: () => this.CONSUME(STRING_LITERAL) }, 210 | { ALT: () => this.CONSUME(NUMBER_LITERAL) }, 211 | ]); 212 | }); 213 | 214 | protected DimStatement = this.RULE("DimStatement", () => { 215 | this.CONSUME(DIM); 216 | this.SUBRULE(this.Identifier); 217 | this.SUBRULE(this.ArrayExpression); 218 | }); 219 | 220 | protected EmptyStatement = this.RULE("EmptyStatement", () => { 221 | this.OPTION(() => { 222 | this.SUBRULE2(this.Comment, { LABEL: "trailingComments" }); 223 | }); 224 | this.CONSUME(NEWLINE); 225 | }); 226 | 227 | protected ExitStatement = this.RULE("ExitStatement", () => { 228 | this.OR([ 229 | { ALT: () => this.CONSUME(EXIT_WHILE) }, 230 | { ALT: () => this.CONSUME(EXIT_FOR) }, 231 | // 232 | ]); 233 | }); 234 | 235 | protected IfStatement = this.RULE("IfStatement", () => { 236 | let isBlock = false; 237 | 238 | this.CONSUME(IF); 239 | this.SUBRULE1(this.ExpressionStatement, { LABEL: "test" }); 240 | 241 | this.OPTION1(() => { 242 | this.CONSUME(THEN); 243 | }); 244 | 245 | this.OPTION2(() => { 246 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 247 | isBlock = true; 248 | }); 249 | 250 | this.OPTION4(() => { 251 | if (isBlock) { 252 | this.SUBRULE2(this.BlockStatement, { LABEL: "consequent" }); 253 | } else { 254 | this.SUBRULE2(this.Statement, { LABEL: "consequent" }); 255 | } 256 | }); 257 | 258 | this.MANY(() => { 259 | this.SUBRULE(this.ElseIfStatement, { LABEL: "alternate" }); 260 | }); 261 | 262 | this.OPTION5(() => { 263 | this.SUBRULE(this.ElseStatement, { LABEL: "alternate" }); 264 | }); 265 | 266 | this.OPTION6(() => { 267 | this.CONSUME(END_IF); 268 | }); 269 | }); 270 | 271 | protected ElseIfStatement = this.RULE("ElseIfStatement", () => { 272 | this.CONSUME(ELSE_IF); 273 | this.SUBRULE(this.ExpressionStatement, { LABEL: "test" }); 274 | 275 | this.OPTION1(() => { 276 | this.CONSUME(THEN); 277 | }); 278 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 279 | 280 | this.OPTION5(() => { 281 | this.SUBRULE2(this.BlockStatement, body); 282 | }); 283 | }); 284 | 285 | protected ElseStatement = this.RULE("ElseStatement", () => { 286 | let isBlock = false; 287 | 288 | this.CONSUME(ELSE); 289 | 290 | this.OPTION1(() => { 291 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 292 | isBlock = true; 293 | }); 294 | 295 | this.OPTION2(() => { 296 | if (isBlock) { 297 | this.SUBRULE2(this.BlockStatement, body); 298 | } else { 299 | this.SUBRULE2(this.Statement, body); 300 | } 301 | }); 302 | }); 303 | 304 | protected ForStatement = this.RULE("ForStatement", () => { 305 | this.CONSUME(FOR); 306 | this.SUBRULE1(this.Identifier, { LABEL: "counter" }); 307 | this.CONSUME(EQUAL); 308 | this.SUBRULE(this.AssignmentExpression, { LABEL: "init" }); 309 | this.CONSUME(TO); 310 | this.SUBRULE2(this.ExpressionStatement, { LABEL: "test" }); 311 | 312 | this.OPTION(() => { 313 | this.CONSUME(STEP); 314 | this.SUBRULE3(this.ExpressionStatement, { LABEL: "update" }); 315 | }); 316 | 317 | this.SUBRULE4(this.EndOfStatement, { LABEL: "trailingComments" }); 318 | 319 | this.OPTION5(() => { 320 | this.SUBRULE5(this.BlockStatement, body); 321 | }); 322 | 323 | this.OPTION1(() => { 324 | this.SUBRULE(this.NextStatement); 325 | }); 326 | 327 | this.OPTION2(() => { 328 | this.CONSUME(END_FOR); 329 | }); 330 | }); 331 | 332 | protected ForEachStatement = this.RULE("ForEachStatement", () => { 333 | this.CONSUME(FOR); 334 | this.CONSUME(EACH); 335 | this.SUBRULE1(this.Identifier, { LABEL: "counter" }); 336 | this.CONSUME(IN); 337 | this.SUBRULE2(this.ExpressionStatement, { LABEL: "countExpression" }); 338 | this.SUBRULE3(this.EndOfStatement, { LABEL: "trailingComments" }); 339 | 340 | this.SUBRULE(this.BlockStatement, body); 341 | 342 | this.OPTION1(() => { 343 | this.SUBRULE(this.NextStatement); 344 | }); 345 | this.OPTION2(() => { 346 | this.CONSUME(END_FOR); 347 | }); 348 | }); 349 | 350 | protected GoToStatement = this.RULE("GoToStatement", () => { 351 | this.CONSUME(GOTO); 352 | this.SUBRULE1(this.Identifier); 353 | }); 354 | 355 | protected LabeledStatement = this.RULE("LabeledStatement", () => { 356 | this.SUBRULE(this.Identifier, { LABEL: "label" }); 357 | this.CONSUME(COLON); 358 | this.CONSUME(NEWLINE); 359 | this.SUBRULE(this.Statement, { LABEL: "body" }); 360 | }); 361 | 362 | protected LibraryStatement = this.RULE("LibraryStatement", () => { 363 | this.CONSUME(LIBRARY); 364 | this.CONSUME(STRING_LITERAL, { LABEL: "path" }); 365 | }); 366 | 367 | protected NextStatement = this.RULE("NextStatement", () => { 368 | this.CONSUME(NEXT); 369 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 370 | }); 371 | 372 | protected PrintStatement = this.RULE("PrintStatement", () => { 373 | this.CONSUME(PRINT); 374 | this.MANY(() => { 375 | this.OR([ 376 | { 377 | ALT: () => this.SUBRULE(this.ExpressionStatement, { LABEL: "value" }), 378 | }, 379 | { ALT: () => this.CONSUME(COMMA) }, 380 | { ALT: () => this.CONSUME(SEMICOLON) }, 381 | // 382 | ]); 383 | }); 384 | }); 385 | 386 | protected ReturnStatement = this.RULE("ReturnStatement", () => { 387 | this.CONSUME(RETURN); 388 | this.OPTION1(() => { 389 | this.SUBRULE(this.ExpressionStatement, { LABEL: "argument" }); 390 | }); 391 | }); 392 | 393 | protected StopStatement = this.RULE("StopStatement", () => { 394 | this.CONSUME(STOP); 395 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 396 | }); 397 | 398 | protected WhileStatement = this.RULE("WhileStatement", () => { 399 | this.CONSUME(WHILE); 400 | this.SUBRULE(this.ExpressionStatement, { LABEL: "test" }); 401 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 402 | this.SUBRULE(this.BlockStatement, body); 403 | this.CONSUME(END_WHILE); 404 | }); 405 | 406 | protected RokuTryStatement = this.RULE("RokuTryStatement", () => { 407 | this.CONSUME(TRY); 408 | this.SUBRULE1(this.BlockStatement, { LABEL: "body" }); 409 | this.CONSUME(CATCH); 410 | this.SUBRULE(this.ExpressionStatement, { LABEL: "exception" }); 411 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 412 | this.SUBRULE2(this.BlockStatement, { LABEL: "onError" }); 413 | this.CONSUME(END_TRY); 414 | }); 415 | 416 | protected FunctionExpression = this.RULE("FunctionExpression", () => { 417 | this.CONSUME(FUNCTION); 418 | 419 | this.OPTION(() => { 420 | this.SUBRULE(this.ParameterList, { LABEL: "params" }); 421 | }); 422 | 423 | this.OPTION1(() => { 424 | this.CONSUME(AS); 425 | this.SUBRULE(this.TypeAnnotation, { LABEL: "ReturnType" }); 426 | }); 427 | 428 | this.OPTION2(() => { 429 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 430 | }); 431 | 432 | this.OPTION3(() => { 433 | this.SUBRULE(this.BlockStatement, body); 434 | }); 435 | 436 | this.CONSUME(END_FUNCTION); 437 | }); 438 | 439 | protected FunctionDeclaration = this.RULE("FunctionDeclaration", () => { 440 | this.CONSUME(FUNCTION); 441 | this.SUBRULE(this.UnTypedIdentifier, { LABEL: "id" }); 442 | 443 | this.OPTION(() => { 444 | this.SUBRULE(this.ParameterList, { LABEL: "params" }); 445 | }); 446 | 447 | this.OPTION1(() => { 448 | this.CONSUME(AS); 449 | this.SUBRULE(this.TypeAnnotation, { LABEL: "ReturnType" }); 450 | }); 451 | 452 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 453 | 454 | this.OPTION3(() => { 455 | this.SUBRULE(this.BlockStatement, body); 456 | }); 457 | this.CONSUME(END_FUNCTION); 458 | }); 459 | 460 | protected SubExpression = this.RULE("SubExpression", () => { 461 | this.CONSUME(SUB); 462 | 463 | this.OPTION(() => { 464 | this.SUBRULE(this.ParameterList, { LABEL: "params" }); 465 | }); 466 | 467 | this.OPTION1(() => { 468 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 469 | }); 470 | 471 | this.OPTION2(() => { 472 | this.SUBRULE(this.BlockStatement, body); 473 | }); 474 | 475 | this.CONSUME(END_SUB); 476 | }); 477 | 478 | protected SubDeclaration = this.RULE("SubDeclaration", () => { 479 | this.CONSUME(SUB); 480 | this.SUBRULE(this.UnTypedIdentifier, { LABEL: "id" }); 481 | 482 | this.OPTION1(() => { 483 | this.SUBRULE(this.ParameterList, { LABEL: "params" }); 484 | }); 485 | 486 | this.OPTION2(() => { 487 | this.CONSUME(AS); 488 | this.SUBRULE(this.TypeAnnotation, { LABEL: "ReturnType" }); 489 | }); 490 | 491 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 492 | 493 | this.OPTION3(() => { 494 | this.SUBRULE(this.BlockStatement, body); 495 | }); 496 | 497 | this.CONSUME(END_SUB); 498 | }); 499 | 500 | protected ParameterList = this.RULE("ParameterList", () => { 501 | this.CONSUME(OPEN_PAREN); 502 | 503 | this.MANY_SEP({ 504 | SEP: COMMA, 505 | DEF() { 506 | this.SUBRULE2(this.Parameter); 507 | }, 508 | }); 509 | 510 | this.CONSUME(CLOSE_PAREN); 511 | }); 512 | 513 | protected Parameter = this.RULE("Parameter", () => { 514 | this.OR([ 515 | { ALT: () => this.SUBRULE(this.Literal) }, 516 | { ALT: () => this.SUBRULE(this.Identifier) }, 517 | { ALT: () => this.SUBRULE(this.ReservedWord) }, 518 | // 519 | ]); 520 | 521 | this.OPTION(() => { 522 | this.OPTION1(() => { 523 | this.CONSUME(EQUAL); 524 | this.SUBRULE(this.AssignmentExpression, { LABEL: "value" }); 525 | }); 526 | 527 | this.OPTION2(() => { 528 | this.CONSUME(AS); 529 | this.SUBRULE(this.TypeAnnotation); 530 | }); 531 | }); 532 | }); 533 | 534 | protected TypeAnnotation = this.RULE("TypeAnnotation", () => { 535 | this.CONSUME(BASE_TYPE); 536 | }); 537 | 538 | protected ExpressionStatement = this.RULE("ExpressionStatement", () => { 539 | this.SUBRULE(this.AssignmentExpression); 540 | }); 541 | 542 | protected AssignmentExpression = this.RULE("AssignmentExpression", () => { 543 | this.SUBRULE(this.AdditionExpression); 544 | }); 545 | 546 | protected AdditionExpression = this.RULE("AdditionExpression", () => { 547 | this.SUBRULE1(this.MultiplicationExpression, { LABEL: "left" }); 548 | this.MANY(() => { 549 | this.CONSUME(ADDICTIVE_OPERATOR); 550 | this.SUBRULE2(this.MultiplicationExpression, right); 551 | }); 552 | }); 553 | 554 | protected MultiplicationExpression = this.RULE( 555 | "MultiplicationExpression", 556 | () => { 557 | this.SUBRULE1(this.ShiftExpression, left); 558 | this.MANY(() => { 559 | this.CONSUME(MULTI_OPERATOR); 560 | this.SUBRULE2(this.ShiftExpression, right); 561 | }); 562 | } 563 | ); 564 | 565 | protected ShiftExpression = this.RULE("ShiftExpression", () => { 566 | this.SUBRULE1(this.RelationExpression, left); 567 | this.MANY(() => { 568 | this.CONSUME(SHIFT_OPERATOR); 569 | this.SUBRULE2(this.RelationExpression, right); 570 | }); 571 | }); 572 | 573 | protected RelationExpression = this.RULE("RelationExpression", () => { 574 | this.SUBRULE1(this.EqualityExpression, left); 575 | this.MANY(() => { 576 | this.CONSUME(RELATIONAL_OPERATOR); 577 | this.SUBRULE2(this.EqualityExpression, right); 578 | }); 579 | }); 580 | 581 | protected EqualityExpression = this.RULE("EqualityExpression", () => { 582 | this.SUBRULE1(this.LogicExpression, left); 583 | this.MANY(() => { 584 | this.CONSUME(EQUALITY_OPERATOR); 585 | this.SUBRULE2(this.LogicExpression, right); 586 | }); 587 | }); 588 | 589 | protected LogicExpression = this.RULE("LogicExpression", () => { 590 | this.SUBRULE1(this.UnaryExpression, left); 591 | this.MANY(() => { 592 | this.CONSUME(LOGIC_OPERATOR); 593 | this.SUBRULE2(this.UnaryExpression, right); 594 | }); 595 | }); 596 | 597 | protected UnaryExpression = this.RULE("UnaryExpression", () => { 598 | this.OR([ 599 | { ALT: () => this.SUBRULE(this.PostfixExpression) }, 600 | { 601 | ALT: () => { 602 | this.CONSUME(UNARY); 603 | this.SUBRULE(this.UnaryExpression, right); 604 | }, 605 | }, 606 | ]); 607 | }); 608 | 609 | protected Arguments = this.RULE("Arguments", () => { 610 | this.CONSUME(OPEN_PAREN); 611 | 612 | this.MANY_SEP({ 613 | SEP: COMMA, 614 | DEF() { 615 | this.SUBRULE(this.AssignmentExpression, { LABEL: "param" }); 616 | }, 617 | }); 618 | 619 | this.CONSUME(CLOSE_PAREN); 620 | }); 621 | 622 | protected PostfixExpression = this.RULE("PostfixExpression", () => { 623 | this.SUBRULE(this.MemberExpression, left); 624 | this.OPTION(() => this.CONSUME(POSTFIX)); 625 | }); 626 | 627 | protected MemberExpression = this.RULE("MemberExpression", () => { 628 | this.SUBRULE(this.PrimaryExpression, { LABEL: "id" }); 629 | this.OPTION1(() => this.SUBRULE1(this.Arguments, { LABEL: "args" })); 630 | 631 | this.MANY(() => { 632 | this.SUBRULE(this.MemberChunkExpression, { LABEL: "properties" }); 633 | }); 634 | }); 635 | 636 | protected MemberChunkExpression = this.RULE("MemberChunkExpression", () => { 637 | this.OR([ 638 | { ALT: () => this.SUBRULE(this.ArrayExpression, { LABEL: "property" }) }, 639 | { 640 | ALT: () => 641 | this.SUBRULE(this.DotMemberExpression, { LABEL: "property" }), 642 | }, 643 | ]); 644 | 645 | this.OPTION2(() => this.SUBRULE2(this.Arguments, { LABEL: "args" })); 646 | }); 647 | 648 | protected DotMemberExpression = this.RULE("DotMemberExpression", () => { 649 | this.OR1([ 650 | { ALT: () => this.CONSUME(PERIOD, operator) }, 651 | { ALT: () => this.CONSUME(ATTRIBUTE, operator) }, 652 | ]); 653 | 654 | this.OR2([ 655 | { ALT: () => this.SUBRULE(this.Identifier, right) }, 656 | { ALT: () => this.SUBRULE(this.ArrayExpression, right) }, 657 | { ALT: () => this.SUBRULE(this.ReservedWord, right) }, 658 | ]); 659 | }); 660 | 661 | protected PrimaryExpression = this.RULE("PrimaryExpression", () => { 662 | this.OR( 663 | this.cachePrimaryExpression || 664 | (this.cachePrimaryExpression = [ 665 | { ALT: () => this.SUBRULE(this.ArrayExpression) }, 666 | { ALT: () => this.SUBRULE(this.ObjectExpression) }, 667 | { ALT: () => this.SUBRULE(this.FunctionExpression) }, 668 | { ALT: () => this.SUBRULE(this.SubExpression) }, 669 | { ALT: () => this.SUBRULE(this.ParenthesisExpression) }, 670 | { ALT: () => this.SUBRULE(this.CallExpression) }, 671 | { ALT: () => this.SUBRULE(this.Identifier) }, 672 | { ALT: () => this.SUBRULE(this.ReservedWord) }, 673 | { ALT: () => this.SUBRULE(this.Literal) }, 674 | ]) 675 | ); 676 | }); 677 | 678 | protected CallExpression = this.RULE("CallExpression", () => { 679 | this.SUBRULE(this.Identifier, { LABEL: "id" }); 680 | this.SUBRULE(this.Arguments, { LABEL: "args" }); 681 | }); 682 | 683 | protected ParenthesisExpression = this.RULE("ParenthesisExpression", () => { 684 | this.CONSUME(OPEN_PAREN); 685 | this.SUBRULE(this.ExpressionStatement, { LABEL: "innerExpression" }); 686 | this.CONSUME(CLOSE_PAREN); 687 | }); 688 | 689 | protected Literal = this.RULE("Literal", () => { 690 | this.CONSUME(LITERAL); 691 | 692 | this.OPTION(() => { 693 | this.CONSUME(TYPE_DECLARATION); 694 | }); 695 | }); 696 | 697 | protected UnTypedIdentifier = this.RULE("UnTypedIdentifier", () => { 698 | this.CONSUME(IDENTIFIER); 699 | }); 700 | 701 | protected Identifier = this.RULE("Identifier", () => { 702 | this.SUBRULE(this.UnTypedIdentifier, { LABEL: "id" }); 703 | this.OPTION(() => { 704 | this.CONSUME(TYPE_DECLARATION, { LABEL: "asType" }); 705 | }); 706 | }); 707 | 708 | protected ReservedWord = this.RULE("ReservedWord", () => { 709 | this.OR( 710 | this.cacheReservedWord || 711 | (this.cacheReservedWord = [ 712 | { ALT: () => this.CONSUME(END) }, 713 | { ALT: () => this.CONSUME(IN) }, 714 | { ALT: () => this.CONSUME(MOD) }, 715 | { ALT: () => this.CONSUME(OBJECT) }, 716 | { ALT: () => this.CONSUME(STOP) }, 717 | { ALT: () => this.CONSUME(NEXT) }, 718 | { ALT: () => this.CONSUME(BOOLEAN) }, 719 | { ALT: () => this.CONSUME(INTEGER) }, 720 | { ALT: () => this.CONSUME(LONGINTEGER) }, 721 | { ALT: () => this.CONSUME(STRING) }, 722 | // 723 | ]) 724 | ); 725 | }); 726 | 727 | protected ConditionalCompilationStatement = this.RULE( 728 | "ConditionalCompilationStatement", 729 | () => { 730 | this.OR( 731 | this.cacheConditionalCompilationStatement || 732 | (this.cacheConditionalCompilationStatement = [ 733 | { ALT: () => this.SUBRULE(this.ConditionalConst) }, 734 | { ALT: () => this.SUBRULE(this.ConditionalError) }, 735 | { ALT: () => this.SUBRULE(this.ConditionalIfStatement) }, 736 | // 737 | ]) 738 | ); 739 | } 740 | ); 741 | 742 | protected ConditionalConst = this.RULE("ConditionalConst", () => { 743 | this.CONSUME(CONDITIONAL_CONST); 744 | this.SUBRULE(this.ExpressionStatement, { LABEL: "assignment" }); 745 | }); 746 | 747 | protected ConditionalError = this.RULE("ConditionalError", () => { 748 | this.CONSUME(CONDITIONAL_ERROR); 749 | }); 750 | 751 | protected ConditionalIfStatement = this.RULE("ConditionalIfStatement", () => { 752 | this.CONSUME(CONDITIONAL_IF); 753 | this.SUBRULE1(this.ExpressionStatement, { LABEL: "test" }); 754 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 755 | 756 | this.SUBRULE2(this.BlockStatement, body); 757 | 758 | this.MANY2(() => { 759 | this.SUBRULE(this.ConditionalElseIfStatement, { LABEL: "alternate" }); 760 | }); 761 | 762 | this.OPTION3(() => { 763 | this.SUBRULE(this.ConditionalElseStatement, { LABEL: "alternate" }); 764 | }); 765 | 766 | this.OPTION4(() => { 767 | this.CONSUME(CONDITIONAL_END_IF); 768 | }); 769 | }); 770 | 771 | protected ConditionalElseIfStatement = this.RULE( 772 | "ConditionalElseIfStatement", 773 | () => { 774 | this.CONSUME(CONDITIONAL_ELSE_IF); 775 | this.SUBRULE(this.ExpressionStatement, { LABEL: "test" }); 776 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 777 | 778 | this.SUBRULE2(this.BlockStatement, body); 779 | } 780 | ); 781 | 782 | protected ConditionalElseStatement = this.RULE( 783 | "ConditionalElseStatement", 784 | () => { 785 | this.CONSUME(CONDITIONAL_ELSE); 786 | this.SUBRULE(this.EndOfStatement, { LABEL: "trailingComments" }); 787 | 788 | this.SUBRULE2(this.BlockStatement, body); 789 | } 790 | ); 791 | 792 | protected Comment = this.RULE("Comment", () => { 793 | this.OR([ 794 | { ALT: () => this.CONSUME(COMMENT_QUOTE) }, 795 | { ALT: () => this.CONSUME(COMMENT_REM) }, 796 | // 797 | ]); 798 | }); 799 | 800 | protected EndOfStatement = this.RULE("EndOfStatement", () => { 801 | this.OPTION(() => { 802 | this.SUBRULE(this.Comment); 803 | }); 804 | this.CONSUME(TERMINATOR); 805 | }); 806 | 807 | private cacheStatement: any = undefined; 808 | private cacheReservedWord: any = undefined; 809 | private cachePrimaryExpression: any = undefined; 810 | private cacheConditionalCompilationStatement: any = undefined; 811 | 812 | constructor(input: IToken[]) { 813 | super(input, ALL_TOKENS, { 814 | outputCst: true, 815 | recoveryEnabled: false, 816 | }); 817 | Parser.performSelfAnalysis(this); 818 | } 819 | } 820 | 821 | export const parserInstance = new RokuBRSParser([]); 822 | 823 | const tokens = (list = []): Token[] => { 824 | return list.map((t: IToken) => { 825 | const length = t.image.length; 826 | const range: [number, number] = [t.startOffset, t.startOffset + length]; 827 | 828 | return { 829 | loc: { 830 | start: { column: t.startColumn, line: t.startLine }, 831 | end: { column: t.startColumn + length, line: t.startLine }, 832 | }, 833 | range, 834 | type: t.tokenType.tokenName, 835 | value: t.image, 836 | }; 837 | }); 838 | }; 839 | 840 | const errors = (list) => 841 | map(list, ({ name, message, token }) => { 842 | const location = { 843 | end: { 844 | line: token.startLine, 845 | column: token.startColumn + token.image.length, 846 | }, 847 | start: { line: token.startLine, column: token.startColumn }, 848 | }; 849 | return { name, message, location }; 850 | }); 851 | 852 | export function parse(source, entryPoint = "Program") { 853 | const lexingResult = BRSLexer.tokenize(source); 854 | parserInstance.input = lexingResult.tokens; 855 | 856 | const value = parserInstance[entryPoint](); 857 | 858 | return { 859 | lexErrors: lexingResult.errors, 860 | parseErrors: errors(parserInstance.errors), 861 | parserInstance, 862 | tokens: tokens(lexingResult.tokens), 863 | value, 864 | }; 865 | } 866 | -------------------------------------------------------------------------------- /test/assets/weird.brs: -------------------------------------------------------------------------------- 1 | '************************************************************* 2 | '** Utilities BrightScript 3 | '** 4 | '** 24i, 30-11-2016 5 | '** 6 | '** The utility functions in this file contain all BrightScript-related utility functionalities. 7 | '** There is a separate utils file for SceneGraph-specific functionality. 8 | '************************************************************* 9 | 10 | '----------------- 11 | ' Print 12 | ' * printl() 13 | ' * printw() 14 | ' * printe() 15 | ' 16 | ' Various 17 | ' * utils_getAnonymisedDeviceId() 18 | ' * utils_getLocalJSON() 19 | ' * utils_encapsulateInQuotes() 20 | ' 21 | ' Registry 22 | ' * utils_saveStringInRegistry() 23 | ' * utils_loadStringFromRegistry() 24 | ' * utils_deleteStringFromRegistry() 25 | ' 26 | ' URL and HTTP 27 | ' * utils_HTTPRequest() 28 | ' * utils_urlencode() 29 | ' * utils_addQueryParamsToURL() 30 | ' * utils_removeParameterFromURL() 31 | 32 | ' Array and AssociativeArray 33 | ' * utils_array_indexOf() 34 | ' * utils_array_contains() 35 | ' * utils_array_findIndexByKeyAndValue() 36 | ' * utils_subarray() 37 | ' * utils_array_join() 38 | ' * utils_shallowCopy() 39 | ' 40 | ' Date and time 41 | ' * utils_httpDateToRoDateTime() 42 | ' * utils_yyyyMMddhhmmssToDateTime() 43 | ' * utils_secondsToReadableTime() 44 | ' 45 | ' Math 46 | ' * utils_mathFloor() 47 | ' * utils_mathCeil() 48 | ' * utils_mathRound() 49 | '----------------- 50 | 51 | 52 | '=================================================================================================== 53 | ' Print 54 | '=================================================================================================== 55 | 56 | '----- 57 | ' printl() 58 | ' printw() 59 | ' printe() 60 | ' 61 | ' These functions offer a slightly shortened way to print LOG, WARNING and ERROR telnet log lines. 62 | ' The value of these functions is that they automatically add LOG/WARNING/ERROR and the name of the 63 | ' component from which they were called. You still need to add the function name to make it easy to 64 | ' find. 65 | ' 66 | ' Parameters: 67 | ' * {Object} messageObject - The message to be printed. 68 | ' 69 | ' Example: 70 | ' printl("init() - Setting up component." 71 | ' >>> "LOG - MainScene - init() - Setting up component." to be logged 72 | '----- 73 | Function printl(messageObject as Dynamic) 74 | printcommon("LOG", messageObject) 75 | End Function 76 | 77 | Function printw(messageObject as String) 78 | printcommon("WARNING", messageObject) 79 | End Function 80 | 81 | Function printe(messageObject as String) 82 | printcommon("ERROR", messageObject) 83 | End Function 84 | 85 | Function printcommon(messageTypeString as String, messageObject as Dynamic) 86 | if(m.top <> invalid) 87 | print messageTypeString + " - " + m.top.subtype().ToStr() + " - " ; messageObject 88 | else 89 | print messageTypeString + " - " ; messageObject 90 | end if 91 | End Function 92 | 93 | 94 | '=================================================================================================== 95 | ' Various 96 | '=================================================================================================== 97 | 98 | '----- 99 | ' utils_getAnonymisedDeviceId() 100 | ' 101 | ' Returns the first 16 characters of the SHA256 of the device ID, split in groups of 4 characters. 102 | ' For example: "9be4-1140-8faa-9a80". This can be used to uniquely identify a device. 103 | ' 104 | ' Returns: 105 | ' * {String} An anonymised identifier. 106 | '----- 107 | Function utils_getAnonymisedDeviceId() as String 108 | deviceInfo = CreateObject("roDeviceInfo") 109 | byteArray = CreateObject("roByteArray") 110 | byteArray.FromAsciiString(deviceInfo.GetDeviceUniqueId()) 111 | evpDigest = CreateObject("roEVPDigest") 112 | evpDigest.Setup("sha256") 113 | anonymisedDeviceIdString = evpDigest.Process(byteArray).Mid(0, 16) 114 | splitAnonymisedDeviceIdString = anonymisedDeviceIdString.Mid(0,4) + "-" + anonymisedDeviceIdString.Mid(4,4) + "-" + anonymisedDeviceIdString.Mid(8,4) + "-" + anonymisedDeviceIdString.Mid(12,4) 115 | 116 | return splitAnonymisedDeviceIdString 117 | End Function 118 | 119 | '----- 120 | ' utils_getLocalJSON() 121 | ' 122 | ' This function reads JSON from a specified filePath and returns either a JSON string or a parsed 123 | ' JSON object. 124 | ' 125 | ' Parameters: 126 | ' * {String} filePath - Path to a local JSON file 127 | ' * (optional) {Boolean} toParse - if the JSON should be parsed or not 128 | ' 129 | ' Returns: 130 | ' * {String or roArray or roAssociativeArray} - Returns either the string or a parsed JSON object. 131 | ' - Returns invalid if file was not found. 132 | '----- 133 | Function utils_getLocalJSON(filePath as String, toParse = false as Boolean) as Dynamic 134 | returnObject = invalid 135 | fileSystem = CreateObject("roFileSystem") 136 | 137 | 'Load the file. 138 | if (fileSystem.exists(filePath) = true) 139 | JSONstring = ReadAsciiFile(filePath) 140 | if(toParse) 141 | 'Parse the configuration file. 142 | parsedJSONObject = ParseJSON(JSONstring) 143 | if (parsedJSONObject <> invalid) 144 | returnObject = parsedJSONObject 145 | else 146 | printe("utils_getLocalJSON() - Parsing " + filePath + " returned invalid result.") 147 | end if 148 | else 149 | returnObject = JSONstring 150 | end if 151 | else 152 | printe("utils_getLocalJSON() - No file found at: " + filePath) 153 | end if 154 | return returnObject 155 | End Function 156 | 157 | '----- 158 | ' utils_encapsulateInQuotes() 159 | ' 160 | ' Utility function to encapsulate the given string in quotes. 161 | ' 162 | ' Parameters: 163 | ' * {String} encapsulate - The string to be encapsulated in quotes. 164 | ' 165 | ' Returns: 166 | ' * {String} A string encapsulated in quotes (""). 167 | ' 168 | ' Example: 169 | ' utils_encapsulateInQuotes("my string to encapsulate") 170 | ' >>> ""my string to encapsulate"" 171 | '----- 172 | Function utils_encapsulateInQuotes(encapsulate as String) as String 173 | return Chr(34) + encapsulate + Chr(34) 174 | End Function 175 | 176 | '=================================================================================================== 177 | ' Registry 178 | '=================================================================================================== 179 | 180 | '----- 181 | ' utils_saveStringInRegistry() 182 | ' utils_loadStringFromRegistry() 183 | ' utils_deleteStringFromRegistry() 184 | ' 185 | ' Methods for easy access to the registry (write / read / delete). 186 | ' NOTE: In SceneGraph apps these can only be used from a Task node! 187 | ' 188 | ' Parameters: 189 | ' * {String} sectionString - The section 190 | ' * {String} keyString - The complete url 191 | ' * {String} valueString - The string to be saved. 192 | '----- 193 | Sub utils_saveStringInRegistry(sectionString as String, keyString as String, valueString as String) 194 | registrySection = CreateObject("roRegistrySection", sectionString) 195 | registrySection.Write(keyString, valueString) 196 | registrySection.Flush() 197 | End Sub 198 | 199 | Function utils_loadStringFromRegistry(sectionString as String, keyString as String) as Object 200 | registrySection = CreateObject("roRegistrySection", sectionString) 201 | if (registrySection.Exists(keyString)) 202 | return registrySection.Read(keyString) 203 | else 204 | return invalid 205 | end if 206 | End Function 207 | 208 | Sub utils_deleteStringFromRegistry(sectionString as String, keyString as String) 209 | registrySection = CreateObject("roRegistrySection", sectionString) 210 | if (registrySection.Exists(keyString)) 211 | registrySection.Delete(keyString) 212 | registrySection.Flush() 213 | end if 214 | End Sub 215 | 216 | Sub utils_deleteRegistrySection(sectionString as String) 217 | registry = CreateObject("roRegistry") 218 | registry.Delete(sectionString) 219 | registry.Flush() 220 | End Sub 221 | 222 | '=================================================================================================== 223 | ' URL and HTTP 224 | '=================================================================================================== 225 | 226 | '----- 227 | ' utils_HTTPRequest() 228 | ' 229 | ' A standard function to perform HTTP requests (GET, POST, PUT or DELETE) with optional extra HTTP 230 | ' headers. 231 | ' 232 | ' Parameters: 233 | ' * {String} httpMethodString - The method (GET, POST) 234 | ' * {String} urlString - The complete url 235 | ' * {String or invalid} postBodyString - In case of HTTP POST, this should contain the HTTP body to 236 | ' be posted. In case of HTTP GET, this should be 'invalid'. 237 | ' * {roAssociativeArray or invalid} headersAssociativeArray - A associative array with the extra 238 | ' headers. Should be invalid if no extra headers are needed. 239 | ' 240 | ' Returns: 241 | ' * {roAssociativeArray} resultObject - An associative array with: 242 | ' .responseCode - the code from message.GetResponseCode() 243 | ' .failureReason - the description from message.GetFailureReason() 244 | ' .bodyString - the response body from message.GetString() 245 | '----- 246 | Function utils_HTTPRequest(httpMethodString as String, urlString as String, postBodyString as Dynamic, headersAssociativeArray as Dynamic) as Object 247 | printl("utils_HTTPRequest() - " + httpMethodString + " " + urlString) 248 | 249 | 'Before doing a request, check the availability of the internet connection. 250 | deviceInfo = CreateObject("roDeviceInfo") 251 | if (deviceInfo.GetLinkStatus() = false) 252 | printe("utils_checkNetworkConnection - deviceInfo.GetLinkStatus(): " + deviceInfo.GetLinkStatus()) 253 | end if 254 | 255 | urlTransfer = CreateObject("roUrlTransfer") 256 | urlTransfer.SetMessagePort(CreateObject("roMessagePort")) 257 | urlTransfer.SetUrl(urlString) 'Set the url. 258 | urlTransfer.EnableEncodings(true) 'Enable gzip compression. 259 | urlTransfer.RetainBodyOnError(true) 'Also return the response body in case of errors. 260 | urlTransfer.SetCertificatesFile("common:/certs/ca-bundle.crt") 'Enable https. 261 | urlTransfer.InitClientCertificates() 'Enable https. 262 | 263 | urlTransfer.EnableCookies() 264 | 265 | 'Add any required HTTP headers. 266 | if (headersAssociativeArray <> invalid) 267 | urlTransfer.SetHeaders(headersAssociativeArray) 268 | end if 269 | 270 | if (httpMethodString = "POST" or httpMethodString = "PUT") 271 | if (httpMethodString = "PUT") 272 | urlTransfer.SetRequest("PUT") 273 | end if 274 | 275 | if (postBodyString <> invalid and postBodyString.Len() > 0) 276 | printl("utils_HTTPRequest() - postBodyString: " + postBodyString) 277 | end if 278 | 279 | urlTransfer.AsyncPostFromString(postBodyString) 280 | else 281 | if (httpMethodString = "DELETE") 282 | urlTransfer.SetRequest("DELETE") 283 | end if 284 | 285 | urlTransfer.AsyncGetToString() 286 | end if 287 | 288 | 'Prepare the object that is to be returned. 289 | resultObject = CreateObject("roAssociativeArray") 290 | 291 | while true 292 | message = wait(0, urlTransfer.GetMessagePort()) 293 | 294 | if (type(message) = "roUrlEvent") 295 | 'printl("utils_HTTPRequest() - GetSourceIdentity():"; message.GetSourceIdentity()) 296 | 297 | resultObject.responseCode = message.GetResponseCode() 298 | resultObject.failureReason = message.GetFailureReason() 299 | resultObject.bodyString = message.GetString() 300 | 301 | if (message.GetResponseCode() = 200 or message.GetResponseCode() = 201) 302 | 'printl("utils_HTTPRequest() - GetString(): " + message.GetString()) 303 | 'printl("utils_HTTPRequest() - GetTargetIpAddress(): " + message.GetTargetIpAddress()) 304 | 'printl("utils_HTTPRequest() - GetResponseHeaders(): " + message.GetResponseHeaders()) 305 | 'printl("utils_HTTPRequest() - GetResponseHeadersArray(): " + message.GetResponseHeadersArray()) 306 | else 307 | printl("utils_HTTPRequest() - GetResponseCode(): " + message.GetResponseCode()) 308 | printl("utils_HTTPRequest() - GetFailureReason(): " + message.GetFailureReason()) 309 | printl("utils_HTTPRequest() - GetString(): " + message.GetString()) 310 | end if 311 | 312 | exit while 313 | end if 314 | end while 315 | 316 | return resultObject 317 | End Function 318 | 319 | '----- 320 | ' utils_urlencode() 321 | ' 322 | ' Utility function to URL encode a given string. 323 | ' See: 'https://sdkdocs.roku.com/display/sdkdoc/ifUrlTransfer#ifUrlTransfer-Escape(textasString)asString 324 | ' 325 | ' Parameters: 326 | ' * {String} query - The string that is to be url encoded. 327 | ' 328 | ' Returns: 329 | ' * {String} A URL encoded string. 330 | '----- 331 | Function utils_urlencode(query as String) as String 332 | urlTransfer = CreateObject("roUrlTransfer") 333 | 334 | return urlTransfer.Escape(query) 335 | End Function 336 | 337 | '----- 338 | ' utils_addQueryParamsToURL() 339 | ' 340 | ' Utility function to add query parameters to an URL. All values are URL encoded. 341 | ' 342 | ' Parameters: 343 | ' * {roAssociativeArray} params - An associative array with the parameters. 344 | ' * {String} url - The URL to which the parameters are to be added. 345 | ' 346 | ' Returns: 347 | ' * {String} A URL encoded string with given parameters included. 348 | '----- 349 | Function utils_addQueryParamsToURL(params as Object, url as String) as String 350 | for each param in params 351 | if (url.Instr("?") <> -1) 352 | url = url + "&" 353 | else 354 | url = url + "?" 355 | end if 356 | url = url + (utils_urlencode(param) + "=" + utils_urlencode(params[param])) 357 | end for 358 | 359 | return url 360 | End Function 361 | 362 | '----- 363 | ' utils_removeParameterFromURL() 364 | ' 365 | ' Utility function to remove a query parameter from an URL. 366 | ' @see http://stackoverflow.com/questions/1634748/how-can-i-delete-a-query-string-parameter-in-javascript#answer-1634841 367 | 368 | ' Parameters: 369 | ' * {String} param - Parameter to be removed from the URL. 370 | ' * {String} url - URL to remove parameter from. 371 | ' 372 | ' Returns: 373 | ' * {String} URL string without given param. 374 | ' 375 | ' Examples: 376 | ' utils_removeParameterFromURL("someParam", "http://mysite.com?someParam=someValue") 377 | ' >>> http://mysite.com 378 | ' utils_removeParameterFromURL("someParam", "http://mysite.com?someParam=someValue&someOtherParam=someOtherValue") 379 | ' >>> http://mysite.com?someOtherParam=someOtherValue 380 | ' utils_removeParameterFromURL("someParam", "http://mysite.com?someParam=someValue&someOtherParam=someOtherValue&anotherParam=anotherValue") 381 | ' >>> http://mysite.com?someOtherParam=someOtherValue&anotherParam=anotherValue 382 | '----- 383 | Function utils_removeParameterFromURL(param as String, url as String) 384 | urlParts = url.Split("?") 385 | 386 | if (urlParts.Count() >= 2) 387 | prefix = utils_urlencode(param) + "=" 388 | 389 | r = CreateObject("roRegex", "[&;]", "g") 390 | parts = r.Split(urlParts[1]) 391 | 392 | ' reverse iteration as may be destructive 393 | for i = (parts.Count() - 1) to 0 step -1 394 | if (parts[i].Instr(0, prefix) <> -1) 395 | parts.Delete(i) 396 | end if 397 | end for 398 | 399 | if (parts.Count() > 0) 400 | url = urlParts[0] + "?" + utils_array_join(parts, "&") 401 | else 402 | url = urlParts[0] 403 | end if 404 | end if 405 | 406 | return url 407 | End Function 408 | 409 | 410 | '=================================================================================================== 411 | ' Array and AssociativeArray 412 | '=================================================================================================== 413 | 414 | '----- 415 | ' utils_array_indexOf() 416 | ' 417 | ' Utility function to return the location (or minus one (-1) if it's not in there) of a specific 418 | ' item in an array. 419 | ' 420 | ' Parameters: 421 | ' * {Object} needle - The item to look for. 422 | ' * {roArray} hayStack - The hayStack (array) where the needle should be looked for. 423 | ' 424 | ' Returns: 425 | ' * {Integer} An integer indicating what the location of the needle in the hayStack (array) is. If 426 | ' it's not in the array it'll return minus one (-1). 427 | '----- 428 | Function utils_array_indexOf(needle as Object, hayStack as Object) as Integer 429 | if (type(hayStack) <> "roArray") 430 | return -1 431 | end if 432 | 433 | for i = 0 to (hayStack.Count() - 1) 434 | if (type(hayStack[i]) = type(needle) and hayStack[i] = needle) 435 | return i 436 | end if 437 | end for 438 | 439 | return -1 440 | End Function 441 | 442 | '----- 443 | ' utils_array_contains() 444 | ' 445 | ' Utility function to check whether an array contains a specific item. 446 | ' 447 | ' Parameters: 448 | ' * {Object} needle - The item to look for. 449 | ' * {roArray} hayStack - The hayStack (array) where the needle should be looked for. 450 | ' 451 | ' Returns: 452 | ' * {Boolean} A boolean indicating whether the hayStack (array) contains the given needle (item). 453 | '----- 454 | Function utils_array_contains(needle as Object, hayStack as Object) as boolean 455 | if (type(hayStack) <> "roArray") 456 | return false 457 | end if 458 | return utils_array_indexOf(needle, hayStack) <> -1 459 | End Function 460 | 461 | '----- 462 | ' utils_array_findIndexByKeyAndValue() 463 | ' 464 | ' Utility function to find an item of an array searching for it on the given key and value. 465 | ' 466 | ' Parameters: 467 | ' * {String} key - The key to check for 468 | ' * {Dynamic} value - The value to check for. 469 | ' * {roArray or roAssociativeArray} hayStack - The hayStack (array containing associative array's) 470 | ' where the needle should be looked for. 471 | ' 472 | ' Returns: 473 | ' * {Integer} An item that matches the given key and value in the hayStack (array) or -1. 474 | '----- 475 | Function utils_array_findIndexByKeyAndValue(key as String, value as Dynamic, haystack as Object) as Integer 476 | if (type(hayStack) <> "roArray") 477 | return -1 478 | end if 479 | 480 | for i = 0 to haystack.Count() - 1 481 | hay = hayStack[i] 482 | if (type(hay) = "roAssociativeArray" and hay[key] <> invalid and value <> invalid and hay[key] = value) 483 | return i 484 | end if 485 | end for 486 | 487 | return -1 488 | End Function 489 | 490 | '----- 491 | ' utils_subarray() 492 | ' 493 | ' This function is used to get part of an array (similar to the functionalities in ifStringOps). 494 | ' 495 | ' Parameters: 496 | ' * {roArray} originalArray - the target array. 497 | ' * {Integer} startIndex - the start of the part of the array we want. 498 | ' * {Integer} endIndex - the end of the part of the array we want. 499 | ' 500 | ' Returns: 501 | ' * {roArray} - The sub array. 502 | '----- 503 | Function utils_subarray(originalArray as Object, startIndex as Integer, endIndex as Integer) as Dynamic 504 | 'Validate the startIndex and endIndex. 505 | if (startIndex < 0 or endIndex < 0 or startIndex > (originalArray.Count() - 1) or endIndex > (originalArray.Count() - 1) or endIndex < startIndex) 506 | return [] 507 | end if 508 | 509 | 'Once validate, create and fill the subArray. 510 | subArray = CreateObject("roArray", 0, true) 511 | for i = startIndex to endIndex 512 | subArray.Push(originalArray[i]) 513 | end for 514 | 515 | return subArray 516 | End Function 517 | 518 | '----- 519 | ' utils_array_join() 520 | ' 521 | ' Utility function to join all elements of an array into a string. 522 | ' 523 | ' Parameters: 524 | ' * {roArray} array - The array which elements should be joined into a string. 525 | ' * (optional) {String} [separator] - A string to be used to separate the items. 526 | ' 527 | ' Returns: 528 | ' * {String} A joined string containing all elements of the given array. Or an empty string if no 529 | ' array is given as the first parameter. 530 | ' 531 | ' Examples: 532 | ' utils_array_join(["el1", "el2", "el3"]) 533 | ' >>> "el1el2el3" 534 | ' utils_array_join(["el1", "el2", "el3"], ", ") 535 | ' >>> "el1, el2, el3" 536 | '----- 537 | Function utils_array_join(array as Object, separator = "" as String) 538 | if (GetInterface(array, "ifArray") = invalid) 539 | return "" 540 | end if 541 | 542 | joinedString = CreateObject("roString") 543 | 544 | max = array.Count() - 1 545 | 546 | for i = 0 to max 547 | if (i = max) 548 | separator = "" 549 | end if 550 | 551 | toAppend = array[i] + separator 552 | 553 | joinedString.AppendString(toAppend, Len(toAppend)) 554 | end for 555 | 556 | return joinedString 557 | End Function 558 | 559 | '----- 560 | ' utils_shallowCopy() 561 | ' 562 | ' Utility function to make a copy of an array or associative array. 563 | ' Taken from: https://forums.roku.com/viewtopic.php?p=392505&sid=80748067620a1a2d1c06c65f4ecab91d#post_content392575 564 | ' 565 | ' Parameters: 566 | ' * {roArray or roAssociativeArray} array - The array or associative array to copy. 567 | ' * (optional) {Integer} [depth] - The depth to use for associative arrays. Defaults to 0 if not 568 | ' present. 569 | ' 570 | ' Returns: 571 | ' * {roArray or roAssociativeArray} A copy of the given (associative) array. 572 | '----- 573 | Function utils_shallowCopy(array As Dynamic, depth = 0 As Integer) As Dynamic 574 | if (Type(array) = "roArray") Then 575 | copy = [] 576 | for each item In array 577 | childCopy = ShallowCopy(item, depth) 578 | if (childCopy <> invalid) Then 579 | copy.Push(childCopy) 580 | end if 581 | next 582 | return copy 583 | else if (Type(array) = "roAssociativeArray") Then 584 | copy = {} 585 | for each key In array 586 | if (depth > 0) Then 587 | copy[key] = ShallowCopy(array[key], depth - 1) 588 | else 589 | copy[key] = array[key] 590 | end if 591 | next 592 | return copy 593 | else 594 | return array 595 | end if 596 | return invalid 597 | End Function 598 | 599 | 600 | '=================================================================================================== 601 | ' Date and time 602 | '=================================================================================================== 603 | 604 | '----- 605 | ' utils_httpDateToRoDateTime() 606 | ' 607 | ' Utility function to convert an HTTP Date string to a roDateTime object. 608 | ' An example of such an HTTP Date is "Date: Tue, 15 Nov 1994 08:12:31 GMT". 609 | ' * see: https://tools.ietf.org/html/rfc822#page-26 610 | ' * see: https://forums.roku.com/viewtopic.php?t=46484 611 | ' 612 | ' Parameters: 613 | ' * {String} httpDate - An HTTP Date string. 614 | ' 615 | ' Returns 616 | ' * {roDateTime} A 'roDateTime' object based on the given HTTP Date. 617 | '----- 618 | Function utils_httpDateToRoDateTime(httpDate as String) as Object 619 | dateInString = httpDate 620 | regex = CreateObject("roRegex", "(\d+)\s+([a-z]+)\s+(\d+)\s+(\d+:\d+:\d+)\D", "i") 621 | da = regex.Match(dateInString) 622 | ml = "JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC" 623 | mon% = (Instr(1, ml, Ucase(da [2])) - 1) / 3 + 1 624 | dateOutString = da[3] + "-" + mon%.ToStr() + "-" + da [1] + " " + da [4] 625 | 626 | dateTime = CreateObject("roDateTime") 627 | dateTime.fromISO8601String(dateOutString) 628 | return dateTime 629 | End Function 630 | 631 | '----- 632 | ' utils_yyyyMMddhhmmssToDateTime() 633 | ' 634 | ' Utility function to convert a yyyyMMddhhmmss string to a `roDateTime` object. 635 | ' 636 | ' Parameters: 637 | ' * {String} yyyyMMddhhmmss - A date time string in the yyyyMMddhhmmss format. 638 | ' 639 | ' Returns: 640 | ' * {roDateTime} A 'roDateTime' object set to the given timestamp. 641 | '----- 642 | Function utils_yyyyMMddhhmmssToDateTime(yyyyMMddhhmmss as String) as Object 643 | year = yyyyMMddhhmmss.Left(4) 644 | month = yyyyMMddhhmmss.Mid(4, 2) 645 | day = yyyyMMddhhmmss.Mid(6, 2) 646 | hours = yyyyMMddhhmmss.Mid(8, 2) 647 | minutes = yyyyMMddhhmmss.Mid(10, 2) 648 | seconds = yyyyMMddhhmmss.Right(2) 649 | 650 | ' An ISO 8601 date string should look like for example "YYYY-MM-DD HH:MM:SS" e.g 651 | ' "2009-01-01 01:00:00.000" or "2009-01-01T01:00:00.000". 652 | dateString = year + "-" + month + "-" + day + " " + hours + ":" + minutes + ":" + seconds + ".000" 653 | 654 | dateTime = createObject("roDateTime") 655 | dateTime.FromISO8601String(dateString) 656 | 657 | return dateTime 658 | End Function 659 | 660 | '----------------------------- 661 | ' utils_secondsToReadableTime() 662 | ' 663 | ' Utility function to transform a number of seconds into a readable string. 664 | ' Works up to the level of hours, so primarily intended for showing the length of a video, trailer, 665 | ' etc. 666 | ' 667 | ' Parameter: 668 | ' * {Integer} secondsInteger - the number of seconds as an integer 669 | ' 670 | ' Returns: 671 | ' * {String} A human readable string with the amount of time 672 | ' 673 | ' Examples: 674 | ' utils_secondsToReadableTime(1) 675 | ' >>> 0:01 676 | ' utils_secondsToReadableTime(10) 677 | ' >>> 0:10 678 | ' utils_secondsToReadableTime(80) 679 | ' >>> 1:20 680 | ' utils_secondsToReadableTime(630) 681 | ' >>> 10:30 682 | ' utils_secondsToReadableTime(3670) 683 | ' >>> 1:01:10 684 | ' utils_secondsToReadableTime(90000) 685 | ' >>> 25:00:00 686 | '----------------------------- 687 | Function utils_secondsToReadableTime(secondsInteger as Integer) as String 688 | numberOfSeconds = secondsInteger MOD 60 689 | numberOfMinutes = secondsInteger MOD 3600 \ 60 690 | numberOfHours = secondsInteger \ 3600 691 | 692 | secondsString = numberOfSeconds.toStr() 693 | if (numberOfSeconds < 10) 694 | secondsString = "0" + secondsString 695 | end if 696 | 697 | minutesString = numberOfMinutes.toStr() 698 | if (numberOfMinutes < 10 and numberOfHours > 0) 699 | minutesString = "0" + minutesString 700 | end if 701 | 702 | hoursString = numberOfHours.toStr() 703 | 704 | if (numberOfHours > 0) 705 | return hoursString + ":" + minutesString + ":" + secondsString 706 | else 707 | return minutesString + ":" + secondsString 708 | end if 709 | End Function 710 | 711 | 712 | '=================================================================================================== 713 | ' Math 714 | '=================================================================================================== 715 | 716 | '----- 717 | ' utils_mathFloor() 718 | ' 719 | ' Utility function to return the largest integer less than or equal to a given float number. 720 | ' 721 | ' Parameters: 722 | ' * {Float} x - A number. 723 | ' 724 | ' Returns: 725 | ' * {Integer} A number representing the largest integer less than or equal to the specified number. 726 | ' 727 | ' Examples: 728 | ' utils_mathFloor(45.95) 729 | ' >>> 45 730 | ' utils_mathFloor(45.05) 731 | ' >>> 45 732 | ' utils_mathFloor(4) 733 | ' >>> 4 734 | ' utils_mathFloor(-45.05) 735 | ' >>> -46 736 | ' utils_mathFloor(-45.95) 737 | ' >>> -46 738 | '----- 739 | Function utils_mathFloor(x as Float) as Integer 740 | return Int(x) 741 | End Function 742 | 743 | '----- 744 | ' utils_mathCeil() 745 | ' Utility function to return the smallest integer greater than or equal to a given float number. 746 | ' 747 | ' Parameters: 748 | ' * {Float} x - A number. 749 | ' 750 | ' Returns: 751 | ' * {Integer} The smallest integer greater than or equal to the given number. 752 | ' 753 | ' Examples: 754 | ' utils_mathCeil(0.95) 755 | ' >>> 1 756 | ' utils_mathCeil(4) 757 | ' >>> 4 758 | ' utils_mathCeil(7.004) 759 | ' >>> 8 760 | ' utils_mathCeil(-0.95) 761 | ' >>> -0 762 | ' utils_mathCeil(-4) 763 | ' >>> -4 764 | ' utils_mathCeil(-7.004) 765 | ' >>> -7 766 | '----- 767 | Function utils_mathCeil(x as Float) as Integer 768 | return Int(x) + 1 769 | End Function 770 | 771 | '----- 772 | ' utils_mathRound() 773 | ' 774 | ' Utility function to return the value of a float number rounded to the nearest integer. 775 | ' 776 | ' Parameters: 777 | ' * {Float} x - A number. 778 | ' 779 | ' Returns: 780 | ' * {Integer} The value of the given number rounded to the nearest integer. 781 | ' 782 | ' Examples: 783 | ' utils_mathRound(20.49) 784 | ' >>> 20 785 | ' utils_mathRound(20.5) 786 | ' >>> 21 787 | ' utils_mathRound(42) 788 | ' >>> 42 789 | ' utils_mathRound(-20.5) 790 | ' >>> -20 791 | ' utils_mathRound(-20.51) 792 | ' >>> -21 793 | '----- 794 | Function utils_mathRound(x as Float) as Integer 795 | return Cint(x) 796 | End Function 797 | -------------------------------------------------------------------------------- /src/ASTVisitor.ts: -------------------------------------------------------------------------------- 1 | import { filter, first, last } from "lodash"; 2 | 3 | import { ASTNode, ContextProps, NodeContext, ProgramNode } from "./AST"; 4 | import { BaseVisitor } from "./BaseVisitor"; 5 | 6 | /** 7 | * 8 | * 9 | * @export 10 | * @class ASTVisitor 11 | * @extends {BaseVisitor} 12 | */ 13 | export class ASTVisitor extends BaseVisitor { 14 | constructor() { 15 | super(); 16 | this.validateVisitor(); 17 | } 18 | 19 | /** 20 | * 21 | * 22 | * @param {NodeContext} ctx 23 | * @param {ContextProps} [props={ tokens: [] }] 24 | * @returns {ASTNode} 25 | * 26 | * @memberOf ASTVisitor 27 | */ 28 | public Program( 29 | ctx: NodeContext, 30 | props: ContextProps = { tokens: [] } 31 | ): ASTNode { 32 | return this.mapArguments(ctx, ({ Declaration = [] }: ProgramNode) => { 33 | const body = this.asArray(Declaration); 34 | 35 | const head = first(body); 36 | const tail = last(body); 37 | 38 | return this.asNode( 39 | { 40 | ...this.Location(head, tail), 41 | body, 42 | comments: [], 43 | sourceType: "module", 44 | tokens: props ? props.tokens : [], 45 | type: "Program", 46 | }, 47 | ctx 48 | ); 49 | }); 50 | } 51 | 52 | /** 53 | * 54 | * 55 | * @param {NodeContext} ctx 56 | * @returns {ASTNode} 57 | * 58 | * @memberOf ASTVisitor 59 | */ 60 | public EndOfStatement(ctx: NodeContext): ASTNode { 61 | return this.mapArguments(ctx, ({ TERMINATOR, Comment }) => { 62 | return Comment ? Comment : TERMINATOR; 63 | }); 64 | } 65 | /** 66 | * 67 | * 68 | * @param {NodeContext} ctx 69 | * @returns {ASTNode} 70 | * 71 | * @memberOf ASTVisitor 72 | */ 73 | public LibraryStatement(ctx: NodeContext): ASTNode { 74 | return this.mapArguments(ctx, ({ LIBRARY, path }) => { 75 | return this.asNode( 76 | { 77 | ...this.Location(LIBRARY, path), 78 | path, 79 | type: "LibraryStatement", 80 | }, 81 | ctx 82 | ); 83 | }); 84 | } 85 | 86 | /** 87 | * 88 | * 89 | * @param {NodeContext} ctx 90 | * @returns {ASTNode} 91 | * 92 | * @memberOf ASTVisitor 93 | */ 94 | public FunctionDeclaration(ctx: NodeContext): ASTNode { 95 | return this.mapArguments( 96 | ctx, 97 | ({ 98 | FUNCTION, 99 | END_FUNCTION, 100 | id, 101 | ReturnType, 102 | params, 103 | body, 104 | trailingComments, 105 | }) => { 106 | trailingComments = this.hasComment(trailingComments); 107 | return this.asNode( 108 | { 109 | type: "FunctionDeclaration", 110 | id, 111 | ReturnType, 112 | params, 113 | trailingComments, 114 | body, 115 | ...this.Location(FUNCTION, END_FUNCTION), 116 | }, 117 | ctx 118 | ); 119 | } 120 | ); 121 | } 122 | 123 | /** 124 | * 125 | * 126 | * @param {NodeContext} ctx 127 | * @returns {ASTNode} 128 | * 129 | * @memberOf ASTVisitor 130 | */ 131 | public BlockStatement(ctx: NodeContext): ASTNode { 132 | return this.mapArguments(ctx, ({ body }) => { 133 | const bodyArray = this.asArray(body); 134 | 135 | const head = first(bodyArray); 136 | const tail = last(bodyArray); 137 | 138 | return this.asNode( 139 | { 140 | ...this.Location(head, tail), 141 | body: filter(bodyArray, (node) => node && node.type !== "NEWLINE"), 142 | type: "BlockStatement", 143 | }, 144 | ctx 145 | ); 146 | }); 147 | } 148 | 149 | /** 150 | * 151 | * 152 | * @param {NodeContext} ctx 153 | * @returns {ASTNode} 154 | * 155 | * @memberOf ASTVisitor 156 | */ 157 | public Statement(ctx: NodeContext): ASTNode { 158 | return this.mapArguments(ctx, ({ trailingComments, Empty, Statement }) => { 159 | trailingComments = this.hasComment(trailingComments); 160 | const node = this.asNode( 161 | { 162 | trailingComments, 163 | ...(Statement || Empty), 164 | }, 165 | ctx 166 | ); 167 | 168 | return node; 169 | }); 170 | } 171 | 172 | /** 173 | * 174 | * 175 | * @param {NodeContext} ctx 176 | * @returns {ASTNode} 177 | * 178 | * @memberOf ASTVisitor 179 | */ 180 | public ExpressionStatement(ctx: NodeContext): ASTNode { 181 | return this.singleNode(ctx); 182 | } 183 | 184 | /** 185 | * 186 | * 187 | * @param {NodeContext} ctx 188 | * @returns {ASTNode} 189 | * 190 | * @memberOf ASTVisitor 191 | */ 192 | public EmptyStatement(ctx: NodeContext): ASTNode { 193 | return this.mapArguments(ctx, ({ NEWLINE, trailingComments }) => { 194 | if (trailingComments) { 195 | return trailingComments; 196 | } 197 | 198 | return { type: "EmptyStatement", ...this.Location(NEWLINE, NEWLINE) }; 199 | }); 200 | } 201 | 202 | /** 203 | * 204 | * 205 | * @param {NodeContext} ctx 206 | * @returns {ASTNode} 207 | * 208 | * @memberOf ASTVisitor 209 | */ 210 | public ArrayExpression(ctx: NodeContext): ASTNode { 211 | return this.mapArguments( 212 | ctx, 213 | ({ OPEN_BRACKET, CLOSE_BRACKET, elements = [], trailingComments }) => { 214 | const elementsAsArray = this.asArray(elements); 215 | const trailingCommentsAsArray = this.asArray(trailingComments); 216 | 217 | return this.asNode( 218 | { 219 | ...this.Location(OPEN_BRACKET, CLOSE_BRACKET), 220 | elements: this.mergeTrailing( 221 | elementsAsArray, 222 | trailingCommentsAsArray 223 | ), 224 | type: "ArrayExpression", 225 | }, 226 | ctx 227 | ); 228 | } 229 | ); 230 | } 231 | 232 | /** 233 | * 234 | * 235 | * @param {NodeContext} ctx 236 | * @returns {ASTNode} 237 | * 238 | * @memberOf ASTVisitor 239 | */ 240 | public ObjectExpression(ctx: NodeContext): ASTNode { 241 | return this.mapArguments( 242 | ctx, 243 | ({ 244 | OPEN_CURLY_BRACE, 245 | CLOSE_CURLY_BRACE, 246 | properties = [], 247 | trailingComments = [], 248 | }) => { 249 | const propsAsArray = this.asArray(properties); 250 | const trailingCommentsAsArray = this.asArray(trailingComments); 251 | 252 | return this.asNode( 253 | { 254 | ...this.Location(OPEN_CURLY_BRACE, CLOSE_CURLY_BRACE), 255 | properties: this.mergeTrailing( 256 | propsAsArray, 257 | trailingCommentsAsArray 258 | ), 259 | trailingComments, 260 | type: "ObjectExpression", 261 | }, 262 | ctx 263 | ); 264 | } 265 | ); 266 | } 267 | 268 | /** 269 | * 270 | * 271 | * @param {NodeContext} ctx 272 | * @returns {ASTNode} 273 | * 274 | * @memberOf ASTVisitor 275 | */ 276 | public GoToStatement(ctx: NodeContext): ASTNode { 277 | return this.mapArguments(ctx, ({ GOTO, Identifier }) => { 278 | return this.asNode( 279 | { 280 | type: "GoToStatement", 281 | id: Identifier, 282 | ...this.Location(GOTO, Identifier), 283 | }, 284 | ctx 285 | ); 286 | }); 287 | } 288 | 289 | /** 290 | * 291 | * 292 | * @param {NodeContext} ctx 293 | * @returns {ASTNode} 294 | * 295 | * @memberOf ASTVisitor 296 | */ 297 | public LabeledStatement(ctx: NodeContext): ASTNode { 298 | return this.mapArguments(ctx, ({ COLON, label, body }) => { 299 | return this.asNode( 300 | { 301 | type: "LabeledStatement", 302 | label, 303 | body, 304 | ...this.Location(label, COLON), 305 | }, 306 | ctx 307 | ); 308 | }); 309 | } 310 | 311 | /** 312 | * 313 | * 314 | * @param {NodeContext} ctx 315 | * @returns {ASTNode} 316 | * 317 | * @memberOf ASTVisitor 318 | */ 319 | public Property(ctx: NodeContext): ASTNode { 320 | return this.mapArguments(ctx, ({ key, value }) => { 321 | return this.asNode( 322 | { type: "Property", key, value, ...this.Location(key, value) }, 323 | ctx 324 | ); 325 | }); 326 | } 327 | 328 | /** 329 | * 330 | * 331 | * @param {NodeContext} ctx 332 | * @returns {ASTNode} 333 | * 334 | * @memberOf ASTVisitor 335 | */ 336 | public ArrayElement(ctx: NodeContext): ASTNode { 337 | return this.mapArguments(ctx, ({ value, trailingComments = [] }) => { 338 | trailingComments = this.hasComment(trailingComments); 339 | return this.asNode( 340 | { 341 | type: "ArrayElement", 342 | value, 343 | trailingComments, 344 | ...this.Location(value, value), 345 | }, 346 | ctx 347 | ); 348 | }); 349 | } 350 | 351 | /** 352 | * 353 | * 354 | * @param {NodeContext} ctx 355 | * @returns {ASTNode} 356 | * 357 | * @memberOf ASTVisitor 358 | */ 359 | public PropertyName(ctx: NodeContext): ASTNode { 360 | return this.singleNode(ctx); 361 | } 362 | 363 | /** 364 | * 365 | * 366 | * @param {NodeContext} ctx 367 | * @returns {ASTNode} 368 | * 369 | * @memberOf ASTVisitor 370 | */ 371 | public DimStatement(ctx: NodeContext): ASTNode { 372 | return this.mapArguments(ctx, ({ DIM, Identifier, ArrayExpression }) => { 373 | return this.asNode( 374 | { 375 | type: "DimStatement", 376 | id: Identifier, 377 | ArrayExpression, 378 | ...this.Location(DIM, ArrayExpression), 379 | }, 380 | ctx 381 | ); 382 | }); 383 | } 384 | 385 | /** 386 | * 387 | * 388 | * @param {NodeContext} ctx 389 | * @returns {ASTNode} 390 | * 391 | * @memberOf ASTVisitor 392 | */ 393 | public ExitStatement(ctx: NodeContext): ASTNode { 394 | return this.asNode({ type: "ExitStatement", ...this.singleNode(ctx) }, ctx); 395 | } 396 | 397 | /** 398 | * 399 | * 400 | * @param {NodeContext} ctx 401 | * @returns {ASTNode} 402 | * 403 | * @memberOf ASTVisitor 404 | */ 405 | public IfStatement(ctx: NodeContext): ASTNode { 406 | return this.mapArguments( 407 | ctx, 408 | ({ IF, END_IF, test, alternate, consequent }) => { 409 | const bodyArray = this.asArray(consequent); 410 | 411 | const tail = END_IF ? END_IF : last(bodyArray); 412 | 413 | return this.asNode( 414 | { 415 | type: "IfStatement", 416 | test, 417 | alternate, 418 | consequent, 419 | ...this.Location(IF, tail), 420 | }, 421 | ctx 422 | ); 423 | } 424 | ); 425 | } 426 | 427 | /** 428 | * 429 | * 430 | * @param {NodeContext} ctx 431 | * @returns {ASTNode} 432 | * 433 | * @memberOf ASTVisitor 434 | */ 435 | public ElseIfStatement(ctx: NodeContext): ASTNode { 436 | return this.mapArguments(ctx, ({ ELSE_IF, body = [], test }) => { 437 | const bodyArray = this.asArray(body); 438 | const tail = bodyArray.length ? last(bodyArray) : ELSE_IF; 439 | 440 | return this.asNode( 441 | { 442 | type: "ElseIfStatement", 443 | test, 444 | body, 445 | ...this.Location(ELSE_IF, tail), 446 | }, 447 | ctx 448 | ); 449 | }); 450 | } 451 | 452 | /** 453 | * 454 | * 455 | * @param {NodeContext} ctx 456 | * @returns {ASTNode} 457 | * 458 | * @memberOf ASTVisitor 459 | */ 460 | public ElseStatement(ctx: NodeContext): ASTNode { 461 | return this.mapArguments(ctx, ({ ELSE, body = [] }) => { 462 | const bodyArray = this.asArray(body); 463 | const tail = bodyArray.length ? last(bodyArray) : ELSE; 464 | 465 | return this.asNode( 466 | { type: "ElseStatement", body, ...this.Location(ELSE, tail) }, 467 | ctx 468 | ); 469 | }); 470 | } 471 | 472 | /** 473 | * 474 | * 475 | * @param {NodeContext} ctx 476 | * @returns {ASTNode} 477 | * 478 | * @memberOf ASTVisitor 479 | */ 480 | public ForStatement(ctx: NodeContext): ASTNode { 481 | return this.mapArguments( 482 | ctx, 483 | ({ 484 | FOR, 485 | END_FOR, 486 | init, 487 | test, 488 | counter, 489 | update, 490 | body, 491 | trailingComments, 492 | }) => { 493 | const tail = first(filter([END_FOR, last(this.asArray(body))])); 494 | trailingComments = this.hasComment(trailingComments); 495 | 496 | return this.asNode( 497 | { 498 | type: "ForStatement", 499 | init, 500 | test, 501 | update, 502 | counter, 503 | body, 504 | trailingComments, 505 | ...this.Location(FOR, tail), 506 | }, 507 | ctx 508 | ); 509 | } 510 | ); 511 | } 512 | 513 | /** 514 | * 515 | * 516 | * @param {NodeContext} ctx 517 | * @returns {ASTNode} 518 | * 519 | * @memberOf ASTVisitor 520 | */ 521 | public ForEachStatement(ctx: NodeContext): ASTNode { 522 | return this.mapArguments( 523 | ctx, 524 | ({ FOR, END_FOR, countExpression, body, trailingComments, counter }) => { 525 | const tail = first(filter([END_FOR, last(this.asArray(body))])); 526 | trailingComments = this.hasComment(trailingComments); 527 | 528 | return this.asNode( 529 | { 530 | type: "ForEachStatement", 531 | countExpression, 532 | trailingComments, 533 | counter, 534 | body, 535 | ...this.Location(FOR, tail), 536 | }, 537 | ctx 538 | ); 539 | } 540 | ); 541 | } 542 | 543 | /** 544 | * 545 | * 546 | * @param {NodeContext} ctx 547 | * @returns {ASTNode} 548 | * 549 | * @memberOf ASTVisitor 550 | */ 551 | public NextStatement(ctx: NodeContext): ASTNode { 552 | return this.mapArguments(ctx, ({ NEXT }) => { 553 | return this.asNode( 554 | { 555 | ...this.Location(NEXT, NEXT), 556 | type: "NextStatement", 557 | }, 558 | ctx 559 | ); 560 | }); 561 | } 562 | 563 | /** 564 | * 565 | * 566 | * @param {NodeContext} ctx 567 | * @returns {ASTNode} 568 | * 569 | * @memberOf ASTVisitor 570 | */ 571 | public PrintStatement(ctx: NodeContext): ASTNode { 572 | return this.mapArguments(ctx, ({ PRINT, value }) => { 573 | const valueAsArray = this.asArray(value); 574 | 575 | const tail = valueAsArray.length ? last(valueAsArray) : PRINT; 576 | 577 | return this.asNode( 578 | { 579 | ...this.Location(PRINT, tail), 580 | type: "PrintStatement", 581 | value, 582 | }, 583 | ctx 584 | ); 585 | }); 586 | } 587 | 588 | /** 589 | * 590 | * 591 | * @param {NodeContext} ctx 592 | * @returns {ASTNode} 593 | * 594 | * @memberOf ASTVisitor 595 | */ 596 | public ReturnStatement(ctx: NodeContext): ASTNode { 597 | return this.mapArguments(ctx, ({ RETURN, argument }) => { 598 | return this.asNode( 599 | { 600 | ...this.Location(RETURN, argument ? argument : RETURN), 601 | argument, 602 | type: "ReturnStatement", 603 | }, 604 | ctx 605 | ); 606 | }); 607 | } 608 | 609 | /** 610 | * 611 | * 612 | * @param {NodeContext} ctx 613 | * @returns {ASTNode} 614 | * 615 | * @memberOf ASTVisitor 616 | */ 617 | public StopStatement(ctx: NodeContext): ASTNode { 618 | return this.mapArguments(ctx, ({ STOP }) => { 619 | return this.asNode( 620 | { 621 | ...this.Location(STOP, STOP), 622 | type: "StopStatement", 623 | }, 624 | ctx 625 | ); 626 | }); 627 | } 628 | 629 | /** 630 | * 631 | * 632 | * @param {NodeContext} ctx 633 | * @returns {ASTNode} 634 | * 635 | * @memberOf ASTVisitor 636 | */ 637 | public WhileStatement(ctx: NodeContext): ASTNode { 638 | return this.mapArguments(ctx, ({ WHILE, END_WHILE, test, body }) => { 639 | return this.asNode( 640 | { 641 | type: "WhileStatement", 642 | test, 643 | body, 644 | ...this.Location(WHILE, END_WHILE), 645 | }, 646 | ctx 647 | ); 648 | }); 649 | } 650 | /** 651 | * 652 | * 653 | * @param {NodeContext} ctx 654 | * @returns {ASTNode} 655 | * 656 | * @memberOf ASTVisitor 657 | */ 658 | public RokuTryStatement(ctx: NodeContext): ASTNode { 659 | return this.mapArguments( 660 | ctx, 661 | ({ TRY, END_TRY, body, trailingComments, exception, onError }) => { 662 | return this.asNode( 663 | { 664 | type: "RokuTryStatement", 665 | body, 666 | trailingComments, 667 | exception, 668 | onError, 669 | ...this.Location(TRY, END_TRY), 670 | }, 671 | ctx 672 | ); 673 | } 674 | ); 675 | } 676 | 677 | /** 678 | * 679 | * 680 | * @param {NodeContext} ctx 681 | * @returns {ASTNode} 682 | * 683 | * @memberOf ASTVisitor 684 | */ 685 | public FunctionExpression(ctx: NodeContext): ASTNode { 686 | return this.mapArguments( 687 | ctx, 688 | ({ FUNCTION, END_FUNCTION, body = [], params = [], ReturnType }) => { 689 | return this.asNode( 690 | { 691 | type: "FunctionExpression", 692 | body, 693 | params, 694 | ReturnType, 695 | ...this.Location(FUNCTION, END_FUNCTION), 696 | }, 697 | ctx 698 | ); 699 | } 700 | ); 701 | } 702 | 703 | /** 704 | * 705 | * 706 | * @param {NodeContext} ctx 707 | * @returns {ASTNode} 708 | * 709 | * @memberOf ASTVisitor 710 | */ 711 | public SubExpression(ctx: NodeContext): ASTNode { 712 | return this.mapArguments(ctx, ({ SUB, END_SUB, body, params }) => { 713 | return this.asNode( 714 | { type: "SubExpression", body, params, ...this.Location(SUB, END_SUB) }, 715 | ctx 716 | ); 717 | }); 718 | } 719 | 720 | /** 721 | * 722 | * 723 | * @param {NodeContext} ctx 724 | * @returns {ASTNode} 725 | * 726 | * @memberOf ASTVisitor 727 | */ 728 | public SubDeclaration(ctx: NodeContext): ASTNode { 729 | return this.mapArguments( 730 | ctx, 731 | ({ SUB, END_SUB, id, params, ReturnType, body }) => { 732 | return this.asNode( 733 | { 734 | type: "SubDeclaration", 735 | id, 736 | params, 737 | body, 738 | ReturnType, 739 | ...this.Location(SUB, END_SUB), 740 | }, 741 | ctx 742 | ); 743 | } 744 | ); 745 | } 746 | 747 | /** 748 | * 749 | * 750 | * @param {NodeContext} ctx 751 | * @returns {ASTNode} 752 | * 753 | * @memberOf ASTVisitor 754 | */ 755 | public AssignmentExpression(ctx: NodeContext): ASTNode { 756 | return this.singleNode(ctx); 757 | } 758 | 759 | /** 760 | * 761 | * 762 | * @param {NodeContext} ctx 763 | * @returns {ASTNode} 764 | * 765 | * @memberOf ASTVisitor 766 | */ 767 | public AdditionExpression(ctx: NodeContext): ASTNode { 768 | return ( 769 | this.singleArgument(ctx) || 770 | this.mapArguments(ctx, ({ ADDICTIVE_OPERATOR, left, right }) => 771 | this.flatListExpression( 772 | "AdditionExpression", 773 | ADDICTIVE_OPERATOR, 774 | left, 775 | right 776 | ) 777 | ) 778 | ); 779 | } 780 | 781 | /** 782 | * 783 | * 784 | * @param {NodeContext} ctx 785 | * @returns {ASTNode} 786 | * 787 | * @memberOf ASTVisitor 788 | */ 789 | public MultiplicationExpression(ctx: NodeContext): ASTNode { 790 | return ( 791 | this.singleArgument(ctx) || 792 | this.mapArguments(ctx, ({ MULTI_OPERATOR, left, right }) => 793 | this.flatListExpression( 794 | "MultiplicationExpression", 795 | MULTI_OPERATOR, 796 | left, 797 | right 798 | ) 799 | ) 800 | ); 801 | } 802 | 803 | /** 804 | * 805 | * 806 | * @param {NodeContext} ctx 807 | * @returns {ASTNode} 808 | * 809 | * @memberOf ASTVisitor 810 | */ 811 | public ShiftExpression(ctx: NodeContext): ASTNode { 812 | return this.singleNode(ctx); 813 | } 814 | 815 | /** 816 | * 817 | * 818 | * @param {NodeContext} ctx 819 | * @returns {ASTNode} 820 | * 821 | * @memberOf ASTVisitor 822 | */ 823 | public RelationExpression(ctx: NodeContext): ASTNode { 824 | return ( 825 | this.singleArgument(ctx) || 826 | this.mapArguments(ctx, ({ RELATIONAL_OPERATOR, left, right }) => 827 | this.flatListExpression( 828 | "RelationExpression", 829 | RELATIONAL_OPERATOR, 830 | left, 831 | right 832 | ) 833 | ) 834 | ); 835 | } 836 | 837 | /** 838 | * 839 | * 840 | * @param {NodeContext} ctx 841 | * @returns {ASTNode} 842 | * 843 | * @memberOf ASTVisitor 844 | */ 845 | public EqualityExpression(ctx: NodeContext): ASTNode { 846 | return ( 847 | this.singleArgument(ctx) || 848 | this.mapArguments(ctx, ({ EQUALITY_OPERATOR, left, right }) => { 849 | const head = first(this.asArray(left)); 850 | const tail = last(this.asArray(right)); 851 | 852 | return this.asNode( 853 | { 854 | ...this.Location(head, tail), 855 | left, 856 | operator: EQUALITY_OPERATOR, 857 | right, 858 | type: "AssignmentExpression", 859 | }, 860 | ctx 861 | ); 862 | }) 863 | ); 864 | } 865 | 866 | /** 867 | * 868 | * 869 | * @param {NodeContext} ctx 870 | * @returns {ASTNode} 871 | * 872 | * @memberOf ASTVisitor 873 | */ 874 | public LogicExpression(ctx: NodeContext): ASTNode { 875 | return ( 876 | this.singleArgument(ctx) || 877 | this.mapArguments(ctx, ({ LOGIC_OPERATOR, left, right }) => 878 | this.flatListExpression("LogicExpression", LOGIC_OPERATOR, left, right) 879 | ) 880 | ); 881 | } 882 | 883 | /** 884 | * 885 | * 886 | * @param {NodeContext} ctx 887 | * @returns {ASTNode} 888 | * 889 | * @memberOf ASTVisitor 890 | */ 891 | public UnaryExpression(ctx: NodeContext): ASTNode { 892 | return ( 893 | this.singleArgument(ctx) || 894 | this.mapArguments(ctx, ({ UNARY, right }) => { 895 | return this.asNode( 896 | { 897 | type: "UnaryExpression", 898 | operator: UNARY, 899 | argument: right, 900 | ...this.Location(UNARY, right), 901 | }, 902 | ctx 903 | ); 904 | }) 905 | ); 906 | } 907 | 908 | /** 909 | * 910 | * 911 | * @param {NodeContext} ctx 912 | * @returns {ASTNode} 913 | * 914 | * @memberOf ASTVisitor 915 | */ 916 | public Arguments(ctx: NodeContext): ASTNode { 917 | return this.mapArguments(ctx, ({ OPEN_PAREN, CLOSE_PAREN, param = [] }) => { 918 | return this.asNode( 919 | { type: "Arguments", param, ...this.Location(OPEN_PAREN, CLOSE_PAREN) }, 920 | ctx 921 | ); 922 | }); 923 | } 924 | 925 | /** 926 | * 927 | * 928 | * @param {NodeContext} ctx 929 | * @returns {ASTNode} 930 | * 931 | * @memberOf ASTVisitor 932 | */ 933 | public PostfixExpression(ctx: NodeContext): ASTNode { 934 | return ( 935 | this.singleArgument(ctx) || 936 | this.mapArguments(ctx, ({ POSTFIX, left }) => { 937 | return this.asNode( 938 | { 939 | type: "PostfixExpression", 940 | operator: POSTFIX, 941 | argument: left, 942 | ...this.Location(left, POSTFIX), 943 | }, 944 | ctx 945 | ); 946 | }) 947 | ); 948 | } 949 | 950 | /** 951 | * 952 | * 953 | * @param {NodeContext} { id, args } 954 | * @returns {ASTNode} 955 | * 956 | * @memberOf ASTVisitor 957 | */ 958 | public CallMemberExpression({ id, args }: NodeContext): ASTNode { 959 | return { 960 | ...this.Location(id, args), 961 | args, 962 | callee: id, 963 | type: "CallExpression", 964 | }; 965 | } 966 | public CallExpression(ctx: NodeContext): ASTNode { 967 | return this.mapArguments(ctx, ({ id, args }) => { 968 | return this.asNode( 969 | { 970 | ...this.Location(id, args), 971 | args, 972 | callee: id, 973 | type: "CallExpression", 974 | }, 975 | ctx 976 | ); 977 | }); 978 | } 979 | 980 | /** 981 | * 982 | * 983 | * @param {NodeContext} { id, properties } 984 | * @returns {ASTNode} 985 | * 986 | * @memberOf ASTVisitor 987 | */ 988 | public ObjectMemberExpression({ id, properties = [] }: NodeContext): ASTNode { 989 | properties = this.asArray(properties); 990 | 991 | return { 992 | ...this.Location(id, last(properties)), 993 | computed: false, 994 | object: id, 995 | properties, 996 | type: "MemberExpression", 997 | }; 998 | } 999 | 1000 | /** 1001 | * 1002 | * 1003 | * @param {NodeContext} ctx 1004 | * @returns {ASTNode} 1005 | * 1006 | * @memberOf ASTVisitor 1007 | */ 1008 | public MemberExpression(ctx: NodeContext): ASTNode { 1009 | return ( 1010 | this.singleArgument(ctx) || 1011 | this.mapArguments(ctx, ({ id, properties = [], args = "" }) => { 1012 | if (args) { 1013 | return this.asNode(this.CallMemberExpression({ id, args }), ctx); 1014 | } else { 1015 | return this.asNode( 1016 | this.ObjectMemberExpression({ id, properties }), 1017 | ctx 1018 | ); 1019 | } 1020 | }) 1021 | ); 1022 | } 1023 | 1024 | /** 1025 | * 1026 | * 1027 | * @param {NodeContext} ctx 1028 | * @returns {ASTNode} 1029 | * 1030 | * @memberOf ASTVisitor 1031 | */ 1032 | public MemberChunkExpression(ctx: NodeContext): ASTNode { 1033 | return ( 1034 | this.singleArgument(ctx) || 1035 | this.mapArguments(ctx, ({ property, args }) => { 1036 | if (args) { 1037 | return this.CallMemberExpression({ id: property, args }); 1038 | } else { 1039 | return this.ObjectMemberExpression({ id: property, properties: [] }); 1040 | } 1041 | }) 1042 | ); 1043 | } 1044 | 1045 | /** 1046 | * 1047 | * 1048 | * @param {NodeContext} ctx 1049 | * @returns {ASTNode} 1050 | * 1051 | * @memberOf ASTVisitor 1052 | */ 1053 | public DotMemberExpression(ctx: NodeContext): ASTNode { 1054 | return ( 1055 | this.singleArgument(ctx) || 1056 | this.asNode( 1057 | this.mapArguments(ctx, ({ operator, right }) => ({ 1058 | ...this.Location(operator, right), 1059 | operator, 1060 | right, 1061 | type: "DotMemberExpression", 1062 | })), 1063 | ctx 1064 | ) 1065 | ); 1066 | } 1067 | 1068 | /** 1069 | * 1070 | * 1071 | * @param {NodeContext} ctx 1072 | * @returns {ASTNode} 1073 | * 1074 | * @memberOf ASTVisitor 1075 | */ 1076 | public PrimaryExpression(ctx: NodeContext): ASTNode { 1077 | return this.singleNode(ctx); 1078 | } 1079 | 1080 | /** 1081 | * 1082 | * 1083 | * @param {NodeContext} ctx 1084 | * @returns {ASTNode} 1085 | * 1086 | * @memberOf ASTVisitor 1087 | */ 1088 | public ParenthesisExpression(ctx: NodeContext): ASTNode { 1089 | return this.mapArguments( 1090 | ctx, 1091 | ({ OPEN_PAREN, CLOSE_PAREN, innerExpression }) => { 1092 | return this.asNode( 1093 | { 1094 | ...this.Location(OPEN_PAREN, CLOSE_PAREN), 1095 | expression: innerExpression, 1096 | type: "ParenthesisExpression", 1097 | }, 1098 | ctx 1099 | ); 1100 | } 1101 | ); 1102 | } 1103 | /** 1104 | * 1105 | * 1106 | * @param {NodeContext} ctx 1107 | * @returns {ASTNode} 1108 | * 1109 | * @memberOf ASTVisitor 1110 | */ 1111 | public Literal(ctx: NodeContext): ASTNode { 1112 | return this.mapArguments(ctx, ({ LITERAL }) => { 1113 | const { loc, range } = LITERAL; 1114 | 1115 | return this.asNode( 1116 | { type: "Literal", range, raw: loc.source, value: loc.source, loc }, 1117 | ctx 1118 | ); 1119 | }); 1120 | } 1121 | 1122 | /** 1123 | * 1124 | * 1125 | * @param {NodeContext} ctx 1126 | * @returns {ASTNode} 1127 | * 1128 | * @memberOf ASTVisitor 1129 | */ 1130 | public ReservedWord(ctx: NodeContext): ASTNode { 1131 | return this.singleNode(ctx); 1132 | } 1133 | 1134 | /** 1135 | * 1136 | * 1137 | * @param {NodeContext} ctx 1138 | * @returns {ASTNode} 1139 | * 1140 | * @memberOf ASTVisitor 1141 | */ 1142 | public ConditionalCompilationStatement(ctx: NodeContext): ASTNode { 1143 | return this.singleNode(ctx); 1144 | } 1145 | 1146 | /** 1147 | * 1148 | * 1149 | * @param {NodeContext} ctx 1150 | * @returns {ASTNode} 1151 | * 1152 | * @memberOf ASTVisitor 1153 | */ 1154 | public ConditionalConst(ctx: NodeContext): ASTNode { 1155 | return this.mapArguments(ctx, ({ CONDITIONAL_CONST, assignment }) => { 1156 | return this.asNode( 1157 | { 1158 | type: "ConditionalConst", 1159 | assignment, 1160 | ...this.Location(CONDITIONAL_CONST, assignment), 1161 | }, 1162 | ctx 1163 | ); 1164 | }); 1165 | } 1166 | 1167 | /** 1168 | * 1169 | * 1170 | * @param {NodeContext} ctx 1171 | * @returns {ASTNode} 1172 | * 1173 | * @memberOf ASTVisitor 1174 | */ 1175 | public ConditionalError(ctx: NodeContext): ASTNode { 1176 | return this.mapArguments(ctx, ({ CONDITIONAL_ERROR }) => { 1177 | return this.asNode( 1178 | { 1179 | type: "ConditionalError", 1180 | error: CONDITIONAL_ERROR, 1181 | ...this.Location(CONDITIONAL_ERROR, CONDITIONAL_ERROR), 1182 | }, 1183 | ctx 1184 | ); 1185 | }); 1186 | } 1187 | 1188 | /** 1189 | * 1190 | * 1191 | * @param {NodeContext} ctx 1192 | * @returns {ASTNode} 1193 | * 1194 | * @memberOf ASTVisitor 1195 | */ 1196 | public ConditionalIfStatement(ctx: NodeContext): ASTNode { 1197 | return this.mapArguments( 1198 | ctx, 1199 | ({ CONDITIONAL_IF, CONDITIONAL_END_IF, body, test, alternate }) => { 1200 | return this.asNode( 1201 | { 1202 | alternate, 1203 | body, 1204 | test, 1205 | type: "ConditionalIfStatement", 1206 | ...this.Location(CONDITIONAL_IF, CONDITIONAL_END_IF), 1207 | }, 1208 | ctx 1209 | ); 1210 | } 1211 | ); 1212 | } 1213 | 1214 | /** 1215 | * 1216 | * 1217 | * @param {NodeContext} ctx 1218 | * @returns {ASTNode} 1219 | * 1220 | * @memberOf ASTVisitor 1221 | */ 1222 | public ConditionalElseIfStatement(ctx: NodeContext): ASTNode { 1223 | return this.mapArguments( 1224 | ctx, 1225 | ({ CONDITIONAL_ELSE_IF, test, body, trailingComments = [] }) => { 1226 | const tail = last(this.asArray(body)); 1227 | trailingComments = this.hasComment(trailingComments); 1228 | 1229 | return this.asNode( 1230 | { 1231 | body, 1232 | test, 1233 | trailingComments, 1234 | type: "ConditionalElseIfStatement", 1235 | ...this.Location(CONDITIONAL_ELSE_IF, tail), 1236 | }, 1237 | ctx 1238 | ); 1239 | } 1240 | ); 1241 | } 1242 | 1243 | /** 1244 | * 1245 | * 1246 | * @param {NodeContext} ctx 1247 | * @returns {ASTNode} 1248 | * 1249 | * @memberOf ASTVisitor 1250 | */ 1251 | public ConditionalElseStatement(ctx: NodeContext): ASTNode { 1252 | return this.mapArguments(ctx, ({ CONDITIONAL_ELSE, body }) => { 1253 | const tail = last(this.asArray(body)); 1254 | 1255 | return this.asNode( 1256 | { 1257 | body, 1258 | type: "ConditionalElseStatement", 1259 | ...this.Location(CONDITIONAL_ELSE, tail), 1260 | }, 1261 | ctx 1262 | ); 1263 | }); 1264 | } 1265 | 1266 | /** 1267 | * 1268 | * 1269 | * @param {NodeContext} ctx 1270 | * @returns {ASTNode} 1271 | * 1272 | * @memberOf ASTVisitor 1273 | */ 1274 | public UnTypedIdentifier(ctx: NodeContext): ASTNode { 1275 | return this.mapArguments(ctx, ({ IDENTIFIER }) => { 1276 | const { loc, range } = IDENTIFIER; 1277 | return this.asNode( 1278 | { type: "UnTypedIdentifier", name: loc.source, loc, range }, 1279 | ctx 1280 | ); 1281 | }); 1282 | } 1283 | 1284 | /** 1285 | * 1286 | * 1287 | * @param {NodeContext} ctx 1288 | * @returns {ASTNode} 1289 | * 1290 | * @memberOf ASTVisitor 1291 | */ 1292 | public ParameterList(ctx: NodeContext): ASTNode { 1293 | return this.mapArguments(ctx, ({ Parameter, OPEN_PAREN, CLOSE_PAREN }) => { 1294 | return this.asNode( 1295 | { 1296 | ...this.Location(OPEN_PAREN, CLOSE_PAREN), 1297 | args: this.asArray(Parameter), 1298 | type: "ParameterList", 1299 | }, 1300 | ctx 1301 | ); 1302 | }); 1303 | } 1304 | 1305 | /** 1306 | * 1307 | * 1308 | * @param {NodeContext} ctx 1309 | * @returns {ASTNode} 1310 | * 1311 | * @memberOf ASTVisitor 1312 | */ 1313 | public Parameter(ctx: NodeContext): ASTNode { 1314 | return this.mapArguments(ctx, ({ Identifier, TypeAnnotation, value }) => { 1315 | const tail = last(filter([Identifier, TypeAnnotation, value])); 1316 | 1317 | return this.asNode( 1318 | { 1319 | type: "Parameter", 1320 | name: Identifier, 1321 | TypeAnnotation, 1322 | value, 1323 | ...this.Location(Identifier, tail), 1324 | }, 1325 | ctx 1326 | ); 1327 | }); 1328 | } 1329 | 1330 | /** 1331 | * 1332 | * 1333 | * @param {NodeContext} ctx 1334 | * @returns {ASTNode} 1335 | * 1336 | * @memberOf ASTVisitor 1337 | */ 1338 | public Identifier(ctx: NodeContext): ASTNode { 1339 | return this.mapArguments(ctx, ({ asType = "", id }) => { 1340 | return this.asNode({ asType, ...id, type: "Identifier" }, ctx); 1341 | }); 1342 | } 1343 | 1344 | /** 1345 | * 1346 | * 1347 | * @param {NodeContext} ctx 1348 | * @returns {ASTNode} 1349 | * 1350 | * @memberOf ASTVisitor 1351 | */ 1352 | public TypeAnnotation(ctx: NodeContext): ASTNode { 1353 | const { loc, range } = this.singleNode(ctx); 1354 | return this.asNode( 1355 | { loc, range, value: loc.source, type: "TypeAnnotation" }, 1356 | ctx 1357 | ); 1358 | } 1359 | 1360 | /** 1361 | * 1362 | * 1363 | * @param {NodeContext} ctx 1364 | * @returns {ASTNode} 1365 | * 1366 | * @memberOf ASTVisitor 1367 | */ 1368 | public Comment(ctx: NodeContext): ASTNode { 1369 | const COMMENT = this.singleNode(ctx) as ASTNode; 1370 | 1371 | let value = null; 1372 | 1373 | if (COMMENT.type === "COMMENT_QUOTE") { 1374 | value = COMMENT.loc.source.substr(1).trim(); 1375 | } else { 1376 | value = COMMENT.loc.source.substr(3).trim(); 1377 | } 1378 | 1379 | return this.asNode( 1380 | { type: "Comment", value, ...this.Location(COMMENT, COMMENT) }, 1381 | ctx 1382 | ); 1383 | } 1384 | 1385 | /** 1386 | * 1387 | * 1388 | * @param {NodeContext} trailingComments 1389 | * @returns 1390 | * 1391 | * @memberOf ASTVisitor 1392 | */ 1393 | public hasComment(trailingComments) { 1394 | if (trailingComments && trailingComments.type === "Comment") { 1395 | return trailingComments; 1396 | } 1397 | 1398 | return ""; 1399 | } 1400 | } 1401 | --------------------------------------------------------------------------------