From d36f7fbedf9f6d42634551e03bca5f7acd4b7f32 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 13 Jun 2025 16:47:09 -0400 Subject: [PATCH 1/5] Adds ability to find the `expressionChainRoot` --- src/astUtils/reflection.ts | 5 +- src/parser/AstNode.spec.ts | 216 ++++++++++++++++++++++++++++++++++++- src/parser/AstNode.ts | 52 +++++++++ src/parser/Expression.ts | 135 ++++++++++++++++++++++- 4 files changed, 401 insertions(+), 7 deletions(-) diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 8496c1e8f..0f8d9d4d2 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -1,5 +1,5 @@ import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassFieldStatement, ClassMethodStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, ThrowStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement, DimStatement, TypecastStatement, AliasStatement } from '../parser/Statement'; -import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, NamespacedVariableNameExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TypeCastExpression, TernaryExpression, NullCoalescingExpression } from '../parser/Expression'; +import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, NamespacedVariableNameExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TypeCastExpression, TernaryExpression, NullCoalescingExpression, RegexLiteralExpression } from '../parser/Expression'; import type { BrsFile } from '../files/BrsFile'; import type { XmlFile } from '../files/XmlFile'; import type { BscFile, File, TypedefProvider } from '../interfaces'; @@ -97,6 +97,9 @@ export function isTernaryExpression(element: AstNode | undefined): element is Te export function isNullCoalescingExpression(element: AstNode | undefined): element is NullCoalescingExpression { return element?.constructor?.name === 'NullCoalescingExpression'; } +export function isRegexLiteralExpression(element: AstNode | undefined): element is RegexLiteralExpression { + return element?.constructor?.name === 'RegexLiteralExpression '; +} export function isEndStatement(element: AstNode | undefined): element is EndStatement { return element?.constructor?.name === 'EndStatement'; } diff --git a/src/parser/AstNode.spec.ts b/src/parser/AstNode.spec.ts index 9b1ecb13a..8617fced2 100644 --- a/src/parser/AstNode.spec.ts +++ b/src/parser/AstNode.spec.ts @@ -1,16 +1,18 @@ import { util } from '../util'; import * as fsExtra from 'fs-extra'; import { Program } from '../Program'; -import type { BrsFile } from '../files/BrsFile'; +import { BrsFile } from '../files/BrsFile'; import { expect } from '../chai-config.spec'; -import type { AALiteralExpression, AAMemberExpression, ArrayLiteralExpression, BinaryExpression, CallExpression, CallfuncExpression, DottedGetExpression, FunctionExpression, GroupingExpression, IndexedGetExpression, NewExpression, NullCoalescingExpression, TaggedTemplateStringExpression, TemplateStringExpression, TemplateStringQuasiExpression, TernaryExpression, TypeCastExpression, UnaryExpression, XmlAttributeGetExpression } from './Expression'; +import { AAMemberExpression, NamespacedVariableNameExpression, VariableExpression, type AALiteralExpression, type ArrayLiteralExpression, type BinaryExpression, type CallExpression, type CallfuncExpression, type DottedGetExpression, type FunctionExpression, type GroupingExpression, type IndexedGetExpression, type NewExpression, type NullCoalescingExpression, type TaggedTemplateStringExpression, type TemplateStringExpression, type TemplateStringQuasiExpression, type TernaryExpression, type TypeCastExpression, type UnaryExpression, type XmlAttributeGetExpression } from './Expression'; import { expectZeroDiagnostics } from '../testHelpers.spec'; import { tempDir, rootDir, stagingDir } from '../testHelpers.spec'; -import { isAALiteralExpression, isAAMemberExpression, isAnnotationExpression, isArrayLiteralExpression, isAssignmentStatement, isBinaryExpression, isBlock, isCallExpression, isCallfuncExpression, isCatchStatement, isClassStatement, isCommentStatement, isConstStatement, isDimStatement, isDottedGetExpression, isDottedSetStatement, isEnumMemberStatement, isEnumStatement, isExpressionStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isGroupingExpression, isIfStatement, isIncrementStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInterfaceStatement, isLibraryStatement, isMethodStatement, isNamespaceStatement, isNewExpression, isNullCoalescingExpression, isPrintStatement, isReturnStatement, isTaggedTemplateStringExpression, isTemplateStringExpression, isTemplateStringQuasiExpression, isTernaryExpression, isThrowStatement, isTryCatchStatement, isTypeCastExpression, isUnaryExpression, isWhileStatement, isXmlAttributeGetExpression } from '../astUtils/reflection'; -import type { ClassStatement, FunctionStatement, InterfaceFieldStatement, InterfaceMethodStatement, MethodStatement, InterfaceStatement, CatchStatement, ThrowStatement, EnumStatement, EnumMemberStatement, ConstStatement, Block, CommentStatement, PrintStatement, DimStatement, ForStatement, WhileStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, TryCatchStatement, DottedSetStatement } from './Statement'; +import { isAALiteralExpression, isAAMemberExpression, isAnnotationExpression, isArrayLiteralExpression, isAssignmentStatement, isBinaryExpression, isBlock, isCallExpression, isCallfuncExpression, isCatchStatement, isClassStatement, isCommentStatement, isConstStatement, isDimStatement, isDottedGetExpression, isDottedSetStatement, isEnumMemberStatement, isEnumStatement, isExpressionStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isGroupingExpression, isIfStatement, isIncrementStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInterfaceStatement, isLibraryStatement, isLiteralNumber, isMethodStatement, isNamespacedVariableNameExpression, isNamespaceStatement, isNewExpression, isNullCoalescingExpression, isPrintStatement, isRegexLiteralExpression, isReturnStatement, isSourceLiteralExpression, isTaggedTemplateStringExpression, isTemplateStringExpression, isTemplateStringQuasiExpression, isTernaryExpression, isThrowStatement, isTryCatchStatement, isTypeCastExpression, isUnaryExpression, isVariableExpression, isWhileStatement, isXmlAttributeGetExpression } from '../astUtils/reflection'; +import type { Body, ClassStatement, FunctionStatement, InterfaceFieldStatement, InterfaceMethodStatement, MethodStatement, InterfaceStatement, CatchStatement, ThrowStatement, EnumStatement, EnumMemberStatement, ConstStatement, Block, CommentStatement, PrintStatement, DimStatement, ForStatement, WhileStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, TryCatchStatement, DottedSetStatement } from './Statement'; import { AssignmentStatement, EmptyStatement } from './Statement'; import { ParseMode, Parser } from './Parser'; import type { AstNode } from './AstNode'; +import { BrsTranspileState } from './BrsTranspileState'; +import { standardizePath as s } from '../util'; type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable }; @@ -1670,4 +1672,210 @@ describe('AstNode', () => { testClone(original); }); }); + + describe('Expression chains', () => { + function findNodeByName(ast: Body, name: string) { + return ast.findChild(node => { + if (isVariableExpression(node) && node.name.text === name) { + return true; + } else if (isDottedGetExpression(node) && node.name.text === name) { + return true; + } else if (isSourceLiteralExpression(node) && node.token.text === name) { + return true; + } else if (isXmlAttributeGetExpression(node) && node.name.text === name) { + return true; + } + }); + } + + type FindMatcher = (ast: Body) => AstNode; + function doTest(code: string, startSelector: string | FindMatcher, expectedRootSelector: string | FindMatcher, insertFunctionBody = true) { + const file = new BrsFile(s`${rootDir}/source/main.bs`, 'pkg:/source/main.bs', program); + + file.parse( + !insertFunctionBody ? code : `function test()\n ${code}\nend function` + ); + + expectZeroDiagnostics(file); + + const ast = file.ast; + + const start = typeof startSelector === 'function' ? startSelector(ast) : findNodeByName(ast, startSelector as string); + const expectedRoot = typeof expectedRootSelector === 'function' ? expectedRootSelector(ast) : findNodeByName(ast, expectedRootSelector as string); + + expect(start).not.to.be.undefined; + expect(expectedRoot).not.to.be.undefined; + + const root = start.getExpressionChainRoot(); + + //transpile both items so we can compare the things they actually represent. (helps sanity check) + expect( + root.transpile(new BrsTranspileState(file)).toString() + ).to.eql( + expectedRoot.transpile(new BrsTranspileState(file)).toString() + ); + + //the real test. did we get the exact same instance of the root that we were expecting? + expect(root).to.equal(expectedRoot); + } + + it('finds root of dotted gets', () => { + doTest(`func = alpha.beta.charlie.delta`, 'alpha', 'delta'); + doTest(`func = alpha.beta.charlie.delta`, 'beta', 'delta'); + doTest(`func = alpha.beta.charlie.delta`, 'charlie', 'delta'); + doTest(`func = alpha.beta.charlie.delta`, 'delta', 'delta'); + }); + + it('finds root of dotted get inside indexed get', () => { + doTest(`func = alpha.beta[charlie.delta]`, 'alpha', ast => ast.findChild(isIndexedGetExpression)); + doTest(`func = alpha.beta[charlie.delta]`, 'beta', ast => ast.findChild(isIndexedGetExpression)); + doTest(`func = alpha.beta[charlie.delta]`, 'charlie', 'delta'); + doTest(`func = alpha.beta[charlie.delta]`, 'delta', 'delta'); + }); + + it('finds root of variable expression', () => { + doTest(`func = alpha`, 'alpha', 'alpha'); + }); + + it('finds root of function call', () => { + doTest(`func = alpha()`, 'alpha', ast => ast.findChild(isCallExpression)); + doTest(`func = alpha.beta()`, 'alpha', ast => ast.findChild(isCallExpression)); + }); + + it('finds root of call inside call', () => { + doTest(`func = alpha()`, 'alpha', ast => ast.findChild(isCallExpression)); + doTest(`func = alpha(beta())`, 'beta', ast => { + return ast.findChild(x => isVariableExpression(x) && x.name.text === 'beta').parent; + }); + doTest(`func = alpha(beta())`, 'alpha', ast => { + return ast.findChild(x => isVariableExpression(x) && x.name.text === 'alpha').parent; + }); + }); + + it('finds root of literal number inside call', () => { + doTest(`func = alpha(1)`, ast => ast.findChild(isLiteralNumber), ast => ast.findChild(isLiteralNumber)); + }); + + it('finds wrapped of literal number inside call', () => { + doTest(`func = alpha(1)`, ast => ast.findChild(isLiteralNumber), ast => ast.findChild(isLiteralNumber)); + }); + + it('GroupExpressions cause new expression chains', () => { + doTest(`func = (((one).two).three)`, 'one', 'one'); + doTest(`func = (((one).two).three)`, ast => { + //the grouping expression around `one` + return ast.findChild(x => isVariableExpression(x) && x.name.text === 'one').parent; + }, 'two'); + doTest(`func = (((one).two).three)`, ast => { + //the grouping expression around `(one).two` + return ast.findChild(x => isDottedGetExpression(x) && x.name.text === 'two').parent; + }, 'three'); + doTest(`func = (((one).two).three)`, ast => { + //the grouping expression around `((one).two).three` + return ast.findChild(x => isDottedGetExpression(x) && x.name.text === 'three').parent; + }, ast => { + //the grouping expression around `((one).two).three` + return ast.findChild(x => isDottedGetExpression(x) && x.name.text === 'three').parent; + }); + }); + + it('UnaryExpression are not included in expressionChainRoot', () => { + doTest(`isAlive = [not isDead]`, 'isDead', 'isDead'); + }); + + it('SourceLiteralExpression do not cause issues', () => { + doTest(`isAlive = [LINE_NUM.ToStr()]`, 'LINE_NUM', ast => ast.findChild(isCallExpression)); + }); + + it('SourceLiteralExpression do not cause issues', () => { + doTest(`func = [function(p1 = alpha.beta)\nend function]`, 'alpha', 'beta'); + }); + + it('BinaryExpression knows its a root', () => { + doTest(`func = [1 + "two"]`, ast => ast.findChild(isBinaryExpression), ast => ast.findChild(isBinaryExpression)); + }); + + it('FunctionExpression knows its a root', () => { + doTest(`func = [sub() : end sub]`, ast => ast.findChild(isFunctionExpression), ast => ast.findChild(isFunctionExpression)); + }); + + it('NamespacedVariableExpression knows its a root', () => { + doTest(` + namespace alpha.beta + class charlie extends delta.echo + end class + end namespace + `, ast => ast.findChild(isNamespacedVariableNameExpression), ast => ast.findChild(isNamespacedVariableNameExpression), false); + }); + + it('XmlAttributeGetExpression works', () => { + doTest(`result = [thing@name]`, 'thing', 'name'); + }); + + it('LiteralExpression knows its a root when standalone', () => { + doTest(`result = [1]`, ast => ast.findChild(isLiteralNumber), ast => ast.findChild(isLiteralNumber)); + }); + + //skipped because this is a bug in the parser (it's valid syntax on device) + it.skip('LiteralExpression is not a root when used on LHS of dotted get', () => { + doTest(`result = 1.ToStr()`, ast => ast.findChild(isLiteralNumber), ast => ast.findChild(isCallExpression)); + }); + + it.only('ArrayLiteralExpression knows its a root', () => { + doTest(`result = [1,2,3]`, ast => ast.findChild(isArrayLiteralExpression), ast => ast.findChild(isArrayLiteralExpression)); + }); + + it.only('AALiteralExpression knows its a root', () => { + doTest(`result = [{}]`, ast => ast.findChild(isAALiteralExpression), ast => ast.findChild(isAALiteralExpression)); + }); + + it.only('AAMemberExpression knows its a root', () => { + doTest(`result = [{ one: 1}]`, ast => ast.findChild(isAAMemberExpression), ast => ast.findChild(isAAMemberExpression)); + }); + + it.only('VariableExpression knows its a root', () => { + doTest(`result = [one]`, 'one', 'one'); + }); + + it.only('NewExpression knows its a root', () => { + doTest(`result = [new Movie()]`, ast => ast.findChild(isNewExpression), ast => ast.findChild(isNewExpression)); + }); + + it.only('CallfuncExpression properly passes along', () => { + doTest(`result = [ node@.someCallfunc() ]`, 'node', ast => ast.findChild(isCallfuncExpression)); + }); + + it.only('TemplateStringExpression knows its a root', () => { + doTest('result = [ `some text` ]', ast => ast.findChild(isTemplateStringExpression), ast => ast.findChild(isTemplateStringExpression)); + }); + + it.only('TaggedStringExpression knows its a root', () => { + doTest('result = [ someTag`some text` ]', ast => ast.findChild(isTaggedTemplateStringExpression), ast => ast.findChild(isTaggedTemplateStringExpression)); + }); + + it.only('AnnotationExpression knows its a root', () => { + doTest(` + @SomeAnnotation() + sub SomeFunc() + end sub + `, ast => ast.findChild(x => { + return isFunctionStatement(x); + }).annotations[0], ast => ast.findChild(x => { + return isFunctionStatement(x); + }).annotations[0], false); + }); + + it.only('TernaryExpression knows its a root', () => { + doTest('result = [ true ? 1 : 2 ]', ast => ast.findChild(isTernaryExpression), ast => ast.findChild(isTernaryExpression)); + }); + + it.only('NullCoalescingExpression knows its a root', () => { + doTest('result = [ 1 ?? 2 ]', ast => ast.findChild(isNullCoalescingExpression), ast => ast.findChild(isNullCoalescingExpression)); + }); + + it.only('regex works correctly', () => { + doTest('result = [ /one/ ]', ast => ast.findChild(isRegexLiteralExpression), ast => ast.findChild(isRegexLiteralExpression)); + doTest('result = [ /one/.exec() ]', ast => ast.findChild(isRegexLiteralExpression), ast => ast.findChild(isCallExpression)); + }); + }); }); diff --git a/src/parser/AstNode.ts b/src/parser/AstNode.ts index a4e00f0ba..b59ea304e 100644 --- a/src/parser/AstNode.ts +++ b/src/parser/AstNode.ts @@ -8,6 +8,7 @@ import type { BrsTranspileState } from './BrsTranspileState'; import type { TranspileResult } from '../interfaces'; import type { AnnotationExpression } from './Expression'; import util from '../util'; +import { isExpression, isStatement } from '../astUtils/reflection'; /** * A BrightScript AST node @@ -151,6 +152,52 @@ export abstract class AstNode { return clone; } + + /** + * Get the root of this expression chain. + * For example, `alpha.beta(charlie.delta)`, the roots would be the DottedGetExpression for `delta`, and the `CallExpression for `beta(...)`. + */ + public getExpressionChainRoot(): Expression | undefined { + let node: Expression = this; + + while (node) { + //if the node is a root, return it + if (node.isExpressionChainRoot) { + return node; + //if we have a parent, do another iteration + } else if (isExpression(node.parent)) { + node = node.parent; + } else { + //there's no parent, this node must be the root + return node; + } + } + return undefined; + } + + /** + * Is this node the root of an expression chain? + */ + public get isExpressionChainRoot() { + //if any of these conditions are true, then this node is an expression chain root + if ( + //if there is no parent, + !this.parent || + //our parent is a `Statement` + isStatement(this.parent) || + //is NOT a part of our parents expression chain + this.parent.childIsInExpressionChain?.(this) === false + ) { + return true; + } + return false; + } + + /** + * Is the node a direct child in the expression chain of this node? + * @param child + */ + protected abstract childIsInExpressionChain(child: AstNode): boolean; } export abstract class Statement extends AstNode { @@ -162,6 +209,11 @@ export abstract class Statement extends AstNode { * Annotations for this statement */ public annotations: AnnotationExpression[] | undefined; + + protected childIsInExpressionChain(child: AstNode): boolean { + // statements cannot contribute child nodes to the same expression chain + return false; + } } diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index f38e0829a..ea62d3719 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -62,6 +62,11 @@ export class BinaryExpression extends Expression { ['left', 'right'] ); } + + public childIsInExpressionChain(child: AstNode): boolean { + //we can't be part of expression chains. we're our own chain. + return false; + } } export class CallExpression extends Expression { @@ -144,6 +149,11 @@ export class CallExpression extends Expression { ['callee', 'args'] ); } + + public childIsInExpressionChain(child: AstNode): boolean { + //only the callee can be part of our expression chain + return child === this.callee; + } } export class FunctionExpression extends Expression implements TypedefProvider { @@ -407,6 +417,10 @@ export class FunctionExpression extends Expression implements TypedefProvider { }, { walkMode: WalkMode.visitExpressions }); return clone; } + + protected childIsInExpressionChain(child: AstNode): boolean { + return false; //these can't be part of an expression chain + } } export class FunctionParameterExpression extends Expression { @@ -494,6 +508,11 @@ export class FunctionParameterExpression extends Expression { ['defaultValue'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class NamespacedVariableNameExpression extends Expression { @@ -551,6 +570,11 @@ export class NamespacedVariableNameExpression extends Expression { ) ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class DottedGetExpression extends Expression { @@ -565,7 +589,6 @@ export class DottedGetExpression extends Expression { super(); this.range = util.createBoundingRange(this.obj, this.dot, this.name); } - public readonly range: Range | undefined; transpile(state: BrsTranspileState) { @@ -597,6 +620,12 @@ export class DottedGetExpression extends Expression { ['obj'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + //the `obj` is the only child node that can be part of our expression chain + return this.obj === child; + } + } export class XmlAttributeGetExpression extends Expression { @@ -638,6 +667,11 @@ export class XmlAttributeGetExpression extends Expression { ['obj'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + //the `obj` is the only child node that can be part of our expression chain + return this.obj === child; + } } export class IndexedGetExpression extends Expression { @@ -707,6 +741,10 @@ export class IndexedGetExpression extends Expression { ['obj', 'index', 'additionalIndexes'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + return this.obj === child; + } } export class GroupingExpression extends Expression { @@ -752,6 +790,11 @@ export class GroupingExpression extends Expression { ['expression'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class LiteralExpression extends Expression { @@ -803,6 +846,11 @@ export class LiteralExpression extends Expression { ) ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } /** @@ -835,6 +883,11 @@ export class EscapedCharCodeLiteralExpression extends Expression { ) ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class ArrayLiteralExpression extends Expression { @@ -915,6 +968,11 @@ export class ArrayLiteralExpression extends Expression { ['elements'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class AAMemberExpression extends Expression { @@ -951,6 +1009,10 @@ export class AAMemberExpression extends Expression { ); } + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class AALiteralExpression extends Expression { @@ -1051,6 +1113,11 @@ export class AALiteralExpression extends Expression { ['elements'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class UnaryExpression extends Expression { @@ -1094,6 +1161,11 @@ export class UnaryExpression extends Expression { ['right'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class VariableExpression extends Expression { @@ -1143,6 +1215,11 @@ export class VariableExpression extends Expression { ) ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class SourceLiteralExpression extends Expression { @@ -1256,6 +1333,11 @@ export class SourceLiteralExpression extends Expression { ) ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } /** @@ -1309,6 +1391,11 @@ export class NewExpression extends Expression { ['call'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class CallfuncExpression extends Expression { @@ -1391,6 +1478,11 @@ export class CallfuncExpression extends Expression { ['callee', 'args'] ); } + + public childIsInExpressionChain(child: AstNode): boolean { + //only the callee can be part of our expression chain + return child === this.callee; + } } /** @@ -1440,6 +1532,11 @@ export class TemplateStringQuasiExpression extends Expression { ['expressions'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class TemplateStringExpression extends Expression { @@ -1539,6 +1636,11 @@ export class TemplateStringExpression extends Expression { ['quasis', 'expressions'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class TaggedTemplateStringExpression extends Expression { @@ -1629,6 +1731,11 @@ export class TaggedTemplateStringExpression extends Expression { ['quasis', 'expressions'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class AnnotationExpression extends Expression { @@ -1686,6 +1793,11 @@ export class AnnotationExpression extends Expression { ); return clone; } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class TernaryExpression extends Expression { @@ -1793,6 +1905,11 @@ export class TernaryExpression extends Expression { ['test', 'consequent', 'alternate'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class NullCoalescingExpression extends Expression { @@ -1892,6 +2009,11 @@ export class NullCoalescingExpression extends Expression { ['consequent', 'alternate'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } export class RegexLiteralExpression extends Expression { @@ -1943,8 +2065,12 @@ export class RegexLiteralExpression extends Expression { }) ); } -} + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } +} export class TypeCastExpression extends Expression { constructor( @@ -1981,6 +2107,11 @@ export class TypeCastExpression extends Expression { ['obj'] ); } + + protected childIsInExpressionChain(child: AstNode): boolean { + // this node cannot contribute child nodes to the same expression chain + return false; + } } // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style From 3c1997b72ffe3163d3508d073d51706c3eb79acd Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 13 Jun 2025 18:47:57 -0400 Subject: [PATCH 2/5] Add remaining expression tests --- src/astUtils/reflection.ts | 2 +- src/parser/AstNode.spec.ts | 28 ++++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 0f8d9d4d2..009f6794e 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -98,7 +98,7 @@ export function isNullCoalescingExpression(element: AstNode | undefined): elemen return element?.constructor?.name === 'NullCoalescingExpression'; } export function isRegexLiteralExpression(element: AstNode | undefined): element is RegexLiteralExpression { - return element?.constructor?.name === 'RegexLiteralExpression '; + return element?.constructor?.name === 'RegexLiteralExpression'; } export function isEndStatement(element: AstNode | undefined): element is EndStatement { return element?.constructor?.name === 'EndStatement'; diff --git a/src/parser/AstNode.spec.ts b/src/parser/AstNode.spec.ts index 8617fced2..39a4f7330 100644 --- a/src/parser/AstNode.spec.ts +++ b/src/parser/AstNode.spec.ts @@ -1821,39 +1821,39 @@ describe('AstNode', () => { doTest(`result = 1.ToStr()`, ast => ast.findChild(isLiteralNumber), ast => ast.findChild(isCallExpression)); }); - it.only('ArrayLiteralExpression knows its a root', () => { + it('ArrayLiteralExpression knows its a root', () => { doTest(`result = [1,2,3]`, ast => ast.findChild(isArrayLiteralExpression), ast => ast.findChild(isArrayLiteralExpression)); }); - it.only('AALiteralExpression knows its a root', () => { + it('AALiteralExpression knows its a root', () => { doTest(`result = [{}]`, ast => ast.findChild(isAALiteralExpression), ast => ast.findChild(isAALiteralExpression)); }); - it.only('AAMemberExpression knows its a root', () => { + it('AAMemberExpression knows its a root', () => { doTest(`result = [{ one: 1}]`, ast => ast.findChild(isAAMemberExpression), ast => ast.findChild(isAAMemberExpression)); }); - it.only('VariableExpression knows its a root', () => { + it('VariableExpression knows its a root', () => { doTest(`result = [one]`, 'one', 'one'); }); - it.only('NewExpression knows its a root', () => { + it('NewExpression knows its a root', () => { doTest(`result = [new Movie()]`, ast => ast.findChild(isNewExpression), ast => ast.findChild(isNewExpression)); }); - it.only('CallfuncExpression properly passes along', () => { + it('CallfuncExpression properly passes along', () => { doTest(`result = [ node@.someCallfunc() ]`, 'node', ast => ast.findChild(isCallfuncExpression)); }); - it.only('TemplateStringExpression knows its a root', () => { + it('TemplateStringExpression knows its a root', () => { doTest('result = [ `some text` ]', ast => ast.findChild(isTemplateStringExpression), ast => ast.findChild(isTemplateStringExpression)); }); - it.only('TaggedStringExpression knows its a root', () => { + it('TaggedStringExpression knows its a root', () => { doTest('result = [ someTag`some text` ]', ast => ast.findChild(isTaggedTemplateStringExpression), ast => ast.findChild(isTaggedTemplateStringExpression)); }); - it.only('AnnotationExpression knows its a root', () => { + it('AnnotationExpression knows its a root', () => { doTest(` @SomeAnnotation() sub SomeFunc() @@ -1865,17 +1865,21 @@ describe('AstNode', () => { }).annotations[0], false); }); - it.only('TernaryExpression knows its a root', () => { + it('TernaryExpression knows its a root', () => { doTest('result = [ true ? 1 : 2 ]', ast => ast.findChild(isTernaryExpression), ast => ast.findChild(isTernaryExpression)); }); - it.only('NullCoalescingExpression knows its a root', () => { + it('NullCoalescingExpression knows its a root', () => { doTest('result = [ 1 ?? 2 ]', ast => ast.findChild(isNullCoalescingExpression), ast => ast.findChild(isNullCoalescingExpression)); }); - it.only('regex works correctly', () => { + it('regex works correctly', () => { doTest('result = [ /one/ ]', ast => ast.findChild(isRegexLiteralExpression), ast => ast.findChild(isRegexLiteralExpression)); doTest('result = [ /one/.exec() ]', ast => ast.findChild(isRegexLiteralExpression), ast => ast.findChild(isCallExpression)); }); + + it('NullCoalescingExpression knows its a root', () => { + doTest('result = [1 as string]', ast => ast.findChild(isTypeCastExpression), ast => ast.findChild(isTypeCastExpression)); + }); }); }); From c9ae5498c1c2e4ec74a535f190425b0c5ddec286 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Fri, 13 Jun 2025 18:56:21 -0400 Subject: [PATCH 3/5] Fix lint issues --- src/parser/AstNode.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser/AstNode.spec.ts b/src/parser/AstNode.spec.ts index 39a4f7330..4d50917c9 100644 --- a/src/parser/AstNode.spec.ts +++ b/src/parser/AstNode.spec.ts @@ -3,7 +3,8 @@ import * as fsExtra from 'fs-extra'; import { Program } from '../Program'; import { BrsFile } from '../files/BrsFile'; import { expect } from '../chai-config.spec'; -import { AAMemberExpression, NamespacedVariableNameExpression, VariableExpression, type AALiteralExpression, type ArrayLiteralExpression, type BinaryExpression, type CallExpression, type CallfuncExpression, type DottedGetExpression, type FunctionExpression, type GroupingExpression, type IndexedGetExpression, type NewExpression, type NullCoalescingExpression, type TaggedTemplateStringExpression, type TemplateStringExpression, type TemplateStringQuasiExpression, type TernaryExpression, type TypeCastExpression, type UnaryExpression, type XmlAttributeGetExpression } from './Expression'; +import type { AAMemberExpression } from './Expression'; +import { type AALiteralExpression, type ArrayLiteralExpression, type BinaryExpression, type CallExpression, type CallfuncExpression, type DottedGetExpression, type FunctionExpression, type GroupingExpression, type IndexedGetExpression, type NewExpression, type NullCoalescingExpression, type TaggedTemplateStringExpression, type TemplateStringExpression, type TemplateStringQuasiExpression, type TernaryExpression, type TypeCastExpression, type UnaryExpression, type XmlAttributeGetExpression } from './Expression'; import { expectZeroDiagnostics } from '../testHelpers.spec'; import { tempDir, rootDir, stagingDir } from '../testHelpers.spec'; import { isAALiteralExpression, isAAMemberExpression, isAnnotationExpression, isArrayLiteralExpression, isAssignmentStatement, isBinaryExpression, isBlock, isCallExpression, isCallfuncExpression, isCatchStatement, isClassStatement, isCommentStatement, isConstStatement, isDimStatement, isDottedGetExpression, isDottedSetStatement, isEnumMemberStatement, isEnumStatement, isExpressionStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isGroupingExpression, isIfStatement, isIncrementStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInterfaceStatement, isLibraryStatement, isLiteralNumber, isMethodStatement, isNamespacedVariableNameExpression, isNamespaceStatement, isNewExpression, isNullCoalescingExpression, isPrintStatement, isRegexLiteralExpression, isReturnStatement, isSourceLiteralExpression, isTaggedTemplateStringExpression, isTemplateStringExpression, isTemplateStringQuasiExpression, isTernaryExpression, isThrowStatement, isTryCatchStatement, isTypeCastExpression, isUnaryExpression, isVariableExpression, isWhileStatement, isXmlAttributeGetExpression } from '../astUtils/reflection'; From 8026b599682f925775fed6672b4216cf7c263015 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 13 Aug 2025 08:13:46 -0400 Subject: [PATCH 4/5] Add preview feature deprecation notices --- src/parser/AstNode.ts | 5 ++++- src/parser/Expression.ts | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/parser/AstNode.ts b/src/parser/AstNode.ts index aa029a7d5..456f4cc1d 100644 --- a/src/parser/AstNode.ts +++ b/src/parser/AstNode.ts @@ -163,6 +163,7 @@ export abstract class AstNode { /** * Get the root of this expression chain. * For example, `alpha.beta(charlie.delta)`, the roots would be the DottedGetExpression for `delta`, and the `CallExpression for `beta(...)`. + * @deprecated this is a preview feature and may be removed or changed in the future */ public getExpressionChainRoot(): Expression | undefined { let node: Expression = this; @@ -184,6 +185,7 @@ export abstract class AstNode { /** * Is this node the root of an expression chain? + * @deprecated this is a preview feature and may be removed or changed in the future */ public get isExpressionChainRoot() { //if any of these conditions are true, then this node is an expression chain root @@ -202,7 +204,8 @@ export abstract class AstNode { /** * Is the node a direct child in the expression chain of this node? - * @param child + * @param child the child node to check + * @deprecated this is a preview feature and may be removed or changed in the future */ protected abstract childIsInExpressionChain(child: AstNode): boolean; } diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 02a5d07b2..c4dead2f5 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -63,7 +63,7 @@ export class BinaryExpression extends Expression { ); } - public childIsInExpressionChain(child: AstNode): boolean { + protected childIsInExpressionChain(child: AstNode): boolean { //we can't be part of expression chains. we're our own chain. return false; } @@ -150,7 +150,7 @@ export class CallExpression extends Expression { ); } - public childIsInExpressionChain(child: AstNode): boolean { + protected childIsInExpressionChain(child: AstNode): boolean { //only the callee can be part of our expression chain return child === this.callee; } @@ -1492,7 +1492,7 @@ export class CallfuncExpression extends Expression { ); } - public childIsInExpressionChain(child: AstNode): boolean { + protected childIsInExpressionChain(child: AstNode): boolean { //only the callee can be part of our expression chain return child === this.callee; } From cea083c9e2bba385b84041fccae28a5748ef4225 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 13 Aug 2025 08:56:27 -0400 Subject: [PATCH 5/5] Fix compile error --- src/bscPlugin/CallExpressionInfo.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bscPlugin/CallExpressionInfo.ts b/src/bscPlugin/CallExpressionInfo.ts index b14ab36d6..4a974e6b4 100644 --- a/src/bscPlugin/CallExpressionInfo.ts +++ b/src/bscPlugin/CallExpressionInfo.ts @@ -25,7 +25,7 @@ export class CallExpressionInfo { expression?: Expression; //the contextually relevant callExpression, which relates to it - callExpression?: CallExpression; + callExpression?: CallExpression | CallfuncExpression; type: CallExpressionType; file: BrsFile; @@ -100,9 +100,9 @@ export class CallExpressionInfo { return util.rangeContains(boundingRange, this.position); } - ascertainCallExpression(): CallExpression { + ascertainCallExpression(): CallExpression | CallfuncExpression { let expression = this.expression; - function isCallFuncOrCallExpression(expression: Expression) { + function isCallFuncOrCallExpression(expression: Expression): expression is CallExpression | CallfuncExpression { return isCallfuncExpression(expression) || isCallExpression(expression); }