diff --git a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts index d61657ad4..7b9c6f269 100644 --- a/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts +++ b/src/bscPlugin/transpile/BrsFilePreTranspileProcessor.ts @@ -209,6 +209,81 @@ export class BrsFilePreTranspileProcessor { } + /** + * Recursively resolve a const or enum value until we get to the final resolved expression + * Returns an object with the resolved value and a flag indicating if a circular reference was detected + */ + private resolveConstValue(value: Expression, scope: Scope | undefined, containingNamespace: string | undefined, visited = new Set()): { value: Expression; isCircular: boolean } { + // If it's already a literal, return it as-is + if (isLiteralExpression(value)) { + return { value: value, isCircular: false }; + } + + // If it's a variable expression, try to resolve it as a const or enum + if (isVariableExpression(value)) { + const entityName = value.name.text.toLowerCase(); + + // Prevent infinite recursion by tracking visited constants + if (visited.has(entityName)) { + return { value: value, isCircular: true }; // Return the original value to avoid infinite loop + } + visited.add(entityName); + + // Try to resolve as const first + const constStatement = scope?.getConstFileLink(entityName, containingNamespace)?.item; + if (constStatement) { + // Recursively resolve the const value + return this.resolveConstValue(constStatement.value, scope, containingNamespace, visited); + } + + // Try to resolve as enum member + const enumInfo = this.getEnumInfo(entityName, containingNamespace, scope); + if (enumInfo?.value) { + // Enum values are already resolved to literals by getEnumInfo + return { value: enumInfo.value, isCircular: false }; + } + } + + // If it's a dotted get expression (e.g., namespace.const or namespace.enum.member), try to resolve it + if (isDottedGetExpression(value)) { + const parts = util.splitExpression(value); + const processedNames: string[] = []; + + for (let part of parts) { + if (isVariableExpression(part) || isDottedGetExpression(part)) { + processedNames.push(part?.name?.text?.toLowerCase()); + } else { + return { value: value, isCircular: false }; // Can't resolve further + } + } + + const entityName = processedNames.join('.'); + + // Prevent infinite recursion + if (visited.has(entityName)) { + return { value: value, isCircular: true }; + } + visited.add(entityName); + + // Try to resolve as const first + const constStatement = scope?.getConstFileLink(entityName, containingNamespace)?.item; + if (constStatement) { + // Recursively resolve the const value + return this.resolveConstValue(constStatement.value, scope, containingNamespace, visited); + } + + // Try to resolve as enum member + const enumInfo = this.getEnumInfo(entityName, containingNamespace, scope); + if (enumInfo?.value) { + // Enum values are already resolved to literals by getEnumInfo + return { value: enumInfo.value, isCircular: false }; + } + } + + // Return the value as-is if we can't resolve it further + return { value: value, isCircular: false }; + } + private processExpression(ternaryExpression: Expression, scope: Scope | undefined) { let containingNamespace = this.event.file.getNamespaceStatementForPosition(ternaryExpression.range.start)?.getName(ParseMode.BrighterScript); @@ -225,11 +300,15 @@ export class BrsFilePreTranspileProcessor { } let value: Expression; + let isCircular = false; //did we find a const? transpile the value let constStatement = scope?.getConstFileLink(entityName, containingNamespace)?.item; if (constStatement) { - value = constStatement.value; + // Recursively resolve the const value to its final form + const resolved = this.resolveConstValue(constStatement.value, scope, containingNamespace); + value = resolved.value; + isCircular = resolved.isCircular; } else { //did we find an enum member? transpile that let enumInfo = this.getEnumInfo(entityName, containingNamespace, scope); @@ -238,7 +317,7 @@ export class BrsFilePreTranspileProcessor { } } - if (value) { + if (value && !isCircular) { //override the transpile for this item. this.event.editor.setProperty(part, 'transpile', (state) => { if (isLiteralExpression(value)) { diff --git a/src/parser/tests/statement/ConstStatement.spec.ts b/src/parser/tests/statement/ConstStatement.spec.ts index 6bbc61f6b..85c457282 100644 --- a/src/parser/tests/statement/ConstStatement.spec.ts +++ b/src/parser/tests/statement/ConstStatement.spec.ts @@ -203,6 +203,255 @@ describe('ConstStatement', () => { end sub `); }); + + it('transpiles nested consts that reference other consts within same namespace', () => { + testTranspile(` + namespace theming + const FLAG_A = "A" + const FLAG_B = "B" + const AD_BREAK_START = { a: FLAG_A, b: FLAG_B } + end namespace + sub main() + print theming.AD_BREAK_START + end sub + `, ` + sub main() + print ({ + a: "A" + b: "B" + }) + end sub + `); + }); + + it('transpiles nested consts that reference other consts in different namespaces', () => { + testTranspile(` + namespace aa.bb + const FLAG_A = "A" + end namespace + namespace main + const FLAG_B = "B" + const AD_BREAK_START = { a: aa.bb.FLAG_A, b: FLAG_B } + end namespace + sub main() + print main.AD_BREAK_START + end sub + `, ` + sub main() + print ({ + a: "A" + b: "B" + }) + end sub + `); + }); + + it('transpiles nested consts that reference other consts across files', () => { + program.setFile('source/constants.bs', ` + namespace theming + const PRIMARY_COLOR = "blue" + end namespace + const FLAG_B = "B" + `); + testTranspile(` + const SECONDARY_COLOR = theming.PRIMARY_COLOR + const AD_BREAK_START = { a: SECONDARY_COLOR, b: FLAG_B } + sub main() + print AD_BREAK_START + end sub + `, ` + sub main() + print ({ + a: "blue" + b: "B" + }) + end sub + `); + }); + + it('recursively resolves nested consts that reference other consts', () => { + testTranspile(` + const FLAG_A = "A" + const FLAG_B = FLAG_A + const AD_BREAK_START = { a: FLAG_A, b: FLAG_B } + sub main() + print AD_BREAK_START + end sub + `, ` + sub main() + print ({ + a: "A" + b: "A" + }) + end sub + `); + }); + + it('handles the exact example from the issue - nested consts with namespace references', () => { + testTranspile(` + namespace aa.bb + const FLAG_A = "test" + end namespace + const FLAG_B = "another" + const AD_BREAK_START = { a: aa.bb.FLAG_A, b: FLAG_B } + sub main() + print AD_BREAK_START + end sub + `, ` + sub main() + print ({ + a: "test" + b: "another" + }) + end sub + `); + }); + + it('handles cyclical const references without infinite loop', () => { + testTranspile(` + const A = B + const B = C + const C = A + sub main() + print A + end sub + `, ` + sub main() + print A + end sub + `); + }); + + it('resolves consts inside array literals', () => { + testTranspile(` + const FLAG_A = "A" + const FLAG_B = "B" + const MY_ARRAY = [FLAG_A, FLAG_B, "C"] + sub main() + print MY_ARRAY + end sub + `, ` + sub main() + print ([ + "A" + "B" + "C" + ]) + end sub + `); + }); + + it('resolves enum used in const - same file', () => { + testTranspile(` + namespace Theming + enum Color + RED = "#FF0000" + BLUE = "#0000FF" + end enum + const PRIMARY_COLOR = Theming.Color.BLUE + end namespace + sub main() + a = Theming.PRIMARY_COLOR + end sub + `, ` + sub main() + a = "#0000FF" + end sub + `); + }); + + it('resolves enum used in const - cross file', () => { + program.setFile('source/theming.bs', ` + namespace Theming + enum Color + BLACK = "#000000" + BLUE = "#0000FF" + end enum + end namespace + `); + testTranspile(` + namespace Theming + const PRIMARY_COLOR = Theming.Color.BLUE + end namespace + sub main() + a = Theming.PRIMARY_COLOR + end sub + `, ` + sub main() + a = "#0000FF" + end sub + `); + }); + + it('resolves const -> enum -> const -> enum chain across files', () => { + program.setFile('source/theming1.bs', ` + namespace Theming + const BACKGROUND_COLOR = Theming.Color.BLACK + end namespace + `); + program.setFile('source/theming2.bs', ` + namespace Theming + enum Color + BLACK = "#000000" + WHITE = "#FFFFFF" + end enum + end namespace + `); + program.setFile('source/theming3.bs', ` + namespace Theming + const OVERLAY_COLOR = Theming.BACKGROUND_COLOR + end namespace + `); + testTranspile(` + sub test() + aa = { + backgroundOverlay: { + color: Theming.OVERLAY_COLOR + } + } + end sub + `, ` + sub test() + aa = { + backgroundOverlay: { + color: "#000000" + } + } + end sub + `); + }); + + it('resolves complex multi-file const-enum chain', () => { + program.setFile('source/colors.bs', ` + namespace Theme + enum Color + PRIMARY = "#0000FF" + SECONDARY = "#00FF00" + end enum + end namespace + `); + program.setFile('source/constants.bs', ` + namespace Theme + const MAIN_COLOR = Theme.Color.PRIMARY + const ALT_COLOR = Theme.MAIN_COLOR + end namespace + `); + testTranspile(` + sub main() + colors = { + main: Theme.ALT_COLOR + secondary: Theme.Color.SECONDARY + } + end sub + `, ` + sub main() + colors = { + main: "#0000FF" + secondary: "#00FF00" + } + end sub + `); + }); }); describe('completions', () => {