From 62ed805f001a22e0a185581c55207d67eb7bbd71 Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Sun, 3 May 2026 11:31:58 -0300 Subject: [PATCH 1/3] Diagnose reserved BrightScript builtins used as values Adds a new diagnostic (`reservedBuiltinUsedAsValue`, code 1147) that fires when a reserved call-only BrightScript builtin appears in any non-call position. These builtins compile cleanly today in the brighterscript parser but produce a device compile error (typically `Syntax Error. Builtin function call expected`, hex &h9d, or `Syntax Error` &h02 depending on the builtin). Builtins covered (all device-verified): - box, createobject, getglobalaa - getlastruncompileerror, getlastrunruntimeerror - objfun, pos, run, tab, type The check fires from `primary()` after an identifier match, gated on `peek().kind !== LeftParen` so canonical call sites (e.g. `type(x)`, `Box(1)`, `CreateObject("roSGNode", "Node")`) remain clean. Property access (`m.type = 1`) and AA-literal keys (`{ type: 1 }`) are not affected because they use different parse paths. BrighterScript's `type Name = ...` statement is also unaffected. Adds 38 tests: - per-builtin RHS-read flag + canonical-call no-flag pairs (16) - per-builtin parameter-name 1045 regression tests (10) - ObjFun/type-specific edge cases: passed-as-arg, property access, AA-keys, type-statement, case-insensitivity (12) `Eval` is intentionally excluded from this set. Its compile error (&h90 RSG 1.2 deprecation) is a different concern and merits a separate, more pointed diagnostic. --- src/DiagnosticMessages.ts | 5 + src/lexer/TokenKind.ts | 20 +++ src/parser/Parser.spec.ts | 263 +++++++++++++++++++++++++++++++++++++- src/parser/Parser.ts | 16 ++- 4 files changed, 302 insertions(+), 2 deletions(-) diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index 12357a6a3..9b96bdb9c 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -770,6 +770,11 @@ export let DiagnosticMessages = { message: `'${featureName}' requires Roku firmware version ${minimumVersion} or higher (current target is ${configuredVersion})`, code: 1146, severity: DiagnosticSeverity.Error + }), + reservedBuiltinUsedAsValue: (name: string) => ({ + message: `'${name}' is a reserved builtin and can only be used as a function call (e.g. '${name}(...)')`, + code: 1147, + severity: DiagnosticSeverity.Error }) }; diff --git a/src/lexer/TokenKind.ts b/src/lexer/TokenKind.ts index 21b40a481..626d55428 100644 --- a/src/lexer/TokenKind.ts +++ b/src/lexer/TokenKind.ts @@ -577,6 +577,26 @@ export const DisallowedLocalIdentifiersText = new Set([ ...DisallowedLocalIdentifiers.map(x => x.toLowerCase()) ]); +/** + * Reserved BrightScript builtins that compile only when invoked as a function call. + * Any non-call reference (e.g. `x = type`, `print foo(ObjFun)`) is a device compile error + * (most produce `Syntax Error. Builtin function call expected`, hex code &h9d; + * `createobject` and `tab` produce a generic `Syntax Error` &h02 with the same outcome). + * Names are lowercased for case-insensitive matching. + */ +export const CallOnlyBuiltins = new Set([ + 'box', + 'createobject', + 'getglobalaa', + 'getlastruncompileerror', + 'getlastrunruntimeerror', + 'objfun', + 'pos', + 'run', + 'tab', + 'type' +]); + /** * List of string versions of TokenKind and various globals that are NOT allowed as scope function names. * Used to throw more helpful "you can't use a reserved word as a function name" errors. diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index 917106821..78fa2593a 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -928,6 +928,264 @@ describe('parser', () => { expect(diagnostics, `assigning to reserved word "${reservedWord}" should have been an error`).to.be.length.greaterThan(0); } }); + + describe('call-only builtins (`ObjFun`, `type`)', () => { + it('flags `x = ObjFun` (RHS value read)', () => { + let { diagnostics } = parse(` + sub a() + x = ObjFun + print x + end sub + `); + expectDiagnostics(diagnostics, [ + DiagnosticMessages.reservedBuiltinUsedAsValue('ObjFun') + ]); + }); + + it('flags `print type(ObjFun)` (passed as argument)', () => { + let { diagnostics } = parse(` + sub a() + print type(ObjFun) + end sub + `); + expectDiagnostics(diagnostics, [ + DiagnosticMessages.reservedBuiltinUsedAsValue('ObjFun') + ]); + }); + + it('flags `f(ObjFun, 2)` (passed by value)', () => { + let { diagnostics } = parse(` + sub a() + f(ObjFun, 2) + end sub + `); + expectDiagnostics(diagnostics, [ + DiagnosticMessages.reservedBuiltinUsedAsValue('ObjFun') + ]); + }); + + it('flags `x = type` (RHS value read)', () => { + let { diagnostics } = parse(` + sub a() + x = type + print x + end sub + `); + expectDiagnostics(diagnostics, [ + DiagnosticMessages.reservedBuiltinUsedAsValue('type') + ]); + }); + + it('does not flag `ObjFun()` (call site - falls through to scope validation)', () => { + let { diagnostics } = parse(` + sub a() + ObjFun() + end sub + `); + expectZeroDiagnostics(diagnostics); + }); + + it('does not flag `type(123)` (canonical call)', () => { + let { diagnostics } = parse(` + sub a() + print type(123) + end sub + `); + expectZeroDiagnostics(diagnostics); + }); + + it('does not flag `m.ObjFun = 1` (property assignment)', () => { + let { diagnostics } = parse(` + sub a() + m.ObjFun = 1 + end sub + `); + expectZeroDiagnostics(diagnostics); + }); + + it('does not flag `m.type = 1` (property assignment)', () => { + let { diagnostics } = parse(` + sub a() + m.type = 1 + end sub + `); + expectZeroDiagnostics(diagnostics); + }); + + it('does not flag `{ ObjFun: 1 }` (AA literal key)', () => { + let { diagnostics } = parse(` + sub a() + aa = { ObjFun: 1 } + end sub + `); + expectZeroDiagnostics(diagnostics); + }); + + it('does not flag `{ type: 1 }` (AA literal key)', () => { + let { diagnostics } = parse(` + sub a() + aa = { type: 1 } + end sub + `); + expectZeroDiagnostics(diagnostics); + }); + + it('does not flag a BrighterScript `type Name = ...` statement', () => { + let { diagnostics } = parse(` + type MyAlias = string or integer + `, ParseMode.BrighterScript); + expectZeroDiagnostics(diagnostics); + }); + + it('case-insensitive match for OBJFUN, ObjFun, objfun', () => { + let { diagnostics } = parse(` + sub a() + x = OBJFUN + y = objfun + end sub + `); + expectDiagnostics(diagnostics, [ + DiagnosticMessages.reservedBuiltinUsedAsValue('OBJFUN'), + DiagnosticMessages.reservedBuiltinUsedAsValue('objfun') + ]); + }); + + //per-builtin coverage for each device-verified entry in CallOnlyBuiltins. + //each pair: (1) bare value read flags, (2) canonical call form does not flag. + + it('flags `x = Box` (RHS value read)', () => { + let { diagnostics } = parse(`sub a()\nx = Box\nend sub`); + expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('Box')]); + }); + + it('does not flag `Box(1)` (canonical call)', () => { + let { diagnostics } = parse(`sub a()\nx = Box(1)\nend sub`); + expectZeroDiagnostics(diagnostics); + }); + + it('flags `x = CreateObject` (RHS value read)', () => { + let { diagnostics } = parse(`sub a()\nx = CreateObject\nend sub`); + expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('CreateObject')]); + }); + + it('does not flag `CreateObject("roSGNode", "Node")` (canonical call)', () => { + let { diagnostics } = parse(`sub a()\nx = CreateObject("roSGNode", "Node")\nend sub`); + expectZeroDiagnostics(diagnostics); + }); + + it('flags `x = GetGlobalAA` (RHS value read)', () => { + let { diagnostics } = parse(`sub a()\nx = GetGlobalAA\nend sub`); + expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('GetGlobalAA')]); + }); + + it('does not flag `GetGlobalAA()` (canonical call)', () => { + let { diagnostics } = parse(`sub a()\nx = GetGlobalAA()\nend sub`); + expectZeroDiagnostics(diagnostics); + }); + + it('flags `x = GetLastRunCompileError` (RHS value read)', () => { + let { diagnostics } = parse(`sub a()\nx = GetLastRunCompileError\nend sub`); + expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('GetLastRunCompileError')]); + }); + + it('does not flag `GetLastRunCompileError()` (canonical call)', () => { + let { diagnostics } = parse(`sub a()\nx = GetLastRunCompileError()\nend sub`); + expectZeroDiagnostics(diagnostics); + }); + + it('flags `x = GetLastRunRunTimeError` (RHS value read)', () => { + let { diagnostics } = parse(`sub a()\nx = GetLastRunRunTimeError\nend sub`); + expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('GetLastRunRunTimeError')]); + }); + + it('does not flag `GetLastRunRunTimeError()` (canonical call)', () => { + let { diagnostics } = parse(`sub a()\nx = GetLastRunRunTimeError()\nend sub`); + expectZeroDiagnostics(diagnostics); + }); + + it('flags `x = Pos` (RHS value read)', () => { + let { diagnostics } = parse(`sub a()\nx = Pos\nend sub`); + expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('Pos')]); + }); + + it('does not flag `Pos(0)` (canonical call)', () => { + let { diagnostics } = parse(`sub a()\nx = Pos(0)\nend sub`); + expectZeroDiagnostics(diagnostics); + }); + + it('flags `x = Run` (RHS value read)', () => { + let { diagnostics } = parse(`sub a()\nx = Run\nend sub`); + expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('Run')]); + }); + + it('does not flag `Run("pkg:/source/foo.brs")` (canonical call)', () => { + let { diagnostics } = parse(`sub a()\nx = Run("pkg:/source/foo.brs")\nend sub`); + expectZeroDiagnostics(diagnostics); + }); + + it('flags `x = Tab` (RHS value read)', () => { + let { diagnostics } = parse(`sub a()\nx = Tab\nend sub`); + expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('Tab')]); + }); + + it('does not flag `Tab(5)` (canonical call)', () => { + let { diagnostics } = parse(`sub a()\nx = Tab(5)\nend sub`); + expectZeroDiagnostics(diagnostics); + }); + + //parameter-name coverage. each of these is caught by the pre-existing + //`cannotUseReservedWordAsIdentifier` (code 1045), not the new 1147. + + it('flags `function f(Box)` (parameter name)', () => { + let { diagnostics } = parse(`function f(Box)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('Box')]); + }); + + it('flags `function f(CreateObject)` (parameter name)', () => { + let { diagnostics } = parse(`function f(CreateObject)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('CreateObject')]); + }); + + it('flags `function f(GetGlobalAA)` (parameter name)', () => { + let { diagnostics } = parse(`function f(GetGlobalAA)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('GetGlobalAA')]); + }); + + it('flags `function f(GetLastRunCompileError)` (parameter name)', () => { + let { diagnostics } = parse(`function f(GetLastRunCompileError)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('GetLastRunCompileError')]); + }); + + it('flags `function f(GetLastRunRunTimeError)` (parameter name)', () => { + let { diagnostics } = parse(`function f(GetLastRunRunTimeError)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('GetLastRunRunTimeError')]); + }); + + it('flags `function f(ObjFun)` (parameter name)', () => { + let { diagnostics } = parse(`function f(ObjFun)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('ObjFun')]); + }); + + it('flags `function f(Pos)` (parameter name)', () => { + let { diagnostics } = parse(`function f(Pos)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('Pos')]); + }); + + it('flags `function f(Run)` (parameter name)', () => { + let { diagnostics } = parse(`function f(Run)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('Run')]); + }); + + it('flags `function f(Tab)` (parameter name)', () => { + let { diagnostics } = parse(`function f(Tab)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('Tab')]); + }); + + it('flags `function f(type)` (parameter name)', () => { + let { diagnostics } = parse(`function f(type)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('type')]); + }); + }); }); describe('import keyword', () => { @@ -1611,7 +1869,10 @@ describe('parser', () => { end function `, ParseMode.BrighterScript); expectDiagnostics(diagnostics, [ - DiagnosticMessages.cannotUseReservedWordAsIdentifier('type').message + //type = 1 -> assignment LHS + DiagnosticMessages.cannotUseReservedWordAsIdentifier('type'), + //return type -> non-call value read, on-device compile error + DiagnosticMessages.reservedBuiltinUsedAsValue('type') ]); }); diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 75ba37549..bfae41532 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -7,6 +7,7 @@ import { AllowedProperties, AssignmentOperators, BrighterScriptSourceLiterals, + CallOnlyBuiltins, DeclarableTypes, DisallowedFunctionIdentifiersText, DisallowedLocalIdentifiersText, @@ -3018,7 +3019,20 @@ export class Parser { return this.templateString(true); case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers): - return new VariableExpression(this.previous() as Identifier); + const ident = this.previous() as Identifier; + //flag reserved call-only builtins (e.g. `ObjFun`, `type`) used in non-call position. + //these compile cleanly as values today but are device compile errors + //(`Syntax Error. Builtin function call expected`). + if ( + CallOnlyBuiltins.has(ident.text.toLowerCase()) && + this.peek().kind !== TokenKind.LeftParen + ) { + this.diagnostics.push({ + ...DiagnosticMessages.reservedBuiltinUsedAsValue(ident.text), + range: ident.range + }); + } + return new VariableExpression(ident); case this.match(TokenKind.LeftParen): let left = this.previous(); From d61def3bb27d37e0906e9aed536af5336a7bd489 Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Sun, 3 May 2026 14:20:10 -0300 Subject: [PATCH 2/3] Add eval to CallOnlyBuiltins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eval is a call-only builtin like the others here — referencing it as a value (`x = eval`, etc.) is a device compile error. --- src/lexer/TokenKind.ts | 1 + src/parser/Parser.spec.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/lexer/TokenKind.ts b/src/lexer/TokenKind.ts index 626d55428..e917eb501 100644 --- a/src/lexer/TokenKind.ts +++ b/src/lexer/TokenKind.ts @@ -587,6 +587,7 @@ export const DisallowedLocalIdentifiersText = new Set([ export const CallOnlyBuiltins = new Set([ 'box', 'createobject', + 'eval', 'getglobalaa', 'getlastruncompileerror', 'getlastrunruntimeerror', diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index 78fa2593a..8830e03ed 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -1133,6 +1133,16 @@ describe('parser', () => { expectZeroDiagnostics(diagnostics); }); + it('flags `x = eval` (RHS value read)', () => { + let { diagnostics } = parse(`sub a()\nx = eval\nend sub`); + expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('eval')]); + }); + + it('does not flag `eval("print 1")` (call site - device may still error but the parser shouldn\'t flag the call form here)', () => { + let { diagnostics } = parse(`sub a()\neval("print 1")\nend sub`); + expectZeroDiagnostics(diagnostics); + }); + //parameter-name coverage. each of these is caught by the pre-existing //`cannotUseReservedWordAsIdentifier` (code 1045), not the new 1147. @@ -1185,6 +1195,11 @@ describe('parser', () => { let { diagnostics } = parse(`function f(type)\nend function`); expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('type')]); }); + + it('flags `function f(eval)` (parameter name)', () => { + let { diagnostics } = parse(`function f(eval)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('eval')]); + }); }); }); From 45f5718a0dc6e1a87731b40b83e2800058a88b0c Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Tue, 5 May 2026 16:19:33 -0300 Subject: [PATCH 3/3] Move unreferencable-builtin diagnostic to file validation Rename CallOnlyBuiltins to UnreferencableBuiltins and move the value-read check from the parser into BrsFileValidator so plugin-injected AST nodes are also caught. Co-Authored-By: Claude Opus 4.7 --- .../validation/BrsFileValidator.spec.ts | 254 ++++++++++++++++++ src/bscPlugin/validation/BrsFileValidator.ts | 21 +- src/lexer/TokenKind.ts | 4 +- src/parser/Parser.spec.ts | 223 +-------------- src/parser/Parser.ts | 16 +- 5 files changed, 278 insertions(+), 240 deletions(-) diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts index ceadd3adb..f6b23bb9c 100644 --- a/src/bscPlugin/validation/BrsFileValidator.spec.ts +++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts @@ -608,4 +608,258 @@ describe('BrsFileValidator', () => { }); }); }); + + describe('unreferencable builtins', () => { + const reservedBuiltinCode = DiagnosticMessages.reservedBuiltinUsedAsValue('').code; + + function reservedBuiltinDiagnostics() { + return program.getDiagnostics().filter(diagnostic => diagnostic.code === reservedBuiltinCode); + } + + function expectFlagged(names: string[]) { + expect( + reservedBuiltinDiagnostics().map(diagnostic => diagnostic.message) + ).to.eql( + names.map(name => DiagnosticMessages.reservedBuiltinUsedAsValue(name).message) + ); + } + + function expectNotFlagged() { + expect(reservedBuiltinDiagnostics()).to.eql([]); + } + + it('flags `x = ObjFun` (RHS value read)', () => { + program.setFile('source/main.brs', ` + sub a() + x = ObjFun + print x + end sub + `); + program.validate(); + expectFlagged(['ObjFun']); + }); + + it('flags `print type(ObjFun)` (passed as argument)', () => { + program.setFile('source/main.brs', ` + sub a() + print type(ObjFun) + end sub + `); + program.validate(); + expectFlagged(['ObjFun']); + }); + + it('flags `f(ObjFun, 2)` (passed by value)', () => { + program.setFile('source/main.brs', ` + sub a() + f(ObjFun, 2) + end sub + sub f(arg1, arg2) + end sub + `); + program.validate(); + expectFlagged(['ObjFun']); + }); + + it('flags `x = type` (RHS value read)', () => { + program.setFile('source/main.brs', ` + sub a() + x = type + print x + end sub + `); + program.validate(); + expectFlagged(['type']); + }); + + it('does not flag `ObjFun(m)` (canonical call)', () => { + program.setFile('source/main.brs', ` + sub a() + ObjFun(m, "") + end sub + `); + program.validate(); + expectNotFlagged(); + }); + + it('does not flag `type(123)` (canonical call)', () => { + program.setFile('source/main.brs', ` + sub a() + print type(123) + end sub + `); + program.validate(); + expectNotFlagged(); + }); + + it('does not flag `m.ObjFun = 1` (property assignment)', () => { + program.setFile('source/main.brs', ` + sub a() + m.ObjFun = 1 + end sub + `); + program.validate(); + expectNotFlagged(); + }); + + it('does not flag `m.type = 1` (property assignment)', () => { + program.setFile('source/main.brs', ` + sub a() + m.type = 1 + end sub + `); + program.validate(); + expectNotFlagged(); + }); + + it('does not flag `{ ObjFun: 1 }` (AA literal key)', () => { + program.setFile('source/main.brs', ` + sub a() + aa = { ObjFun: 1 } + end sub + `); + program.validate(); + expectNotFlagged(); + }); + + it('does not flag `{ type: 1 }` (AA literal key)', () => { + program.setFile('source/main.brs', ` + sub a() + aa = { type: 1 } + end sub + `); + program.validate(); + expectNotFlagged(); + }); + + it('does not flag a BrighterScript `type Name = ...` statement', () => { + program.setFile('source/main.bs', ` + type MyAlias = string or integer + `); + program.validate(); + expectNotFlagged(); + }); + + it('case-insensitive match for OBJFUN, ObjFun, objfun', () => { + program.setFile('source/main.brs', ` + sub a() + x = OBJFUN + y = objfun + end sub + `); + program.validate(); + expectFlagged(['OBJFUN', 'objfun']); + }); + + //per-builtin coverage for each device-verified entry in UnreferencableBuiltins. + //each pair: (1) bare value read flags, (2) canonical call form does not flag. + + it('flags `x = Box` (RHS value read)', () => { + program.setFile('source/main.brs', `sub a()\nx = Box\nend sub`); + program.validate(); + expectFlagged(['Box']); + }); + + it('does not flag `Box(1)` (canonical call)', () => { + program.setFile('source/main.brs', `sub a()\nx = Box(1)\nend sub`); + program.validate(); + expectNotFlagged(); + }); + + it('flags `x = CreateObject` (RHS value read)', () => { + program.setFile('source/main.brs', `sub a()\nx = CreateObject\nend sub`); + program.validate(); + expectFlagged(['CreateObject']); + }); + + it('does not flag `CreateObject("roSGNode", "Node")` (canonical call)', () => { + program.setFile('source/main.brs', `sub a()\nx = CreateObject("roSGNode", "Node")\nend sub`); + program.validate(); + expectNotFlagged(); + }); + + it('flags `x = GetGlobalAA` (RHS value read)', () => { + program.setFile('source/main.brs', `sub a()\nx = GetGlobalAA\nend sub`); + program.validate(); + expectFlagged(['GetGlobalAA']); + }); + + it('does not flag `GetGlobalAA()` (canonical call)', () => { + program.setFile('source/main.brs', `sub a()\nx = GetGlobalAA()\nend sub`); + program.validate(); + expectNotFlagged(); + }); + + it('flags `x = GetLastRunCompileError` (RHS value read)', () => { + program.setFile('source/main.brs', `sub a()\nx = GetLastRunCompileError\nend sub`); + program.validate(); + expectFlagged(['GetLastRunCompileError']); + }); + + it('does not flag `GetLastRunCompileError()` (canonical call)', () => { + program.setFile('source/main.brs', `sub a()\nx = GetLastRunCompileError()\nend sub`); + program.validate(); + expectNotFlagged(); + }); + + it('flags `x = GetLastRunRunTimeError` (RHS value read)', () => { + program.setFile('source/main.brs', `sub a()\nx = GetLastRunRunTimeError\nend sub`); + program.validate(); + expectFlagged(['GetLastRunRunTimeError']); + }); + + it('does not flag `GetLastRunRunTimeError()` (canonical call)', () => { + program.setFile('source/main.brs', `sub a()\nx = GetLastRunRunTimeError()\nend sub`); + program.validate(); + expectNotFlagged(); + }); + + it('flags `x = Pos` (RHS value read)', () => { + program.setFile('source/main.brs', `sub a()\nx = Pos\nend sub`); + program.validate(); + expectFlagged(['Pos']); + }); + + it('does not flag `Pos(0)` (canonical call)', () => { + program.setFile('source/main.brs', `sub a()\nx = Pos(0)\nend sub`); + program.validate(); + expectNotFlagged(); + }); + + it('flags `x = Run` (RHS value read)', () => { + program.setFile('source/main.brs', `sub a()\nx = Run\nend sub`); + program.validate(); + expectFlagged(['Run']); + }); + + it('does not flag `Run("pkg:/source/foo.brs")` (canonical call)', () => { + program.setFile('source/main.brs', `sub a()\nx = Run("pkg:/source/foo.brs")\nend sub`); + program.validate(); + expectNotFlagged(); + }); + + it('flags `x = Tab` (RHS value read)', () => { + program.setFile('source/main.brs', `sub a()\nx = Tab\nend sub`); + program.validate(); + expectFlagged(['Tab']); + }); + + it('does not flag `Tab(5)` (canonical call)', () => { + program.setFile('source/main.brs', `sub a()\nx = Tab(5)\nend sub`); + program.validate(); + expectNotFlagged(); + }); + + it('flags `x = eval` (RHS value read)', () => { + program.setFile('source/main.brs', `sub a()\nx = eval\nend sub`); + program.validate(); + expectFlagged(['eval']); + }); + + it('does not flag `eval("print 1")` (canonical call)', () => { + program.setFile('source/main.brs', `sub a()\neval("print 1")\nend sub`); + program.validate(); + expectNotFlagged(); + }); + }); }); diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index 9d99ae8fc..430b41025 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -1,9 +1,9 @@ -import { isAliasStatement, isBody, isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespaceStatement, isTypecastStatement, isTypeStatement, isUnaryExpression, isWhileStatement } from '../../astUtils/reflection'; +import { isAliasStatement, isBody, isCallExpression, isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespaceStatement, isTypecastStatement, isTypeStatement, isUnaryExpression, isWhileStatement } from '../../astUtils/reflection'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import { DiagnosticMessages } from '../../DiagnosticMessages'; import type { BrsFile } from '../../files/BrsFile'; import type { OnFileValidateEvent } from '../../interfaces'; -import { TokenKind } from '../../lexer/TokenKind'; +import { TokenKind, UnreferencableBuiltins } from '../../lexer/TokenKind'; import type { AstNode, Expression, Statement } from '../../parser/AstNode'; import { CallExpression, type FunctionExpression, type LiteralExpression } from '../../parser/Expression'; import { ParseMode } from '../../parser/Parser'; @@ -197,6 +197,23 @@ export class BrsFileValidator { }, ContinueStatement: (node) => { this.validateContinueStatement(node); + }, + VariableExpression: (node) => { + //flag reserved unreferencable builtins (e.g. `ObjFun`, `type`) used in non-call position. + //these compile cleanly as values today but are device compile errors + //(`Syntax Error. Builtin function call expected`). + const name = node.name?.text; + if ( + name && + UnreferencableBuiltins.has(name.toLowerCase()) && + //only valid use is as the callee of a CallExpression + !(isCallExpression(node.parent) && node.parent.callee === node) + ) { + this.event.file.addDiagnostic({ + ...DiagnosticMessages.reservedBuiltinUsedAsValue(name), + range: node.name.range + }); + } } }); diff --git a/src/lexer/TokenKind.ts b/src/lexer/TokenKind.ts index e917eb501..10eb915ca 100644 --- a/src/lexer/TokenKind.ts +++ b/src/lexer/TokenKind.ts @@ -578,13 +578,13 @@ export const DisallowedLocalIdentifiersText = new Set([ ]); /** - * Reserved BrightScript builtins that compile only when invoked as a function call. + * Reserved BrightScript builtins that cannot be referenced — they compile only when invoked as a function call. * Any non-call reference (e.g. `x = type`, `print foo(ObjFun)`) is a device compile error * (most produce `Syntax Error. Builtin function call expected`, hex code &h9d; * `createobject` and `tab` produce a generic `Syntax Error` &h02 with the same outcome). * Names are lowercased for case-insensitive matching. */ -export const CallOnlyBuiltins = new Set([ +export const UnreferencableBuiltins = new Set([ 'box', 'createobject', 'eval', diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index e2c56bd99..9710cc223 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -929,223 +929,7 @@ describe('parser', () => { } }); - describe('call-only builtins (`ObjFun`, `type`)', () => { - it('flags `x = ObjFun` (RHS value read)', () => { - let { diagnostics } = parse(` - sub a() - x = ObjFun - print x - end sub - `); - expectDiagnostics(diagnostics, [ - DiagnosticMessages.reservedBuiltinUsedAsValue('ObjFun') - ]); - }); - - it('flags `print type(ObjFun)` (passed as argument)', () => { - let { diagnostics } = parse(` - sub a() - print type(ObjFun) - end sub - `); - expectDiagnostics(diagnostics, [ - DiagnosticMessages.reservedBuiltinUsedAsValue('ObjFun') - ]); - }); - - it('flags `f(ObjFun, 2)` (passed by value)', () => { - let { diagnostics } = parse(` - sub a() - f(ObjFun, 2) - end sub - `); - expectDiagnostics(diagnostics, [ - DiagnosticMessages.reservedBuiltinUsedAsValue('ObjFun') - ]); - }); - - it('flags `x = type` (RHS value read)', () => { - let { diagnostics } = parse(` - sub a() - x = type - print x - end sub - `); - expectDiagnostics(diagnostics, [ - DiagnosticMessages.reservedBuiltinUsedAsValue('type') - ]); - }); - - it('does not flag `ObjFun()` (call site - falls through to scope validation)', () => { - let { diagnostics } = parse(` - sub a() - ObjFun() - end sub - `); - expectZeroDiagnostics(diagnostics); - }); - - it('does not flag `type(123)` (canonical call)', () => { - let { diagnostics } = parse(` - sub a() - print type(123) - end sub - `); - expectZeroDiagnostics(diagnostics); - }); - - it('does not flag `m.ObjFun = 1` (property assignment)', () => { - let { diagnostics } = parse(` - sub a() - m.ObjFun = 1 - end sub - `); - expectZeroDiagnostics(diagnostics); - }); - - it('does not flag `m.type = 1` (property assignment)', () => { - let { diagnostics } = parse(` - sub a() - m.type = 1 - end sub - `); - expectZeroDiagnostics(diagnostics); - }); - - it('does not flag `{ ObjFun: 1 }` (AA literal key)', () => { - let { diagnostics } = parse(` - sub a() - aa = { ObjFun: 1 } - end sub - `); - expectZeroDiagnostics(diagnostics); - }); - - it('does not flag `{ type: 1 }` (AA literal key)', () => { - let { diagnostics } = parse(` - sub a() - aa = { type: 1 } - end sub - `); - expectZeroDiagnostics(diagnostics); - }); - - it('does not flag a BrighterScript `type Name = ...` statement', () => { - let { diagnostics } = parse(` - type MyAlias = string or integer - `, ParseMode.BrighterScript); - expectZeroDiagnostics(diagnostics); - }); - - it('case-insensitive match for OBJFUN, ObjFun, objfun', () => { - let { diagnostics } = parse(` - sub a() - x = OBJFUN - y = objfun - end sub - `); - expectDiagnostics(diagnostics, [ - DiagnosticMessages.reservedBuiltinUsedAsValue('OBJFUN'), - DiagnosticMessages.reservedBuiltinUsedAsValue('objfun') - ]); - }); - - //per-builtin coverage for each device-verified entry in CallOnlyBuiltins. - //each pair: (1) bare value read flags, (2) canonical call form does not flag. - - it('flags `x = Box` (RHS value read)', () => { - let { diagnostics } = parse(`sub a()\nx = Box\nend sub`); - expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('Box')]); - }); - - it('does not flag `Box(1)` (canonical call)', () => { - let { diagnostics } = parse(`sub a()\nx = Box(1)\nend sub`); - expectZeroDiagnostics(diagnostics); - }); - - it('flags `x = CreateObject` (RHS value read)', () => { - let { diagnostics } = parse(`sub a()\nx = CreateObject\nend sub`); - expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('CreateObject')]); - }); - - it('does not flag `CreateObject("roSGNode", "Node")` (canonical call)', () => { - let { diagnostics } = parse(`sub a()\nx = CreateObject("roSGNode", "Node")\nend sub`); - expectZeroDiagnostics(diagnostics); - }); - - it('flags `x = GetGlobalAA` (RHS value read)', () => { - let { diagnostics } = parse(`sub a()\nx = GetGlobalAA\nend sub`); - expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('GetGlobalAA')]); - }); - - it('does not flag `GetGlobalAA()` (canonical call)', () => { - let { diagnostics } = parse(`sub a()\nx = GetGlobalAA()\nend sub`); - expectZeroDiagnostics(diagnostics); - }); - - it('flags `x = GetLastRunCompileError` (RHS value read)', () => { - let { diagnostics } = parse(`sub a()\nx = GetLastRunCompileError\nend sub`); - expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('GetLastRunCompileError')]); - }); - - it('does not flag `GetLastRunCompileError()` (canonical call)', () => { - let { diagnostics } = parse(`sub a()\nx = GetLastRunCompileError()\nend sub`); - expectZeroDiagnostics(diagnostics); - }); - - it('flags `x = GetLastRunRunTimeError` (RHS value read)', () => { - let { diagnostics } = parse(`sub a()\nx = GetLastRunRunTimeError\nend sub`); - expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('GetLastRunRunTimeError')]); - }); - - it('does not flag `GetLastRunRunTimeError()` (canonical call)', () => { - let { diagnostics } = parse(`sub a()\nx = GetLastRunRunTimeError()\nend sub`); - expectZeroDiagnostics(diagnostics); - }); - - it('flags `x = Pos` (RHS value read)', () => { - let { diagnostics } = parse(`sub a()\nx = Pos\nend sub`); - expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('Pos')]); - }); - - it('does not flag `Pos(0)` (canonical call)', () => { - let { diagnostics } = parse(`sub a()\nx = Pos(0)\nend sub`); - expectZeroDiagnostics(diagnostics); - }); - - it('flags `x = Run` (RHS value read)', () => { - let { diagnostics } = parse(`sub a()\nx = Run\nend sub`); - expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('Run')]); - }); - - it('does not flag `Run("pkg:/source/foo.brs")` (canonical call)', () => { - let { diagnostics } = parse(`sub a()\nx = Run("pkg:/source/foo.brs")\nend sub`); - expectZeroDiagnostics(diagnostics); - }); - - it('flags `x = Tab` (RHS value read)', () => { - let { diagnostics } = parse(`sub a()\nx = Tab\nend sub`); - expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('Tab')]); - }); - - it('does not flag `Tab(5)` (canonical call)', () => { - let { diagnostics } = parse(`sub a()\nx = Tab(5)\nend sub`); - expectZeroDiagnostics(diagnostics); - }); - - it('flags `x = eval` (RHS value read)', () => { - let { diagnostics } = parse(`sub a()\nx = eval\nend sub`); - expectDiagnostics(diagnostics, [DiagnosticMessages.reservedBuiltinUsedAsValue('eval')]); - }); - - it('does not flag `eval("print 1")` (call site - device may still error but the parser shouldn\'t flag the call form here)', () => { - let { diagnostics } = parse(`sub a()\neval("print 1")\nend sub`); - expectZeroDiagnostics(diagnostics); - }); - - //parameter-name coverage. each of these is caught by the pre-existing - //`cannotUseReservedWordAsIdentifier` (code 1045), not the new 1147. - + describe('unreferencable builtins as parameter names', () => { it('flags `function f(Box)` (parameter name)', () => { let { diagnostics } = parse(`function f(Box)\nend function`); expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('Box')]); @@ -1884,10 +1668,7 @@ describe('parser', () => { end function `, ParseMode.BrighterScript); expectDiagnostics(diagnostics, [ - //type = 1 -> assignment LHS - DiagnosticMessages.cannotUseReservedWordAsIdentifier('type'), - //return type -> non-call value read, on-device compile error - DiagnosticMessages.reservedBuiltinUsedAsValue('type') + DiagnosticMessages.cannotUseReservedWordAsIdentifier('type').message ]); }); diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index f45b5222a..368c33668 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -7,7 +7,6 @@ import { AllowedProperties, AssignmentOperators, BrighterScriptSourceLiterals, - CallOnlyBuiltins, DeclarableTypes, DisallowedFunctionIdentifiersText, DisallowedLocalIdentifiersText, @@ -3027,20 +3026,7 @@ export class Parser { return this.templateString(true); case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers): - const ident = this.previous() as Identifier; - //flag reserved call-only builtins (e.g. `ObjFun`, `type`) used in non-call position. - //these compile cleanly as values today but are device compile errors - //(`Syntax Error. Builtin function call expected`). - if ( - CallOnlyBuiltins.has(ident.text.toLowerCase()) && - this.peek().kind !== TokenKind.LeftParen - ) { - this.diagnostics.push({ - ...DiagnosticMessages.reservedBuiltinUsedAsValue(ident.text), - range: ident.range - }); - } - return new VariableExpression(ident); + return new VariableExpression(this.previous() as Identifier); case this.match(TokenKind.LeftParen): let left = this.previous();