Repository: rokucommunity/brighterscript
Pull request #1225 (2024-06-10 18:42:13)

Allow updating leading trivia
- All AstNodes will return an editable array of tokens when calling `.getLeadingTrivia()`
- No longer concatenates leading trivia of annotations above a statement
- Refined utils functions that get node documentation to ensure all comments are included, and these concatenate leading trivia of associated annotations 


Addresses #1206


Patch:
diff --git a/src/bscPlugin/SignatureHelpUtil.ts b/src/bscPlugin/SignatureHelpUtil.ts
index b2566bb4..831aea40 100644
--- a/src/bscPlugin/SignatureHelpUtil.ts
+++ b/src/bscPlugin/SignatureHelpUtil.ts
@@ -78,7 +78,7 @@ export class SignatureHelpUtil {
         const funcStartPosition = func.range.start;
 
         // Get function comments in reverse order
-        const trivia = func.getLeadingTrivia().reverse();
+        const trivia = util.concatAnnotationLeadingTrivia(func).reverse();
         let functionComments = [] as string[];
 
         for (const currentToken of trivia) {
diff --git a/src/lexer/Lexer.ts b/src/lexer/Lexer.ts
index 505a99b8..a2a39928 100644
--- a/src/lexer/Lexer.ts
+++ b/src/lexer/Lexer.ts
@@ -111,7 +111,7 @@ export class Lexer {
                 ? util.createRange(this.lineBegin, this.columnBegin, this.lineEnd, this.columnEnd + 1)
                 : undefined,
             leadingWhitespace: this.leadingWhitespace,
-            leadingTrivia: this.leadingTrivia
+            leadingTrivia: this.leadingTrivia ?? []
         });
         this.leadingWhitespace = '';
         return this;
diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts
index f79adccf..b6af9276 100644
--- a/src/parser/Expression.ts
+++ b/src/parser/Expression.ts
@@ -256,11 +256,11 @@ export class FunctionExpression extends Expression implements TypedefProvider {
     public functionStatement?: FunctionStatement;
 
     public getLeadingTrivia(): Token[] {
-        return this.tokens.functionType?.leadingTrivia ?? [];
+        return this.tokens.functionType?.leadingTrivia;
     }
 
     public getEndTrivia(): Token[] {
-        return this.tokens.endFunctionType?.leadingTrivia ?? [];
+        return this.tokens.endFunctionType?.leadingTrivia;
     }
 
     /**
@@ -515,7 +515,7 @@ export class FunctionParameterExpression extends Expression {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.name.leadingTrivia ?? [];
+        return this.tokens.name.leadingTrivia;
     }
 }
 
@@ -779,7 +779,7 @@ export class GroupingExpression extends Expression {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.leftParen?.leadingTrivia ?? [];
+        return this.tokens.leftParen?.leadingTrivia;
     }
 }
 
@@ -833,7 +833,7 @@ export class LiteralExpression extends Expression {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.value.leadingTrivia ?? [];
+        return this.tokens.value.leadingTrivia;
     }
 }
 
@@ -938,11 +938,11 @@ export class ArrayLiteralExpression extends Expression {
         return new ArrayType(...innerTypes);
     }
     getLeadingTrivia(): Token[] {
-        return this.tokens.open?.leadingTrivia ?? [];
+        return this.tokens.open?.leadingTrivia;
     }
 
     getEndTrivia(): Token[] {
-        return this.tokens.close?.leadingTrivia ?? [];
+        return this.tokens.close?.leadingTrivia;
     }
 }
 
@@ -991,7 +991,7 @@ export class AAMemberExpression extends Expression {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.key.leadingTrivia ?? [];
+        return this.tokens.key.leadingTrivia;
     }
 
 }
@@ -1091,11 +1091,11 @@ export class AALiteralExpression extends Expression {
     }
 
     public getLeadingTrivia(): Token[] {
-        return this.tokens.open?.leadingTrivia ?? [];
+        return this.tokens.open?.leadingTrivia;
     }
 
     public getEndTrivia(): Token[] {
-        return this.tokens.close?.leadingTrivia ?? [];
+        return this.tokens.close?.leadingTrivia;
     }
 
 }
@@ -1150,7 +1150,7 @@ export class UnaryExpression extends Expression {
     }
 
     public getLeadingTrivia(): Token[] {
-        return this.tokens.operator.leadingTrivia ?? [];
+        return this.tokens.operator.leadingTrivia;
     }
 }
 
@@ -1221,7 +1221,7 @@ export class VariableExpression extends Expression {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.name.leadingTrivia ?? [];
+        return this.tokens.name.leadingTrivia;
     }
 }
 
@@ -1337,7 +1337,7 @@ export class SourceLiteralExpression extends Expression {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.value.leadingTrivia ?? [];
+        return this.tokens.value.leadingTrivia;
     }
 }
 
@@ -1407,7 +1407,7 @@ export class NewExpression extends Expression {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.new.leadingTrivia ?? [];
+        return this.tokens.new.leadingTrivia;
     }
 }
 
@@ -1823,7 +1823,7 @@ export class AnnotationExpression extends Expression {
     }
 
     public getLeadingTrivia(): Token[] {
-        return this.tokens.at?.leadingTrivia ?? [];
+        return this.tokens.at?.leadingTrivia;
     }
 
     transpile(state: BrsTranspileState) {
@@ -2104,7 +2104,7 @@ export class RegexLiteralExpression extends Expression {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.regexLiteral?.leadingTrivia ?? [];
+        return this.tokens.regexLiteral?.leadingTrivia;
     }
 }
 
diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts
index 12ef0a77..aab0f658 100644
--- a/src/parser/Parser.spec.ts
+++ b/src/parser/Parser.spec.ts
@@ -1660,13 +1660,20 @@ describe('parser', () => {
                 ' hello comment 1
                 ' hello comment 2
                 @annotation
+                ' hello comment 3
+                @otherAnnotation
                 sub sayHello(name as string = "world")
                 end sub
             `, ParseMode.BrighterScript);
             const funcStatements = statements.filter(isFunctionStatement);
             const helloTrivia = funcStatements[0].getLeadingTrivia();
-            expect(helloTrivia.length).to.be.greaterThan(0);
-            expect(helloTrivia.filter(t => t.kind === TokenKind.Comment).length).to.eq(2);
+            expect(helloTrivia.filter(t => t.kind === TokenKind.Comment).length).to.eq(0);
+            const helloAnnotationTrivia = funcStatements[0].annotations[0].getLeadingTrivia();
+            expect(helloAnnotationTrivia.length).to.be.greaterThan(0);
+            expect(helloAnnotationTrivia.filter(t => t.kind === TokenKind.Comment).length).to.eq(2);
+            const otherAnnotationTrivia = funcStatements[0].annotations[1].getLeadingTrivia();
+            expect(otherAnnotationTrivia.length).to.be.greaterThan(0);
+            expect(otherAnnotationTrivia.filter(t => t.kind === TokenKind.Comment).length).to.eq(1);
         });
 
 
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index 9bf8814f..f234f719 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -1810,12 +1810,14 @@ export class Parser {
         });
     }
 
-    private ifStatement(): IfStatement {
+    private nestedInlineConditionalCount = 0;
+
+    private ifStatement(incrementNestedCount = true): IfStatement {
         // colon before `if` is usually not allowed, unless it's after `then`
         if (this.current > 0) {
             const prev = this.previous();
             if (prev.kind === TokenKind.Colon) {
-                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then) {
+                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then && this.nestedInlineConditionalCount === 0) {
                     this.diagnostics.push({
                         ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
                         range: prev.range
@@ -1844,6 +1846,9 @@ export class Parser {
 
         if (isInlineIfThen) {
             /*** PARSE INLINE IF STATEMENT ***/
+            if (!incrementNestedCount) {
+                this.nestedInlineConditionalCount++;
+            }
 
             thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
 
@@ -1863,7 +1868,7 @@ export class Parser {
 
                 if (this.check(TokenKind.If)) {
                     // recurse-read `else if`
-                    elseBranch = this.ifStatement();
+                    elseBranch = this.ifStatement(false);
 
                     //no multi-line if chained with an inline if
                     if (!elseBranch.isInline) {
@@ -1901,7 +1906,7 @@ export class Parser {
             if (!elseBranch || !isIfStatement(elseBranch)) {
                 //enforce newline at the end of the inline if statement
                 const peek = this.peek();
-                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && !this.isAtEnd()) {
+                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && peek.kind !== TokenKind.Else && !this.isAtEnd()) {
                     //ignore last error if it was about a colon
                     if (this.previous().kind === TokenKind.Colon) {
                         this.diagnostics.pop();
@@ -1914,7 +1919,7 @@ export class Parser {
                     });
                 }
             }
-
+            this.nestedInlineConditionalCount--;
         } else {
             /*** PARSE MULTI-LINE IF STATEMENT ***/
 
@@ -1960,8 +1965,7 @@ export class Parser {
             else: elseToken,
             condition: condition,
             thenBranch: thenBranch,
-            elseBranch: elseBranch,
-            isInline: isInlineIfThen
+            elseBranch: elseBranch
         });
     }
 
diff --git a/src/parser/Statement.spec.ts b/src/parser/Statement.spec.ts
index 9774fc37..d36cee11 100644
--- a/src/parser/Statement.spec.ts
+++ b/src/parser/Statement.spec.ts
@@ -1,6 +1,6 @@
 import { expect } from '../chai-config.spec';
-import type { NamespaceStatement, ClassStatement } from './Statement';
-import { Body, EmptyStatement } from './Statement';
+import { NamespaceStatement, ClassStatement } from './Statement';
+import { AssignmentStatement, Block, Body, CatchStatement, DottedSetStatement, EmptyStatement, EndStatement, ExitForStatement, ExitWhileStatement, ExpressionStatement, ForEachStatement, ForStatement, FunctionStatement, GotoStatement, IfStatement, ImportStatement, IncrementStatement, IndexedSetStatement, LabelStatement, LibraryStatement, PrintStatement, ReturnStatement, StopStatement, ThrowStatement, TryCatchStatement, WhileStatement } from './Statement';
 import { ParseMode, Parser } from './Parser';
 import { WalkMode } from '../astUtils/visitors';
 import { isClassStatement, isNamespaceStatement } from '../astUtils/reflection';
@@ -8,6 +8,10 @@ import { Program } from '../Program';
 import { trim } from '../testHelpers.spec';
 import type { BrsFile } from '../files/BrsFile';
 import { tempDir } from '../testHelpers.spec';
+import { createStringLiteral, createToken, createVariableExpression } from '../astUtils/creators';
+import { TokenKind } from '../lexer/TokenKind';
+import { FunctionExpression } from './Expression';
+import type { AstNode } from './AstNode';
 
 describe('Statement', () => {
     let program: Program;
@@ -111,4 +115,94 @@ describe('Statement', () => {
         });
     });
 
+
+    describe('all Statements', () => {
+
+        let allStatements: AstNode[] = [];
+
+        beforeEach(() => {
+            const ident = createToken(TokenKind.Identifier, 'a');
+            const expr = createStringLiteral('');
+            const token = createToken(TokenKind.StringLiteral, '');
+            const body = new Body({ statements: [] });
+            const assignment = new AssignmentStatement({ equals: undefined, name: ident, value: expr });
+            const block = new Block({ statements: [] });
+            const expression = new ExpressionStatement({ expression: expr });
+            const exitFor = new ExitForStatement({ exitFor: token });
+            const exitWhile = new ExitWhileStatement({ exitWhile: token });
+            const funs = new FunctionStatement({
+                name: ident,
+                func: new FunctionExpression({
+                    parameters: [],
+                    body: block,
+                    functionType: token,
+                    leftParen: token,
+                    rightParen: token,
+                    endFunctionType: token
+                })
+            });
+            const ifs = new IfStatement({ if: token, condition: expr, thenBranch: block });
+            const increment = new IncrementStatement({ value: expr, operator: token });
+            const print = new PrintStatement({ print: token, expressions: [] });
+            const gotos = new GotoStatement({ goto: token, label: token });
+            const labels = new LabelStatement({ name: ident, colon: token });
+            const returns = new ReturnStatement({ return: token });
+            const ends = new EndStatement({ end: token });
+            const stop = new StopStatement({ stop: token });
+            const fors = new ForStatement({ for: token, counterDeclaration: assignment, to: token, finalValue: expr, body: block, endFor: token, step: token, increment: expr });
+            const foreach = new ForEachStatement({ forEach: token, in: token, endFor: token, item: token, target: expr, body: block });
+            const whiles = new WhileStatement({ while: token, endWhile: token, condition: expr, body: block });
+            const dottedSet = new DottedSetStatement({ obj: expr, name: ident, value: expr });
+            const indexedSet = new IndexedSetStatement({ obj: expr, indexes: [expr], value: expr, openingSquare: token, closingSquare: token });
+            const library = new LibraryStatement({ library: token, filePath: token });
+            const namespace = new NamespaceStatement({ namespace: token, nameExpression: createVariableExpression('a'), body: body, endNamespace: token });
+            const cls = new ClassStatement({ class: token, name: ident, body: [], endClass: token });
+            const imports = new ImportStatement({ import: token, path: token });
+            const catchStmt = new CatchStatement({ catch: token, exceptionVariable: ident, catchBranch: block });
+            const tryCatch = new TryCatchStatement({ try: token, tryBranch: block, catchStatement: catchStmt });
+            const throwSt = new ThrowStatement({ throw: createToken(TokenKind.Throw) });
+
+            allStatements = [
+                expression,
+                exitFor,
+                exitWhile,
+                funs,
+                ifs,
+                increment,
+                print,
+                gotos,
+                labels,
+                returns,
+                ends,
+                stop,
+                fors,
+                foreach,
+                whiles,
+                dottedSet,
+                indexedSet,
+                library,
+                namespace,
+                cls,
+                imports,
+                catchStmt,
+                tryCatch,
+                throwSt
+            ];
+
+        });
+
+        it('has editable leading trivia', () => {
+            for (const stmt of allStatements) {
+                const beforeTrivia = stmt.getLeadingTrivia();
+                expect(beforeTrivia.length, `${stmt.kind} already has leading trivia`).to.eq(0);
+                stmt.getLeadingTrivia().push(createToken(TokenKind.Comment, 'This is an added comment'));
+                const afterComments = stmt.getLeadingTrivia().filter(t => t.kind === TokenKind.Comment);
+                expect(afterComments.length, `${stmt.kind} leading trivia was not edited`).to.eq(1);
+                expect(afterComments[0].text, `${stmt.kind} leading trivia was not edited`).to.eq('This is an added comment');
+                stmt.getLeadingTrivia().pop();
+            }
+        });
+
+    });
+
 });
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 51d1a72f..461e90d4 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -185,7 +185,7 @@ export class AssignmentStatement extends Statement {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.name.leadingTrivia ?? [];
+        return this.tokens.name.leadingTrivia;
     }
 }
 
