Repository: rokucommunity/brighterscript
Pull request #1109 (2024-03-13 19:08:33)

Fix transpile crash when token has no text
Fix crash in during transpile when token is defined but doesn't have `text` (no idea how this happens, but better to prevent crashing)


Patch:
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index 5dd8bab9..b567bf19 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -25,6 +25,8 @@ import { SymbolTypeFlag } from '../SymbolTypeFlag';
 import { ClassType, EnumType, FloatType, InterfaceType } from '../types';
 import type { StandardizedFileEntry } from 'roku-deploy';
 import * as fileUrl from 'file-url';
+import { isAALiteralExpression } from '../astUtils/reflection';
+import type { AALiteralExpression } from '../parser/Expression';
 
 let sinon = sinonImport.createSandbox();
 
@@ -1665,6 +1667,23 @@ describe('BrsFile', () => {
     });
 
     describe('transpile', () => {
+        it('does not crash when AA is missing closing curly token', async () => {
+            const file = program.setFile<BrsFile>('source/main.bs', `
+                sub main()
+                    aa = {}
+                end sub
+            `);
+            //delete the ending token `}`
+            const aa = file.ast.findChild<AALiteralExpression>(isAALiteralExpression);
+            delete (aa.tokens as any).close;
+
+            await testTranspile(file, `
+                sub main()
+                    aa = {}
+                end sub
+            `, undefined, undefined, false);
+        });
+
         describe('null tokens', () => {
             it('succeeds when token locations are omitted', () => {
                 doTest(`
diff --git a/src/parser/TranspileState.ts b/src/parser/TranspileState.ts
index 92f1299e..60b86562 100644
--- a/src/parser/TranspileState.ts
+++ b/src/parser/TranspileState.ts
@@ -134,7 +134,7 @@ export class TranspileState {
      * Create a SourceNode from a token, accounting for missing range and multi-line text
      */
     public transpileToken(token: TranspileToken, defaultValue?: string): TranspileResult {
-        if (!token && defaultValue !== undefined) {
+        if (!token?.text && defaultValue !== undefined) {
             return [new SourceNode(null, null, null, defaultValue)];
         }
         const leadingCommentsSourceNodes = this.transpileLeadingComments(token);
@@ -143,7 +143,7 @@ export class TranspileState {
             return [new SourceNode(null, null, null, [...leadingCommentsSourceNodes, token.text])];
         }
         //split multi-line text
-        if (token.range.end.line > token.range.start.line) {
+        if (token.range?.end.line > token.range?.start.line) {
             const lines = token.text.split(/\r?\n/g);
             const code = [
                 this.sourceNode(token, [...leadingCommentsSourceNodes, lines[0]])


Test patch:
diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts
index 4cb5e10f..4865c3c2 100644
--- a/src/testHelpers.spec.ts
+++ b/src/testHelpers.spec.ts
@@ -258,10 +258,18 @@ function getTestFileAction(
     action: (file: BscFile) => Promise<{ code: string; map?: string }>,
     scopeGetter: () => [program: Program, rootDir: string]
 ) {
-    return async function testFileAction<TFile extends BscFile = BscFile>(source: string, expected?: string, formatType: 'trim' | 'none' = 'trim', destPath = 'source/main.bs', failOnDiagnostic = true) {
+    return async function testFileAction<TFile extends BscFile = BscFile>(sourceOrFile: string | TFile, expected?: string, formatType: 'trim' | 'none' = 'trim', destPath = 'source/main.bs', failOnDiagnostic = true) {
         let [program, rootDir] = scopeGetter();
-        expected = expected ? expected : source;
-        let file = program.setFile<TFile>({ src: s`${rootDir}/${destPath}`, dest: destPath }, source);
+        let file: TFile;
+        if (typeof sourceOrFile === 'string') {
+            expected = expected ? expected : sourceOrFile;
+            file = program.setFile<TFile>({ src: s`${rootDir}/${destPath}`, dest: destPath }, sourceOrFile);
+        } else {
+            file = sourceOrFile;
+            if (!expected) {
+                throw new Error('`expected` is required when passing a file');
+            }
+        }
         program.validate();
         if (failOnDiagnostic !== false) {
             expectZeroDiagnostics(program);
@@ -279,7 +287,7 @@ function getTestFileAction(
         expect(sources[0]).to.equal(sources[1]);
         return {
             file: file,
-            source: source,
+            source: sourceOrFile,
             expected: expected,
             actual: codeWithMap.code,
             map: codeWithMap.map