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/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 21b40a481..10eb915ca 100644 --- a/src/lexer/TokenKind.ts +++ b/src/lexer/TokenKind.ts @@ -577,6 +577,27 @@ export const DisallowedLocalIdentifiersText = new Set([ ...DisallowedLocalIdentifiers.map(x => x.toLowerCase()) ]); +/** + * 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 UnreferencableBuiltins = new Set([ + 'box', + 'createobject', + 'eval', + '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 52e6b40cb..9710cc223 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -928,6 +928,63 @@ describe('parser', () => { expect(diagnostics, `assigning to reserved word "${reservedWord}" should have been an error`).to.be.length.greaterThan(0); } }); + + 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')]); + }); + + 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')]); + }); + + it('flags `function f(eval)` (parameter name)', () => { + let { diagnostics } = parse(`function f(eval)\nend function`); + expectDiagnostics(diagnostics, [DiagnosticMessages.cannotUseReservedWordAsIdentifier('eval')]); + }); + }); }); describe('import keyword', () => {