@@ -386,6 +386,10 @@ export class Block extends Statement {
         return results;
     }
 
+    public getLeadingTrivia(): Token[] {
+        return this.statements[0]?.getLeadingTrivia() ?? [];
+    }
+
     walk(visitor: WalkVisitor, options: WalkOptions) {
         if (options.walkMode & InternalWalkMode.walkStatements) {
             walkArray(this.statements, visitor, options, this);
@@ -451,7 +455,7 @@ export class ExitForStatement extends Statement {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.exitFor?.leadingTrivia ?? [];
+        return this.tokens.exitFor?.leadingTrivia;
     }
 
 }
@@ -484,7 +488,7 @@ export class ExitWhileStatement extends Statement {
     }
 
     getLeadingTrivia(): Token[] {
-        return this.tokens.exitWhile?.leadingTrivia ?? [];
+        return this.tokens.exitWhile?.leadingTrivia;
     }
 }
 
@@ -528,7 +532,7 @@ export class FunctionStatement extends Statement implements TypedefProvider {
     }
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.func.getLeadingTrivia());
+        return this.func.getLeadingTrivia();
     }
 
     transpile(state: BrsTranspileState) {
@@ -587,13 +591,11 @@ export class IfStatement extends Statement {
         condition: Expression;
         thenBranch: Block;
         elseBranch?: IfStatement | Block;
-        isInline?: boolean;
     }) {
         super();
         this.condition = options.condition;
         this.thenBranch = options.thenBranch;
         this.elseBranch = options.elseBranch;
-        this.isInline = options.isInline;
 
         this.tokens = {
             if: options.if,
@@ -619,12 +621,23 @@ export class IfStatement extends Statement {
     public readonly condition: Expression;
     public readonly thenBranch: Block;
     public readonly elseBranch?: IfStatement | Block;
-    public readonly isInline?: boolean;
 
     public readonly kind = AstNodeKind.IfStatement;
 
     public readonly range: Range | undefined;
 
+    get isInline() {
+        const allLeadingTrivia = [
+            ...this.thenBranch.getLeadingTrivia(),
+            ...this.thenBranch.statements.map(s => s.getLeadingTrivia()).flat(),
+            ...(this.tokens.else?.leadingTrivia ?? []),
+            ...(this.tokens.endIf?.leadingTrivia ?? [])
+        ];
+
+        const hasNewline = allLeadingTrivia.find(t => t.kind === TokenKind.Newline);
+        return !hasNewline;
+    }
+
     transpile(state: BrsTranspileState) {
         let results = [] as TranspileResult;
         //if   (already indented by block)
@@ -971,7 +984,7 @@ export class LabelStatement extends Statement {
     public readonly range: Range | undefined;
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.name.leadingTrivia);
+        return this.tokens.name.leadingTrivia;
     }
 
     transpile(state: BrsTranspileState) {
@@ -1647,11 +1660,11 @@ export class NamespaceStatement extends Statement implements TypedefProvider {
     }
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.namespace?.leadingTrivia);
+        return this.tokens.namespace?.leadingTrivia;
     }
 
     public getEndTrivia(): Token[] {
-        return this.tokens.endNamespace?.leadingTrivia ?? [];
+        return this.tokens.endNamespace?.leadingTrivia;
     }
 
     public getNameParts() {
@@ -1842,12 +1855,11 @@ export class InterfaceStatement extends Statement implements TypedefProvider {
     }
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this,
-            this.tokens.interface?.leadingTrivia);
+        return this.tokens.interface?.leadingTrivia;
     }
 
     public getEndTrivia(): Token[] {
-        return this.tokens.endInterface?.leadingTrivia ?? [];
+        return this.tokens.endInterface?.leadingTrivia;
     }
 
 
@@ -2028,7 +2040,7 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide
     };
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia);
+        return this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia;
     }
 
     public get name() {
@@ -2153,7 +2165,7 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid
     }
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.optional?.leadingTrivia ?? this.tokens.functionType.leadingTrivia);
+        return this.tokens.optional?.leadingTrivia ?? this.tokens.functionType.leadingTrivia;
     }
 
     walk(visitor: WalkVisitor, options: WalkOptions) {
@@ -2315,7 +2327,7 @@ export class ClassStatement extends Statement implements TypedefProvider {
     }
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.class?.leadingTrivia);
+        return this.tokens.class?.leadingTrivia;
     }
 
     public getEndTrivia(): Token[] {
@@ -2753,7 +2765,7 @@ export class MethodStatement extends FunctionStatement {
     }
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.func.getLeadingTrivia());
+        return this.func.getLeadingTrivia();
     }
 
     transpile(state: BrsTranspileState) {
@@ -2968,7 +2980,7 @@ export class FieldStatement extends Statement implements TypedefProvider {
     public readonly range: Range | undefined;
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.accessModifier?.leadingTrivia ?? this.tokens.optional?.leadingTrivia ?? this.tokens.name?.leadingTrivia ?? []);
+        return this.tokens.accessModifier?.leadingTrivia ?? this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia;
     }
 
     public get isOptional() {
@@ -3242,7 +3254,7 @@ export class EnumStatement extends Statement implements TypedefProvider {
     }
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.enum?.leadingTrivia);
+        return this.tokens.enum?.leadingTrivia;
     }
 
     public getEndTrivia(): Token[] {
@@ -3423,7 +3435,7 @@ export class EnumMemberStatement extends Statement implements TypedefProvider {
     }
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.name.leadingTrivia);
+        return this.tokens.name.leadingTrivia;
     }
 
     public transpile(state: BrsTranspileState): TranspileResult {
@@ -3499,7 +3511,7 @@ export class ConstStatement extends Statement implements TypedefProvider {
     }
 
     public getLeadingTrivia(): Token[] {
-        return util.concatAnnotationLeadingTrivia(this, this.tokens.const?.leadingTrivia);
+        return this.tokens.const?.leadingTrivia;
     }
 
     /**
diff --git a/src/util.spec.ts b/src/util.spec.ts
index f59e6ce5..f714934c 100644
--- a/src/util.spec.ts
+++ b/src/util.spec.ts
@@ -6,7 +6,7 @@ import type { BsConfig } from './BsConfig';
 import * as fsExtra from 'fs-extra';
 import { createSandbox } from 'sinon';
 import { DiagnosticMessages } from './DiagnosticMessages';
-import { tempDir, rootDir, expectTypeToBe, expectRangeToBe } from './testHelpers.spec';
+import { tempDir, rootDir, expectTypeToBe } from './testHelpers.spec';
 import { Program } from './Program';
 import type { BsDiagnostic } from './interfaces';
 import { TypeChainEntry } from './interfaces';
@@ -656,9 +656,9 @@ describe('util', () => {
             const pos11 = { line: 1, character: 1 };
             const pos99 = { line: 9, character: 9 };
 
-            expectRangeToBe(util.createRangeFromPositions(pos11, pos99), util.createRange(1, 1, 9, 9));
-            expectRangeToBe(util.createRangeFromPositions(null, pos99), util.createRange(9, 9, 9, 9));
-            expectRangeToBe(util.createRangeFromPositions(pos11, null), util.createRange(1, 1, 1, 1));
+            expect(util.createRangeFromPositions(pos11, pos99)).to.eql(util.createRange(1, 1, 9, 9));
+            expect(util.createRangeFromPositions(null, pos99)).to.eql(util.createRange(9, 9, 9, 9));
+            expect(util.createRangeFromPositions(pos11, null)).to.eql(util.createRange(1, 1, 1, 1));
         });
 
 
@@ -667,10 +667,10 @@ describe('util', () => {
             const range2 = util.createRange(2, 2, 3, 3);
             const range3 = util.createRange(100, 100, 100, 100);
 
-            expectRangeToBe(util.createBoundingRange(range1), util.createRange(1, 1, 2, 2));
-            expectRangeToBe(util.createBoundingRange(range1, range2), util.createRange(1, 1, 3, 3));
-            expectRangeToBe(util.createBoundingRange(range2, range1), util.createRange(1, 1, 3, 3));
-            expectRangeToBe(util.createBoundingRange(range2, range3, range1), util.createRange(1, 1, 100, 100));
+            expect(util.createBoundingRange(range1)).to.eql(util.createRange(1, 1, 2, 2));
+            expect(util.createBoundingRange(range1, range2)).to.eql(util.createRange(1, 1, 3, 3));
+            expect(util.createBoundingRange(range2, range1)).to.eql(util.createRange(1, 1, 3, 3));
+            expect(util.createBoundingRange(range2, range3, range1)).to.eql(util.createRange(1, 1, 100, 100));
         });
     });
 
@@ -1133,7 +1133,7 @@ describe('util', () => {
                 end function
             `);
             const func = (statements[0] as FunctionStatement).func;
-            const docs = util.getTokenDocumentation(func);
+            const docs = util.getNodeDocumentation(func);
             expect(docs).to.eql('This is a comment.\nit has two lines');
         });
 
@@ -1149,7 +1149,7 @@ describe('util', () => {
                 end function
             `);
             const func = (statements[0] as FunctionStatement).func;
-            const docs = util.getTokenDocumentation(func);
+            const docs = util.getNodeDocumentation(func);
             expect(docs).to.eql('Add 1 to a number\n\n\n_@public_\n\n_@param_ {integer} the number to add to\n\n_@return_ {integer} the result');
         });
 
diff --git a/src/util.ts b/src/util.ts
index f4497d34..608a6959 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -26,7 +26,7 @@ import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionP
 import { LogLevel, createLogger } from './logging';
 import { isToken, type Identifier, type Locatable, type Token } from './lexer/Token';
 import { TokenKind } from './lexer/TokenKind';
-import { isAnyReferenceType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isDottedGetExpression, isDoubleType, isDynamicType, isEnumMemberType, isExpression, isFloatType, isIndexedGetExpression, isInvalidType, isLiteralString, isLongIntegerType, isNamespaceType, isNewExpression, isNumberType, isStringType, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUnionType, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
+import { isAnyReferenceType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isDottedGetExpression, isDoubleType, isDynamicType, isEnumMemberType, isExpression, isFloatType, isIndexedGetExpression, isInvalidType, isLiteralString, isLongIntegerType, isNamespaceType, isNewExpression, isNumberType, isStatement, isStringType, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUnionType, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
 import { WalkMode } from './astUtils/visitors';
 import { SourceNode } from 'source-map';
 import * as requireRelative from 'require-relative';
@@ -631,10 +631,16 @@ export class Util {
     }
 
     /**
-     * Combine all the documentation found before a token (i.e. comment tokens)
+     * Combine all the documentation for a node - uses the AstNode's leadingTrivia property
      */
-    public getTokenDocumentation(token?: Token | AstNode) {
-        const leadingTrivia = isToken(token) ? token.leadingTrivia : token?.getLeadingTrivia() ?? [];
+    public getNodeDocumentation(node: AstNode) {
+        if (!node) {
+            return '';
+        }
+        const nodeTrivia = node?.getLeadingTrivia() ?? [];
+        const leadingTrivia = isStatement(node)
+            ? [...(node.annotations?.map(anno => anno.getLeadingTrivia() ?? []).flat() ?? []), ...nodeTrivia]
+            : nodeTrivia;
         const tokens = leadingTrivia?.filter(t => t.kind === TokenKind.Newline || t.kind === TokenKind.Comment);
         const comments = [] as Token[];
 
@@ -689,16 +695,7 @@ export class Util {
                     return line;
                 }).join('\n');
         }
-    }
-
-    /**
-     * Combine all the documentation for a node - uses the AstNode's leadingTrivia property
-     */
-    public getNodeDocumentation(node: AstNode) {
-        if (!node) {
-            return;
-        }
-        return this.getTokenDocumentation(node);
+        return '';
     }
 
     /**
@@ -1651,8 +1648,8 @@ export class Util {
     }
 
 
-    public concatAnnotationLeadingTrivia(stmt: Statement, otherTrivia: Token[] = []): Token[] {
-        return [...(stmt.annotations?.map(anno => anno.getLeadingTrivia()).flat() ?? []), ...otherTrivia];
+    public concatAnnotationLeadingTrivia(stmt: Statement): Token[] {
+        return [...(stmt.annotations?.map(anno => anno.getLeadingTrivia() ?? []).flat() ?? []), ...stmt.getLeadingTrivia()];
     }
 
     /**


Test patch:
diff --git a/src/parser/tests/controlFlow/If.spec.ts b/src/parser/tests/controlFlow/If.spec.ts
index 73ceee30..bc6815d2 100644
--- a/src/parser/tests/controlFlow/If.spec.ts
+++ b/src/parser/tests/controlFlow/If.spec.ts
@@ -6,6 +6,8 @@ import { TokenKind } from '../../../lexer/TokenKind';
 import { EOF, identifier, rangeMatch, token } from '../Parser.spec';
 import { isBlock, isFunctionStatement, isIfStatement } from '../../../astUtils/reflection';
 import type { Block, FunctionStatement, IfStatement } from '../../Statement';
+import { expectDiagnosticsIncludes, expectZeroDiagnostics } from '../../../testHelpers.spec';
+import { DiagnosticMessages } from '../../../DiagnosticMessages';
 
 describe('parser if statements', () => {
     it('allows empty if blocks', () => {
@@ -233,9 +235,148 @@ describe('parser if statements', () => {
                 if true then return else print 1
             `);
 
-            expect(diagnostics).to.be.lengthOf(0);
+            expectZeroDiagnostics(diagnostics);
             expect(statements).to.be.length.greaterThan(0);
         });
+
+        it('colon used between inline else statements', () => {
+            const { statements, diagnostics } = Parser.parse(`
+                if x print 1 else print 2 : print 3
+            `);
+
+            expectZeroDiagnostics(diagnostics);
+            expect(statements).to.be.lengthOf(1);
+            const ifStatement = statements[0] as IfStatement;
+            expect(ifStatement.thenBranch.statements).to.be.lengthOf(1);
+            expect((ifStatement.elseBranch as Block).statements).to.be.lengthOf(2);
+        });
+
+        it('colon used between inline else if statements', () => {
+            const { statements, diagnostics } = Parser.parse(`
+                if x print 1 else if y print 2 : print 3
+            `);
+
+            expectZeroDiagnostics(diagnostics);
+            expect(statements).to.be.lengthOf(1);
+            const ifStatement = statements[0] as IfStatement;
+            expect(ifStatement.thenBranch.statements).to.be.lengthOf(1);
+            expect((ifStatement.elseBranch as IfStatement).thenBranch.statements).to.be.lengthOf(2);
+        });
+
+        it('colon used between all kinds of statements', () => {
+            const { statements, diagnostics } = Parser.parse(`
+                if x print 1 : print 1 else if y print 2 : print 2 : print 2 else print 3 : print 3: print 3: print 3
+            `);
+
+            expectZeroDiagnostics(diagnostics);
+            expect(statements).to.be.lengthOf(1);
+            const ifStatement = statements[0] as IfStatement;
+            expect(ifStatement.thenBranch.statements).to.be.lengthOf(2);
+            const elseIf = ifStatement.elseBranch as IfStatement;
+            expect(elseIf.thenBranch.statements).to.be.lengthOf(3);
+            expect((elseIf.elseBranch as Block).statements).to.be.lengthOf(4);
+        });
+
+        it('has diagnostic with extra else', () => {
+            const { diagnostics } = Parser.parse(`
+                if x=1 print 1 else print 2 else print 3
+            `);
+
+            expectDiagnosticsIncludes(diagnostics, [
+                DiagnosticMessages.expectedNewlineOrColon().message
+            ]);
+        });
+
+        it('nested inline if statements', () => {
+            const { statements, diagnostics } = Parser.parse(`
+                if x=1 print 1 else if x=2 print 2 : if y=1 print "y is 1" else print "y is not 1" else print 3: print 3: print 3
+            `);
+
+            expectZeroDiagnostics(diagnostics);
+            expect(statements).to.be.lengthOf(1);
+            const ifStatement = statements[0] as IfStatement;
+            expect(ifStatement.thenBranch.statements).to.be.lengthOf(1);
+            const elseIf = ifStatement.elseBranch as IfStatement;
+            expect(elseIf.thenBranch.statements).to.be.lengthOf(2);
+            expect(isIfStatement(elseIf.thenBranch.statements[1])).to.be.true;
+            const nestedInlineIf = elseIf.thenBranch.statements[1] as IfStatement;
+            expect(nestedInlineIf.thenBranch.statements).to.be.lengthOf(1);
+            expect((nestedInlineIf.elseBranch as Block).statements).to.be.lengthOf(1);
+            expect((elseIf.elseBranch as Block).statements).to.be.lengthOf(3);
+        });
+
+        describe('expected inline if', () => {
+
+            it('non-inline statement used in inline if', () => {
+                const { statements, diagnostics } = Parser.parse(`
+                    if true print 1 else
+                        print 1
+                    end if
+                `);
+                expectDiagnosticsIncludes(diagnostics, [
+                    DiagnosticMessages.expectedInlineIfStatement().message
+                ]);
+                expect(statements.length).to.be.greaterThan(0);
+            });
+
+            it('non inline statement used in inline else if', () => {
+                const { statements, diagnostics } = Parser.parse(`
+                    if x print 1 else if y
+                        print 2
+                    end if
+                `);
+                expectDiagnosticsIncludes(diagnostics, [
+                    DiagnosticMessages.expectedInlineIfStatement().message
+                ]);
+                expect(statements.length).to.be.greaterThan(0);
+            });
+
+            it('colon used after inline else', () => {
+                const { statements, diagnostics } = Parser.parse(`
+                    if x print 1 else : print 2 : print 3: end if
+                `);
+                expectDiagnosticsIncludes(diagnostics, [
+                    DiagnosticMessages.expectedInlineIfStatement().message
+                ]);
+                expect(statements.length).to.be.greaterThan(0);
+            });
+
+            it('new line used in inline else', () => {
+                const { statements, diagnostics } = Parser.parse(`
+                    if x print 1 else
+                        print 2
+                        print 3
+                    end if
+                `);
+                expectDiagnosticsIncludes(diagnostics, [
+                    DiagnosticMessages.expectedInlineIfStatement().message
+                ]);
+                expect(statements.length).to.be.greaterThan(0);
+            });
+
+
+            it('colon used after inline else if', () => {
+                const { diagnostics } = Parser.parse(`
+                    if x print 1 else if y : print 2
+                `);
+                expectDiagnosticsIncludes(diagnostics, [
+                    DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock().message
+                ]);
+            });
+
+            it('new line used in inline else if', () => {
+                const { statements, diagnostics } = Parser.parse(`
+                    if x print 1 else if y
+                        print 2
+                    end if
+                `);
+                expectDiagnosticsIncludes(diagnostics, [
+                    DiagnosticMessages.expectedInlineIfStatement().message
+                ]);
+                expect(statements.length).to.be.greaterThan(0);
+            });
+
+        });
     });
 
     describe('block if', () => {
diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts
index b90244f8..fdebd84a 100644
--- a/src/testHelpers.spec.ts
+++ b/src/testHelpers.spec.ts
@@ -383,16 +383,6 @@ export function expectTypeToBe(someType: BscType, expectedTypeClass: any) {
     expect(someType?.constructor?.name).to.eq(expectedTypeClass.name);
 }
 
-/**
- * Test that a range is waht was expected
- */
-export function expectRangeToBe(actual: Range, expected: Range) {
-    expect(actual.start.line, 'start line').to.eq(expected.start.line);
-    expect(actual.start.character, 'start character').to.eq(expected.start.character);
-    expect(actual.end.line, 'end line').to.eq(expected.end.line);
-    expect(actual.end.character, 'end character').to.eq(expected.end.character);
-}
-
 export function stripConsoleColors(inputString) {
     // Regular expression to match ANSI escape codes for colors
     // eslint-disable-next-line no-control-regex