Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
};

Expand Down
254 changes: 254 additions & 0 deletions src/bscPlugin/validation/BrsFileValidator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
21 changes: 19 additions & 2 deletions src/bscPlugin/validation/BrsFileValidator.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
});
}
}
});

Expand Down
21 changes: 21 additions & 0 deletions src/lexer/TokenKind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
57 changes: 57 additions & 0 deletions src/parser/Parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading